[Swift] static과 class method, property 효과적으로 사용하기
들어가기 전에
static property와 method를 언제 사용하면 좋을지 아주 혼란스러워하던 중 너무 잘 정리 되어있는 글을 발견했다. 그래서 따로 번역으로 빼서 작성한다. 각 잡고 번역하느라 허락까지 받았다. 그러니까 이거에 대해서 궁금하셨던 분들은 읽어보시면 좋을것 같습니다 ~.~
그럼 시작 ㄱㄱ
시작하며
Swift는 method와 property에 static
접두사를 사용하여 인스턴스(instance)가 아닌 선언된 type과 연결할 수 있도록 합니다. static property를 이용하여 object의 singleton 또한 만들 수 있는데, 이것은 아마 들어봤듯이 거대한 안티 패턴(anti-pattern)입니다. 그렇다면 인스턴스(instance)가 아닌 type에 정의된 property나 method를 언제 사용해야 할까요? 이 글에서는 정적 프로퍼티와 메소드에 대한 몇 가지 사용 사례를 다룹니다. 그리고 공유 인스턴스 (shared instance)와 singleton에 대해 살펴봅니다.
설정(configuration) 을 위한 static property 사용하기
static property의 가장 일반적인 사용 사례는 환경 설정(configuration) 입니다. 이러한 목적을 갖는 static property는 UIKit
전역에서 찾아볼 수 있습니다. 이것들이 static한 String으로 정의되는 주된 이유는 그것이 네임스페이스(개체를 구분할 수 있는 범위) 를 제공하기 때문입니다. 일반적인 예시는 정적 속성이 있는 개체를 스타일 가이드로 사용하는 경우입니다. 특정 색상이나 글꼴 크기를 변경하기 위해 전체 앱을 일일히 살펴봐야 한다면 무조건 누락되는 부분이 생길 것입니다. 따라서 코드 전체에 값을 분산시키는 것보다 한 곳에서 색상이나 글꼴 크기를 정의하는 것이 좋습니다. 아래의 설정 코드를 봅시다.
enum AppStyles {
enum Colors {
static let mainColor = UIColor(red: 1, green: 0.2, blue: 0.2, alpha: 1)
static let darkAccent = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
} enum FontSizes {
static let small: CGFloat = 12
static let medium: CGFloat = 14
static let large: CGFloat = 18
static let xlarge: CGFloat = 21
}
}
메인 컬러를 살짝 수정해야 한다면 한 부분에서만 변경하면 됩니다. 설명적인 이름(descriptive name)을 사용하여 색상의 이름을 지정하면, 화면의 시각적인 표현에 구속되지 않습니다.
여기서 enum
을 통해 static
property를 사용하고 있는 이유는 설정 object를 인스턴스로 만들지 못하도록 하고 싶었기 때문입니다. enum
은 initializer을 가지고 있지 않기 때문에 해당 목적에 부합합니다. struct
에서 private
initializer를 써도 똑같이 동작할테지만 enum
이 해당 작업에 더 부합하다고 생각합니다.
static property를 통해 iOS의 Notification Center를 통해 전송되는 알림에 사용할 수 있는 제한된 문자열 집합을 정의하거나, 애플리케이션 전체에서 일정하게 유지되어야 하는 전역 설정(global configuration)을 configuration object를 전달할 필요 없이 사용할 수 있습니다.
비싼(expensive) 객체에 대해 static property 사용하기
static property의 또 다른 용도는 그것들을 캐시로 사용하는 것입니다. 특정 object를 생성하는 것은 많은 비용이 들 수 있기 때문입니다. static property에 보관할 수 있는 값비싼 객체의 좋은 예는 dateformatter
입니다.
struct Blogpost {
private static var dateFormatter = ISO8601DateFormatter()
let publishDate: Date?
// other properties ...
init(_ publishDateString: String /* other properties */) {
self.publishDate = Blogpost.dateFormatter.date(from: publishDateString)
}
}
Blogpost
인스턴스를 얼마나 많이 많들던 간에, string 을 변환하는 date formatter는 언제나 ISO8601DateFormatter
가 될 것입니다. date formatter는 생성 비용이 많이 들고, 아무런 영향(consequences) 없이 재사용될 수 있기 때문에 이것을 static property로 생성할 수 있습니다. 이렇게 date formatter를 static하게 만들어서 type과 연관시키는 대신, Blogpost
의 instance와 연관시킨다면, Blogpos
instnace가 생성될 때마다 date formatter도 같이 생성될 것입니다. 동일한 date formatter가 중복적으로 만들어 지는 것은 상당히 낭비적입니다.
따라서 생성하는데 비용이 많이 들면서, 안전하게 재사용될 수 있는 object에 대해서는 정적으로 정의하는 것이 좋습니다. 그러면 해당 type에서는 비싼 object를 매번 생성하는 대신 한번만 생성하고 사용할 수 있습니다.
static method로 factory 만들기
프로그래밍에서 일반적인 패턴은 factory 패턴입니다. factory는 대상 object의 initializer에 대한 특정 세부사항을 숨기면서도, 간단하게 복잡한 object를 만들 수 있습니다. 예시를 봅시다.
enum BlogpostFactory {
static func create(withTitle title: String, body: String) -> Blogpost {
let metadata = Metadata(/* metadata properties */)
return Blogpost(title: title, body: body, createdAt: Date(), metadata: metadata)
}
}
여기서 좋은 점은 위의 BlogpostFactory
를 사용하여 Blogpost
의 새로운 인스턴스를 만들 수 있다는 것입니다. 상황에 따라 (factory와 관련된 상태(state)가 있는 경우 등) 이런 식으로 factory를 만들고 싶지 않을 수도 있지만 위와 같이 간단한 경우에는, object의 인스턴스를 만드는 간단한 static method를 사용하는 것이 나을 것입니다. 블로그 게시 또는 양식(form)의 기본 시작점을 만들기 위해 type에 default
static method를 사용할 수도 있습니다.
extension Blogpost {
static func sensibleDefault() -> Blogpost {
return Blogpost(title: "Hello, world!",
body: "Hello, sample body",
createdAt: Date())
}
}
사용자가 새로운 게시물을 만들려고 할 때마다 이 default()
static method를 사용하여 placeholder object를 만들 수 있습니다.
static method는 특정 method를 인스턴스가 아닌 type과 연결하려는 경우에 유용합니다. blog post 인스턴스를 만드는 defaultBlogpost()
라는 함수를 만들 수도 있지만, default()
메소드를 Blogpost
와 직접 연관시키는 것이 훨씬 낫습니다.
class method와 static method의 차이
지금까지는 static method에 대해 설명해왔습니다. 스위프트의 class에서는 class method를 사용할 수도 있습니다. class method도 인스턴스보다는 type과 관련 되어 있습니다. 하지만 하위 클래스(subclass)가 class method를 재정의(override)할 수 있다는 점이 주된 차이점입니다.
class SomeClass {
class func date(from string: String) -> Date {
return ISO8601DateFormatter().date(from: string)!
}
}class SubClass: SomeClass {
override class func date(from string: String) -> Date {
return DateFormatter().date(from: string)!
}
}
SubClass
는 다른 date formatter를 사용하기 위해 슈퍼클래스의 date(from:)
를 override
합니다. 이런 재정의 행동은 class method만 가능하고, static method는 불가능합니다.
shared instance는 언제 만드는가
지금까지 설정(configuration)이나 비싼 object에 static property를 사용할 수 있다는 것과, 어떻게 static method를 사용하여 간단한 factory를 만들 수 있는지 살펴 보았습니다. 여기서 더 논란이 되는 주제는 바로 공유 인스턴스에 관한 것입니다. 아마 이 중 몇 가지는 사용해 본적이 있을 것입니다.
URLSession.shared
UserDefaults.standard
NotificationCenter.default
DispatchQueue.main
이들은 모두 공유 인스턴스의 예시입니다.
각 object는 default
를 갖고 있는 static property가 있거나, type의 shared instance가 있습니다. 이 object들을 singleton
이라고 생각한다면 잘못 된 것입니다. 왜 그런지 살펴봅시다.
싱글톤은 오직 한 가지 인스턴스만 가질 수 있는 object입니다. Swift에서는 static property로 정의되는 경우가 많지만, 다른 언어에서는 단순히 이니셜라이저(types initializer)를 사용할 수도 있고 동일한 인스턴스를 반복해서 얻을 수도 있습니다.
type의 static property는 singleton
이라기 보다는 공유 객체(shared instance)입니다. 왜냐하면 모든 object에 대해 각각의 instance를 만들 수 있기 때문입니다. 우리는 우리만의 UserDefaults
저장소(store)나, URLSession
를 자유롭게 만들 수 있습니다.
이러한 object 에서 제공되는 공유 인스턴스는 단지 제안 사항일 뿐입니다. 이러한 것들은 완전히 구성 되었거나(configured), 신속하게 시작하고 실행하는 데 사용할 수 있는 유용한 인스턴스입니다. DispatchQueue.main
또는 NotificationCenter.default
와 같은 공유 인스턴스는 앱에서 특정 목적을 갖습니다. 예를 들어, 모든 UI 조작은 DispatchQueue.main
이 사용되고, UIKit 및 시스템에서 보낸 모든 알림은 NotificationCenter.default
가 사용됩니다.
공유 인스턴스를 사용할 때에도, 다른 인스턴스를 사용할 수 있는 경우를 대비해 예비 방안(built-in escape hatch)을 만들어 두세요. 이에 대한 예시를 살펴 봅시다.
struct NetworkingObject {
let urlSession: URLSession init(urlSession: URLSession = URLSession.shared) {
self.urlSession = urlSession
}
}
NetworkingObject
는 요청(request)을 보내기 위해 URLSession
을 사용합니다. initializer는 default URLSession
을 가지고 있으면서도, URLSession
인스턴스를 받습니다(accept). 이는 대부분의 경우 URL 세션을 명시적으로 전달하지(passing) 않고도, NetworkingObject
의 새 인스턴스를 만들 수 있음을 의미합니다. 단위 테스트(unit test) 등에서 만약 다른 URLSession
을 사용하기로 결정했다면, 그저 NetworkingObject
의 이니셜라이저에 전달하면 됩니다. 그때부턴 custom URLSession
을 사용하게 될 것입니다.
공유 인스턴스는 하나의 인스턴스만 필요로하는 객체에 매우 유용하며, 이는 사전 설정되어 앱 전체에서 쉽게 액세스 할 수 있습니다. 이것의 주요 이점은 자체 인스턴스를 생성 할 수 있다는 것입니다. 즉, 필요한 경우 자체 상태를 갖는 고유한 인스턴스를 생성 할 수 있는 능력을 잃지 않으면서 공유 상태의 공유 인스턴스를 갖는 이점을 얻을 수 있습니다.
singleton은 언제 사용해야 하는가
개발 커뮤니티 전체에 걸쳐 싱글톤은 안티 패턴(anti-pattern)으로 알려져 있습니다. 저는 개인적으로 싱글톤보다 공유 인스턴스를 선호합니다. 왜냐하면 공유 인스턴스를 사용해도, 필요할 경우 자신만의 object 인스턴스를 만들 수 있기 때문입니다.
그러나 저는 Swift에서 싱글톤 패턴을 사용하는 책임 있는(responsible) 방법이 있다고 생각합니다. 하나의 인스턴스만 가지는 것이 싱글톤의 유일한 구현 요구 사항이라고 할 때, 다음과 같은 코드를 작성할 수 있습니다.
protocol Database {
/* requirements */
}struct AppDatabase: Database {
static let singleton = AppDatabase() private init() {}
}struct UserProvider {
let database: Database
}
위의 코드에서 싱글톤은 프로토콜(Database
)을 따릅니다(conform). 데이터베이스를 사용하는 object에는 Database
를 따르는(conform) object가 필요한 속성이 있습니다. 데이터베이스에 접근할 때 싱글톤의 singleton
속성에 액세스하지 않고 대신 UserProvider
및 데이터베이스의 다른 사용자에게 주입하면 싱글톤은 다른 종속성(dependency)처럼 사용됩니다. (무슨 말인지 모르겠다!)
그렇다면 왜 AppDatabase
를 싱글톤으로 만들까요? 라고 물어볼 수도 있습니다. 이유는 간단합니다. read/write 메커니즘이 제대로 갖춰져 있지 않으면서 데이터베이스의 인스턴스가 두 개 있는 경우, 두 개체가 동시에 기본 스토리지(storage)에 write 할 수 있습니다. 따라서 AppDatabase
의 인스턴스를 하나만 만들 수 있도록 싱글톤으로 구현해야합니다.
해당 접근 방식의 주요한 단점은 우리가 싱글톤을 사용하고 있다는 사실을 숨기기 위해 싱글톤 인스턴스(instance)를 주입하는 의존성 주입(dependency injection)을 사용해야 함에도 불구하고 싱글톤 property를 사용하도록 권장할 수 있다는 것입니다. 하지만 이것이 코드 리뷰를 위한 것이고, 만약 팀원 모두가 이렇게 싱글톤을 사용해도 괜찮다고 동의한다면, 그대로 진행할 수 있습니다.
싱글톤은 여전히 안티 패턴(anti-pattern)이지만, 제가 여기서 설명한 것은 단점이 제한적(limited)이고 고립(isolated)되어 있다고 생각하는 사례라는 것을 기억하세요.
마치며
이 게시물에서는 정적 속성을 사용하여 인스턴스 레벨이 아닌 type 레벨에서 설정을 정의하는 방법을 보여 주었습니다. 또한 정적 속성이 자주 재사용되고 생성 비용이 많이 드는 물체를 저장하는 데 좋다는 것을 보여주었습니다. 다음으로, 정적 메소드를 사용하여 어떤 종류의 factory를 구현할 수 있는지 보고, class method가 static method와 어떻게 다른지 살펴보았습니다.
이후 공유 인스턴스(shared instance)와 singleton을 탐구해 보았습니다. 필요할 경우 공유 인스턴스를 제공하는 object의 인스턴스를 직접 만들 수 있기 때문에 공유 인스턴스가 singleton보다 더 좋은 경우가 있다고 말했습니다. 그리고 나서 만약 singleton이 프로토콜을 구현(implement)하고, 싱글톤의 명시적 유형이 아닌, 프로토콜에 의존하는 object의 이니셜라이저에 싱글톤을 주입한다면 싱글톤이 그렇게 나쁘지 않을 수 있다는 것을 보여주었습니다.
만약 피드백 및 제안이 있거나 singleton 또는 shared instance에 대해 이야기 하고 싶다면 트위터로 연락주세요.