(Swift) 클로저 내부에서 self를 캡쳐하는 방법

항상 [unowned self]를 써야 할까요?

http://stackoverflow.com/questions/24320347/shall-we-always-use-unowned-self-inside-closure-in-swift 의 내용중 일부를 번역하였습니다.

아니오, 분명히 [unowned self]를 쓰지 말아야할 상황이 존재합니다. 특정한 클로져가 호출되었을 때에
그 시점까지 self가 파괴되지 않고 살아있음을 보장할 필요가 있을 때가 있단 말이죠.

예를 한가지 들어봅시다. 비동기 네트워크 요청을 하고, 요청에 대한 응답을 받았을 때 self로부터
어떤 무언가를 호출해야 한다고 가정해봅시다. 비동기 요청이 끝나는 시점 이전에 해당 객체가 해제되어 사라졌다면
프로그램이 뻗고 말겁니다.

그렇다면 언제 unowned selfweak self를 써야하는가? 바로 강한 참조 사이클이 만들어질 때입니다.
이는 두개 이상의 객체가 서로 순환하면서 서로를 순서대로 참조해나갈 때 생길 수 있습니다. 이 경우 사이클 내의 객체들은 어떤 경우에도 항상 참조수 1 이상을 갖기 때문에 파괴되지 않고 리소스를 낭비하게 됩니다.

클로저의 경우, 클로저가 캡쳐한 – 즉 클로저 내에서 선언되지 않았지만, 사용은되는- 모든 객체는 클로저에 의해 ‘소유’됩니다. 따라서 클로저가 남아있다면, 해당 객체는 제거되지 않습니다. 이 순환고리르 끊는 방법 역시 [unowned self][weak self]를 쓰는 것입니다. 만약 어떤 클래스가 클로저를 소유하고, 클로저가 또 해당 클래스에 대해 강한 참조를 가지면 클래스와 클로저 사이에도 강한 참조 사이클이 발생하게 됩니다. 혹은 클로저가 다른 객체를 소유하고, 해당 객체가 다시 클로저를 소유하고, 클로저는 다시 해당 객체를 소유하는 3각 이상의 관계에서도 적용될 수 있습니다.

이런 예들에 대해서 애플 공식 문서는 아주 훌륭한 예를 들어서 설명하고 있습니다.

https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html

먼저 아래의 예를 보겠습니다. 아래 HTML 클래스는 asHTML이라는 메소드 를 가지고 있습니다. 메소드는 클로저의 일종이기는 합니다만, 메소드 인스턴스 객체는 참조 순환을 만들지 않습니다. 왜냐하면 메소드가 호출되는 시점에는 항상 객체 인스턴스가 유효해야 하므로 굳이 캡쳐링할 필요가 없기 때문이죠.

class HTML {
    let tagName: String
    let defaultContent: String = "hello, world"
    var content: String?

    init(tagName: String) {
        self.tagName = tagName
    }

    func asHTML() -> String {
        return "<\(self.tagName)> \(self.content ?? self.defaultContent) </\(self.tagName)>"
    }

    deinit {
        print("\(tagName) is deallocated...")
    }
}

var h: HTML? = HTML(tagName:"h1")
print(h?.asHTML())
h = nil

따라서 아래와 같이 실행하면 해당 객체에 대한 참조가 사라지는 시점에 객체가 해당된다는 메시지가 출력됩니다.

var h: HTML? = HTML(tagName: "H1")
print(h!.asHTML())
h = nil

/// <h1> hello, world </h1>
/// h1 is deallocated...

하지만 아래와 같이 이를 클로저로 바꾼다면 어떨까요?

class HTML {
    let tagName: String
    let defaultContent: String = "hello, world"
    var content: String?

    init(tagName: String) {
        self.tagName = tagName
    }

    lazy var asHTML: () -> String = { 
        return "<\(self.tagName)> \(self.content ?? self.defaultContent) </\(self.tagName)>"
    }

    deinit {
        print("\(tagName) is deallocated...")
    }
}

