
static, 프로젝트에서도 많이 쓰고, 어느정도 사용 가능하다고 생각했던 키워드인데, ‘확실하게’ 정리는 안됐다고 생각해서 애매한 내용 위주로 정리를 한 번 해보려고 한다.
당연히 Static 키워드를 사용하면 Type Property / Method가 된다. 우리가 자연스럽게 사용하는 static을 제외한 다른 프로퍼티와 함수는 Instance Property / Method라고 보면 된다.
당연하게도 우리가 알고있듯이 Instance Property / Method는 ‘인스턴스를 생성’ 할 때 메모리에 올라간다(Method는 호출 시에 호출 프레임이 stack에 저장). 그래서 항상 인스턴스를 생성한 다음 그 인스턴스에서 Property / Method를 접근하는 방식으로 사용한다.
Type Property / Method는 그 반대로, 인스턴스와는 전혀 관계없이 Type으로만 호출이 가능하다.
→ 근데 타입으로 어떻게 호출이 되는거지?? 하고 생각하던 중 발견한 사실인데 처음 타입으로 호출을 할 때 사실은 하나의 인스턴스가 만들어지고 그 인스턴스를 주소값으로 쭉 사용한다고 한다. 무슨 말이냐면
class Test {
static let testProperty = "Test"
}
let first = Test.testProperty //0x1b17c304
let second = Test.testProperty //0x1b17c304
이렇게 해서 주소값을 찍어보았을때, 인스턴스가 생성이 되고, 그 인스턴스의 주소값을 그 다음부터 쭉 사용한다고 한다!
사실 Type Property / Method에는 static말고 class 키워드도 있는데, 지난 UserDefaults 정리에서 보았던 standard부분에서 살펴볼 수 있다. (static과 class 키워드가 다른 부분은 단지 딱 하나, method overriding이 가능하다는 것이다. UserDefaults의 standard는 상속되어 override하기 위해서 class로 설정해놨다고 보면 될 것 같다. 또, 상속이 불가능한 struct나 enum에서는 아예 사용이 불가능하다.)


Property
알고있듯이 Property의 경우 Type(static, class) Property는 Data Section에 저장된다. 전역 변수처럼 관리되고, 프로그램의 Lifecycle동안 유지된다. 한 번 초기화되면 모든 인스턴스가 이를 공유한다.
(갑자기 이상한 궁금증이 들어서 전역변수로 static키워드를 설정해보았는데, 오류가 났다. Type Property이니만큼 Type안에서 설정해주어야 하는데, 전역이라 담아줄 Type이 없어서 오류가 난다.)
Instance Property는 각 인스턴스의 메모리에 저장된다. Struct일 때는 Stack에, Class일때는 Heap의 인스턴스 안에 저장된다.
Method
Type Method와 Instance Method의 경우 모두 Code Section에 저장되지만, Instance Method는 호출 시 Stack에 함수 call이 쌓이게 된다. 또한 Type Method의 경우 모든 인스턴스가 마찬가지로 동일한 메소드 코드를 공유한다.
또 갑자기 든 궁금증!
lazy는 무조건 var를 사용하는데, static의 경우 let, var 모두 사용 가능한 이유가 뭘까?
lazy는 결국 인스턴스 프로퍼티이기때문에 인스턴스가 만들어지는 시점에 메모리에 올라가야 한다. 하지만 이 때 ‘값이 없는 상태’로 올라갈 뿐. (이 ‘값이 없는 상태’라는 것은 내부적으로 정의된 부분이라 정확히 알 수는 없다. 처음엔 nil이라 생각했는데, 다시 생각해보니 c언어에서 초기화를 하지 않으면 채워져있는 garbage value같은 느낌이라고 생각하는게 더 비슷할 것 같다.) 그리고 lazy property가 불리는 시점에 값이 넣어지게 된다. 이러한 값변동이 일어나기 때문에 var만 가능하다.

