Swift - Lazy Stored Properties 좀 더 살펴보기

이미 Swift의 프로퍼티(Properties)에 관한 내용 중 '나중에 생성되는 프로퍼티(Lazy Stoed Properties)' 항목에서 간략하게 설명했었지만, 조금은 더 실생활(?)에 도움이 되는 예제가 필요하다는 생각이 들었다. 그래서 이 lazy 프로퍼티를 약간 더 자세히(?) 정리해 본다.

lazy의 일반적인 뜻인 '게으르다' 라는 표현은 좀 어울리진 않는다. 정확히 말해서 '참조될 때 생성되는 프로퍼티' 라는 표현이 직관적이다. 이 말의 의미 처럼 이 형식의 프로퍼티는 참조를 하기 전 까진 실제로 생성되지 않는다.

Lazy 동작 실험

간단한 예제를 하나 보자.
class SomeYear {
    let year: Int
    lazy var nextYear: SomeYear = self.getNextYear()
    
    init(year: Int) {
        self.year = year
    }
    
    func getNextYear() -> SomeYear {
        println("Getting Next Year")
        return SomeYear(year: self.year + 1)
    }
}
'왜 코드가 병신같냐' 라고 생각된다면 이미 잘 알고 있는 것이니 안읽어도 될 것 같은 약간은 불만인 코드이다. 물론 일부러 이렇게 만들었다. :-)

어쨌거나, SomeYear 클래스의 nextYear 라는 프로퍼티가 이번 글의 대상이다.

위 코드를 아래와 같이 실행시켜 보자.
let sy = SomeYear(year: 2015)
sy.nextYear    // 1
sy.nextYear    // 2
sy.nextYear    // 3
nextYear 프로퍼티를 총 3번 참조하는 코드이다. 이 코드가 실행되면 "Getting Next Year" 라는 로그가 단 한번만 출력된다.

즉, nextYear 프로퍼티는 첫 참조가 시작되는 시점(1)에 초기화 되어서 값이 들어가게 된다. 실제로 들어가게 되는 값은 getNextYear() 라는 메소드가 리턴하는 값이다.

이 후의 참조(2, 3) 과정에서는 getNextYear() 가 호출되지 않는다.

결과적으로 nextYear 프로퍼티는 참조가 필요할 때에 딱 한 번만 getNextYear()의 리턴값을 가지고 생성되며 이 후 이 값을 지속적으로 보관하며 참조 할 수 있다. 일종의 캐시(Cache) 형태로 동작한다고도 볼 수 있다.

만약 Objective-C로 이런 코드를 구현한다고 생각해보자. 아마도 아래와 같은 형식과 비슷하다고 생각하면 될 것 같다.
@interface SomeYear: NSObject
@property (assign) NSInteger year;
@property (nonatomic, strong) SomeYear *nextYear;

- (id)initWithYear:(NSInteger)year;

@end

@implementation SomeYear
@synthesize nextYear = _nextYear;

- (id)initWithYear:(NSInteger)year {
    self = [super init];
    if (self) {
        self.year = year;
    }
    return self;
}

- (SomeYear *)nextYear {
    if (_nextYear == nil) {
        _nextYear = [[SomeYear alloc] initWithYear:self.year + 1];
    }
    return _nextYear;
}

@end
헉헉 길다. Swift의 간결함이라는 파워가 느껴진다. 이 코드는 그냥 구현체 비교를 위한 것이니 Objective-C에 관심이 없다면 잊어버리자. =_=

클로져를 이용한 초기화

앞의 예제는 아래와 같은 식으로 좀 더 엘레강트하게(?) 교체가 가능하다.
class SomeYear {
    let year: Int
    lazy var nextYear: SomeYear = {
        println("Getting Next Year")
        return SomeYear(year: self.year + 1)
    }()
    
    init(year: Int) {
        self.year = year
    }
}
메소드의 내용을 클로져로 옮겼다. 아마도 앞의 코드보단 위 코드가 훨신 가독성이 좋을 것이다.

별도로 설명은 안하겠다. 왜냐하면 완전히 동일한 동작을 하는 코드이기 때문이다.

다만, 마지막의 괄호를 잊지 말자. lazy 프로퍼티는 클로져를 담는 변수가 아니다.

ARC 이슈의 해결?

과연 이게 필요한지는 잘 모르겠다. 우선은 코드를 보자.
class SomeYear {
    let year: Int
    lazy var nextYear: SomeYear = {
        [unowned self] in
        println("Getting Next Year")
        return SomeYear(year: self.year + 1)
    }()
    
    init(year: Int) {
        self.year = year
    }
}
nextYear의 값을 생성하는 루틴 내부에서 unowned self 라는 캡쳐리스트를 사용하고 있다. 따라서 클로져가 self에 참조를 걸지 않으니 불필요한 참조 카운트 증가가 발생하지 않게 된다.

다만, 아직까지도 이 클로져의 소유 주체가 누구인지를 잘 모르겠다. 꼭 필요한 걸까? 하지만 써도 문제는 없으니 아마도 있는게 좋은 것 같기는 하다. -_-; 대충 예상으로는 언제 어떻게 누군가에(?) 의해 실행될 지 알 수가 없으니 미리 self에 대한 강한 참조를 하지 않도록 예방하는 거라고 생각된다.

개인적으론 이런 식으로 코딩하도록 습관이 들이려 노력하는 중이다.

읽기전용으로 만들기

상황에 따라 다르긴 하겠지만, Immutable한 클래스를 설계하고 싶다면 이런 lazy 프로퍼티도 let으로 선언하는게 맞다.

하지만 불행히도 lazy 프로퍼티는 var 로만 선언이 가능하다. 뭐 나중에 바뀔 가능성이 없는건 아니겠지만, 현재 Swift 1.1에선 var로만 만들 수 있다.

그리고 getter 만을 제공하는 방식도 불가능하다. 이름을 보면 알겠지만 Lazy Stored Properties, 즉 값을 생성해서 보관하기 위한 용도의 프로퍼티이지 동적인 getter를 제공하기 위한 용도가 아니기 때문이다.

결국 상수(let)나 getter 형식은 불가능하다. 대신, 이 프로퍼티를 읽기 전용(read-only)으로 만들 수 있다.
class SomeYear {
    let year: Int
    private(set) lazy var nextYear: SomeYear = {
        [unowned self] in
        println("Getting Next Year")
        return SomeYear(year: self.year + 1)
    }()
    
    init(year: Int) {
        self.year = year
    }
}
private 라는 접근 제어자(Access Controller)는 이 코드가 선언된 파일 외부에서는 접근이 불가능하게 만드는 제어자이다. 그런데 여기서 성향(?)을 설정 할 수도 있다. 'private(set)' 이라는 것을 명시하게 되면 set 즉 값을 대입하는 상황에선 private로 동작하고, 이 외에는 기본동작(internal)으로 제어된다.

아마도 이 항목이 가장 유용한 팁인 것 같다. -_-;

관련글:

댓글

이 블로그의 인기 게시물

버전(Version)을 제대로 이해하기

소수점 제거 함수 삼총사 ceil(), floor(), round()