var h: HTML? = HTML(tagName:"h1")
print(h!.asHTML())
h = nil

이 때는 해당 객체가 자동으로 제거되지 않습니다.

여기서 잠깐, 왜 lazy한 프로퍼티로 설정했냐 하면, lazy하지 않은 프로퍼티로 만드는 경우에 해당 객체의 초기화가 끝나지 않은 시점에 self를 참조하는 셈이 되므로 이는 초기화 룰을 위반하기 때문입니다.1

아무튼 이 경우에는 자체적으로 객체와 클로저(어쨌든 이 클로저는 호출되려는 시점에 생성됩니다.)가 서로 순환 참조를 가지게 되므로, 다음과 같이 변경해야 합니다.

lazy var asHTML: () -> String = { [unowned self] in 
  return "<\(self.tagName)> \(self.content ?? self.defaultContent) </\(self.tagName)>"
}

  1. lazy하지 않은 모든 저장 프로퍼티는 초기화가 끝나기 전까지 초기값을 가져야하며, 초기화 중에는 self를 쓰는 것이 위험합니다. 특히 init()외부에서 초기화되는 경우에 이는 초기화 과정 이전에 실행되는 메소드이므로, [unowned self] in은 반드시 필요합니다. 

(Swift) flatMap에 대해

flatMap에 대해서 뭔지 모르겠다는 이야기들이 들려와서 나름대로 한 번 정리해본다.

map, flatMap

배열의 메소드 중에는 map()이 있다. 먼저 이 메소드의 타입 시그니처를 확인해보자.

func map<T>(_ transform: @noescape (Element) throws -> T) rethrows -> [T] 

이 메소드는 배열의 원소타입을 받아 임의의 타입 T의 값으로 변환하는 변환함수를 인자로 받고, 배열의 각 원소에 대해서 transform을 적용한 결과를 가지고 새로운 배열을 만드는 함수이다.

map 함수는 (아마도 jQuery로부터 유명해지기 시작해서) 워낙에 흔히 쓰이기도 하는 함수이기 때문에 마치 물과 공기처럼 당연하게 취급하기 쉬운데, 여기서 중요한 점을 하나 짚어보고 가야한다. 그것은 map이라는 함수 그자체 보다는 map을 적용받는 배열 그 자체에 대한 것이다.

하나의 배열은 여러 개의 값을 묶은 집합으로 흔히 사용된다. 하지만 또 다른 맥락에서 배열은 값이 하나 이상이될 수 있다는 계산적인 맥락을 내포한다. 즉 배열은 하나 이상의 값이 될 수 있도록 어떤 값을 둘러싸는 wrapper로 기능하고 있다는 점이다. 따라서 map은 수학적으로 어떤 “집합”에 대한 연산이 아니라 그 아래에 어떤 값을 내포할 수 있는 “계산의 맥락”에 대한 연산인 셈이다.

만약 어떤 값이 어떤 컨테이너나 래퍼에 의해 감싸져 있을 때, 그것이 비단 배열이 아니더라도 map과 같은 동작을 할 수 있는 경우가 더 있을 것이다. 이러한 이렇게 특정한 함수를 내부 구조에 사상할 수 있는 컨테이너를 수학에서는 functor라 부르며, 이것은 단순히 말그대로 “mapped over”될 수 있는 컨테이너라는 뜻이다.

Swift의 기본 타입 중에서 이러한 functor의 한 종류로는 옵셔널이 있다. 옵셔널은 기본적으로 여러 문법적 장식을 통해서 Swift에 녹아들어있기는 하지만 다음과 같이 열거 타입으로 정의되어 있다.

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

이 때 .none은 우리가 흔히 알고 있는 nil이며 .some()은 nil이 아닌 값이 옵셔널이라는 컨테이너에 들어있는 것으로 보며, 옵셔널 컨테이너는 값이 있을 수도 있지만 없을 수도 있는이라는 의미를 추가적으로 갖는 컨테이너이다.