‘Static은 내부적으로 lazy로 구현되어있다’. 이 개념때문에 사실 이부분을 다시 정리했다. lazy로 구현되어있으면 let이 불가능한 것은 똑같을텐데…???
계속 생각을 하다가, “lazy의 특성을 갖고 있어 사용시점에 메모리에 올라가는 부분은 똑같지만, 완전하게 lazy와 동치라고 생각하면 조금 오류가 있다.” 라고 결론이 났다. 인스턴스와 관계가 없는 타입 프로퍼티이다보니 인스턴스 생성 시점에 다른 값을 가질 필요도 없고, 사용시점에만 메모리에 올라가면 되니까 변경이 필요없을 때는 let, 변경이 필요할 때는 var을 둘 다 가질 수 있는 것이다.
타입 연산 프로퍼티는 상관 없지만, 타입 저장 프로퍼티일 경우에는 인스턴스 생성시마다 매 번 생성되는 인스턴스 프로퍼티와는 다르게 한 번 메모리에 올라가면 앱이 종료될 때까지 언제 어디서든 생성된 이 하나의 타입 프로퍼티에 접근한다. == Initializer가 없다. 이로 인해 static stored property는 초기값이 필요하다.
새롭게 알게 된 사실!
extension UICollectionViewCell: Identifier {
static var identifier: String {
return String(describing: self)
}
}
이와 같은 Computed Property의 경우 조금 헷갈렸는데, 일단 연산 프로퍼티이므로 Data 영역에 메모리를 차지하지는 않는다. 하지만 함수의 실행과정과 비슷하게 함수Call이 왔을 때 Code영역에 기계어로 변환된 이 로직이 값을 방출하는 형식으로 작동하게 된다.
static에 대한 개념적인 부분들을 그래도 얼추 정리를 한 것 같다. 그러면 static을 어디에서 사용하는 것이 효율적일까? 아 static 잘 사용했구나! 하고 느낄 수 있을만한 사용처들을 살펴보자.
App Configuration
앱을 만들다보면 어떤 색을 사용했었는지 일일히 전 화면들을 찾아보는 경우가 자주 있다. 또 전체 Color나 Font Style을 바꿔야하는 일이 있다면 모든 코드를 순회하면서 변경을 해야 할 것이다. 따라서, 코드 전체에 직접 하나씩 명시를 해주는 것보다 한 곳에서 정의해주는 것이 좋은데, 이 때 static을 사용하면 효율적이다.
enum AppStyle {
//App Color
enum Color {
static let main = UIColor(red: 245/255, green: 252/255, blue: 252/255, alpha: 1)
static let sub = UIColor(red: 77/255, green: 106/255, blue: 120/255, alpha: 1)
}
//App Font
enum Font {
static let title = UIFont.boldSystemFont(ofSize: 18)
static let main = UIFont.boldSystemFont(ofSize: 15)
static let sub = UIFont.systemFont(ofSize: 13)
}
}
여기서 Struct가 아니라 Enum을 사용한 이유!
바로 인스턴스로 만들지 못하도록 하기 위해서이다. 인스턴스를 잘못해서 만들어버리면 static으로 구현해놓은 변수들이 의미가 없어지기도 하고, 불필요한 기능이 있는 것이기 때문에 깔끔하게 만들기 위해서 만들지 못하도록 하고싶은데, Enum타입은 initializer를 가지고 있지 않기 때문에 이 목적에 정확히 부합한다.
물론 Struct에서 priviate init을 설정해줘도 똑같겠지만, 이게 더 깔끔하니까..
Expensive Object
어떤 Object, 예를들어 DateFormatter()같은 객체를 생성하는데에 비용이 많이 든다면, static을 통해Cache로 사용할 수 있다.
struct Post {
private static var dateFormatter = ISO8601DateFormatter()
let publishDate: Date?
init(publishDateString: String) {
self.publishDate = Post.dateFormatter.date(from: publishDateString)
}
}
Post가 몇 개나 생성될지 모르는 상황에서, dateFormatter를 매 번 생성해준다면 리소스 낭비가 될 것이다. static property로 만들면 아까 알아봤던 것처럼 사용할 때마다 같은 주소값을 가진 Instance가 재사용될 것이므로 효율적으로 사용될 수 있다.
Factory Pattern
디자인 패턴 중 Factory Pattern이 있다. 쉽게 말하면 객체에서 직접 인스턴스를 생성하는 것이 아니라 Factory라는 객체에게 생성 작업을 맡겨서 의존성을 주입받는 방법이다.
복잡한 비즈니스 로직이나 세부사항이 있을 때 이를 객체로부터 분리할 수 있다는 이점을 가지고 있다.
enum PostFactory {
static func create(title: String, body: String) -> Post {
let metadata = Metadata(complexMetadatas...)
return Post(title: title, body: body, metadata: metadata)
}
}
여기서도 인스턴스를 만들 이유가 없으므로 Enum타입으로 Factory를 생성했다. 물론 실제 Factory Pattern을 사용할 때에는 여러 다른 비즈니스 로직에 따라 state를 구분해서 메소드를 만들기 때문에 static을 사용하지 않지만 위처럼 간단하게 하나의 방법으로 인스턴스를 만들때는 이렇게 static을 사용한 정적 메소드로 생성하는 것도 좋은 방법이다.
extension PostFactory {
static func defaultPost() -> Post {
return Post(title: "default", body: "")
}
}
또한, 데이터의 초기값을 설정할 때, default value를 통해서 placeholer등의 역할로 사용해야 할 경우가 종종 있는데, 이러한 경우 또한 type method를 사용하여 생성하는 편이 instance method보다 더 직관적이다.
Shared instance
URLSession.shared, UserDefaults.standard, NotificationCenter.default, DispatchQueue.main 개발하면서 정말 많이 본 object들일 것이다. 저번 UserDefaults 포스트에서도 잠깐 언급한 적이 있는데, 이들은 엄밀히 말하자면 싱글톤(Singleton)이 아니라 공유 인스턴스(Shared Instance)이다.
왜냐하면 이 모든 object들은 각각의 instance 생성이 가능하기 때문이다. 또한 우리도 이러한 목적으로 많이 사용을 해보았을 것이다.
struct NetworkingObject {
let urlSession: URLSession
init(urlSession: URLSession = URLSession.shared) {
self.urlSession = urlSession //원하는 urlSession 생성해서 사용
}
}
Singleton이 아니라고 해서 Shared Instance가 나쁜 것은 아니다. 인스턴스를 생성할 수 있는 능력은 잃지 않으면서 동시에 공유 인스턴스 또한 가질 수 있어서 장점 또한 많다.
(편의상 후술되는 곳에서는 Shared Instance = Singleton으로 기술한다.)