이 때 값이 없다는 것은 수학적인 의미이다. 예를 들어 let emptyString = ""이라는 표현에서 emptyString은 값이 없는 것이 아니라 비어있는 문자열 값을 갖고 있다. 왜냐면 emptyString.characters.count < otherString.characters.count라는 표현식을 사용할 수 있기 때문이다. 진짜 값이 없다면 이러한 비교 자체를 할 수 없어야 정상이다. 비슷한 예로 let a = 0를 보자. 0은 ‘없다’는 뜻의 값이기도 하지만, -1, 0, 1에서 볼 수 있듯이 정수 내 포함되는 분명히 존재하는 (정의된) 값이다. nil은 그와는 달리 어떤 정의된 값도 가지고 있지 않다는 뜻이다.

흥미로운 것은 옵셔널에 의해 감싸져 있는 어떤 값은 실제 값이며, 이 값을 조작하는 함수는 얼마든지 존재가능하다는 점이다. 따라서 옵셔널에 대해서도 map 동작은 수행할 수 있다. 개념상으로도 충분히 가능한 일이지만, 실제로도 가능하다.

func fmap<T, U>(_ v: T?, _ fn: (T) -> U) -> U? {
    guard let a = v else { return nil }
    return fn(a)
}

위와 같이 특정한 값가진 옵셔널내에 변환함수를 사상해주는 함수를 작성할 수 있으며, 실제로 옵셔널 타입은 다음과 같이 map 메소드를 가지고 있다.


let a: Int? = 5
let b = a.map{ $0 * 2 }
print(b)
/// Optional(10)

그렇다면 트리 구조는 어떨까? 트리 구조는 배열은 아니지만, 값이 일정한 규칙에 의해 연결되어 있으며 복수의 값이 감싸져 있는 컨테이너이다.

물론 트리구조는 Swift의 기본형은 아니기에 커스텀 타입으로 구현해야 하지만, 매우 쉽게 functor로 구현할 수 있다.

indirect enum Tree<T> {
    case leaf(T)
    case node(T, Tree<T>, Tree<T>)
}

extension Tree {
    func map<U> (_ transform: @noescape (T) throws -> U) rethrows -> Tree<U> {
        switch self {
        case let .leaf(v): return try .leaf(transform(v))
        case let .node(v, a, b): return try .node(tranform(v), a.map(transform), b.map(transform))
    }
}

let tree: Tree<Int> = .node(1, .node(2, .leaf(3), .leaf(4)), .node(5, .leaf(6), .leaf(7)))
let tree2 = tree.map{ $0 * 2 }

flatMap

그렇다면 그 외에 종종 볼 수 있는 flatMap은 무엇일까? 가장 쉽게 생각하면 flattenmap의 합성이다. Array 타입에는 실제로 flatten 메소드가 정의되어 있고, 이는 배열이나 Range를 원소로 갖는 배열을 (즉 2차 배열) 1차 배열로 풀어주는 역할을 한다.

let a = [[1,2,3],[4,5,6],[7,8,9]]
let b = a.flatten()
print(b)
/// "[1, 2, 3, 4, 5, 6, 7, 8, 9]

따라서 flatMap(T) -> [U] 타입의 transform 함수로 맵핑한 결과를 flatten하여 리턴하는 함수로 이해할 수 있다.

실제로 이러한 타입 시그니처를 가지고 있다.

func flatMap<SegmentOfResult: Sequence> 
    (_ transform: @noescape (Element) throws -> SegmentOfResult) 
    rethrows -> [SegmentOfResult.Iterator.Element]

다소 복잡해 보이기는 한데 풀어쓰면 다음과 같다.

  1. 이 메소드는 하나의 원소를 일련의 시퀀스로 변환하는 함수를 사상한다.
  2. 1차적인 결과로 원래의 배열로부터 배열의 배열, 즉 2차 배열이 만들어질 수 있다.
  3. 이 1차 결과물은 다시 하나의 배열로 연결되어 1차 배열로 변환된다.

이 과정을 단순하게만 바라보면 Array 혹은 Sequence 타입이 연결(concatenate)이 가능한 타입이기 때문에 이 동작이 가능하다는 것을 볼 수 있다. 즉 [1, 2, 3]과 [4, 5, 6]이 연결되어 [1, 2, 3, 4, 5, 6]이 될 수 있기 때문에 이러한 동작이 가능한 것 아니겠느냐는 것이다. 하지만 맵핑이 일어난 이후의 상황을 보면 정확하게는 [ [1, 2, 3], [4, 5, 6] ]이 [1, 2, 3, 4, 5, 6] 이 된 것이기 때문에 이는 단순히 연결가능한 타입에 대한 기능이 아니다.

엄밀하게 말하면 이는 배열 내 원소들이 어떤 컨테이너에 싸여 있을 때, 그 컨테이너를 벗겨낸 값들로 맵핑되는 동작이라고 봐야 한다. 이 관점에서 본다면 flatMap은 배열 뿐만 아니라 옵셔널타입에 대해서도 적용될 수 있다. 다음 코드는 셔널 값에 대해서 `(T) -> U?` 타입의 클로저를 맵핑했을 때, map, flatMap의 차이를 보여준다.

var a: Int? = 4 // Optional(4)
let q: (Int) -> Int? = { n in
    if n % 2 == 0 {
        return n + 3
    }
    return nil
}

let b = a.map(q)
print(b)
/// Optional(Optional(5))

let c = a.flatMap(q)
print(c)
/// Optional(5)

즉 옵셔널 내에 다시 옵셔널로 둘러싸진 결과를 내놓는 맵핑에 대해서 하위 옵셔널을 풀어낸 값이 나올 수 있게 하는 것이다. 결국 배열과 옵셔널은 모두 수학적으로 유사한 성질을 가지고 있으며 map, flatMap을 (약간 다른 관점으로 바라보게는 되지만) 똑같은 방식으로 적용할 수 있는 셈이다.

물론 컨테이너 속의 컨테이너라는 개념이 같은 컨테이너에 대해서만 적용되는 것은 아니다, 옵셔널의 배열에 대해서도 얼마든지 적용이 가능하다.

let a = [1,2,3,4,5]
let c : (Int) -> Int? = { n in 
    if n % 2 == 0 {
        return n * 2 
    }
    return nil
}

let b = a.flatMap(c)
print(b)
/// [4, 8]

결국 flatMap은 컨테이너의 깊이를 증가시키지 않고 map한 결과를 얻는 함수라고 이해하면 된다. 특히 위에서 살펴본바대로 T -> [U] 혹은 T -> U? 타입의 함수를 맵핑할 때 매우 유용하다.

예를 들어 일련의 숫자의 집합을 숫자값의 집합으로 바꾸는 과정을 생각해보자. 문자열을 정수값으로 변환하는 경우 Int.init?이 적용되기 때문에 변환한 값은 정수가 아닌 Int? 타입이 된다. 따라서 보통은 다음과 같은 코드를 작성하게 된다.

let words = ["123", "456.7", "eighty nine", "10", "100"]
let numbers = words.map{ Int($0) }.filter{ $0 != nil }.map{ $0! }

하지만 flatMap을 적용하면 보다 간단하게 코드를 작성할 수 있다.

let numbers = words.flatMap{ Int($0) }

정리

  • map 은 배열외에도 어떤 계산적 맥락에 대해 연산(함수)을 사상할 수 있는 기능으로 이해한다.
  • flatten 역시 배열 내의 계산적 맥락에 대해 각 원소를 벗겨내고(unwrap) 하나의 배열로 정리하는 기능이다. 이 때 배열로 표현된 상위 맥락 역시 다른 계산 맥락일 수 있다.
  • 따라서 T->[U] 외에 T->U? 같은 함수를 집합에 대해 사상해야 한다면, map 대신 flatMap을 사용하면 추가적인 필터, 맵 처리를 해주지 않아도 깔끔하게 정돈된 결과를 얻을 수 있다.