이러한 공유 인스턴스(Singleton)는 한 번만 생성되므로 메모리 낭비를 방지하고, 공통된 객체를 여러 개 생성하는 것 대신에 전역 Instance로 다른 클래스들과 자원 공유가 용이하다는 장점이 있다.
또한, Multi-Thread 환경에서 싱글톤 인스턴스 생성시, static은 앞서 알아본 것과 같이 lazy로 작동하기 때문에, ‘인스턴스 생성’ 관점에서 Thread-Safe하다. (우연히 여러개의 인스턴스가 생성될 걱정을 하지 않아도 된다.)
이렇게 좋은 점들만 있으면 다들 Singleton 썻겠지.. 단점도 알아보자.
다들 Thread-Safe하다고만 적어서 좀 의아했는데, 이 Thread-Safe는 ‘생성’ 관점에서만 안전한 것이지, Multi-Thread 환경에서 Singleton 객체 Read-Write를 하게 되면 당연히 Data Race가 일어날 수 있다.
또한, Singleton Pattern은 OOP의 SOLID원칙 중 하나인 개방-폐쇄 원칙(Open-Closed Principle)을 위배하기 때문에 유명한 Anti-Pattern으로 알려져있기도 하다. → 너무 많은 곳에서 Singleton Instance가 쓰이면 다른 Class Instance들간의 결합도가 높아진다.
개방-폐쇄 원칙(Open-Closed Principle) - Class등 모듈은 확장에 대해서는 열려 있어야 하고, 수정에 있어서는 닫혀 있어야 한다.
static 정적 프로퍼티/메소드가 너무나 많은 곳에서 쓰이다보니 lazy, Factory Pattern, Singleton, Enum등 정말 다채롭게 주변 개념까지 이해를 할 수밖에 없었던 것 같다. 봐도 항상 모호하게만 느껴졌던 SOLID원칙, Pattern같은 개념들을 점점 접하고 이해가 되는 것 같아서 뭔가 뿌듯하다. 한 발짝씩 다가가야지.
참조
스위프트에서 static 키워드란? 'static' in swift
(Swift) 인스턴스 메서드 , 타입 메서드 (instance method, type method)
[Swift] static과 class method, property 효과적으로 사용하기