이 포스팅은 Swift 시리즈 20 편 중 6 번째 글 입니다.

  • Part 1 - 01: Optional, Any, AnyObject, nil
  • Part 2 - 02: struct, class, enum
  • Part 3 - 03: Closure
  • Part 4 - 04: Property
  • Part 5 - 05: 상속, 생성자, 소멸자
  • Part 6 - This Post
  • Part 7 - 07: 타입 캐스팅
  • Part 8 - 08: assert, guard
  • Part 9 - 09: Protocol
  • Part 10 - 10: Extention
  • Part 11 - 11: 오류처리
  • Part 12 - 12: 고차함수
  • Part 13 - 13: ARC(Automatic Reference Counting)
  • Part 14 - 14: Access control, Access modifier
  • Part 15 - 15: Generics
  • Part 16 - 16: Optional에 대한 깊은 이해
  • Part 17 - 17: Lazy Variables
  • Part 18 - 18: Enumeration
  • Part 19 - 19: Initialization
  • Part 20 - 20: Concurrency
▼ 목록 보기

옵셔널 체이닝

  • 옵셔널 내부의 내부의 내부로 옵셔널이 연결되어 있을 때 유용
  • 매널 nil을 확인하고 옵셔널을 원래 하나씩 풀어줘야 함.. 아래를 보자.
// 사람 클래스
class Person {
    var name: String
    var job: String?
    var home: Apartment?
    
    init?(name: String) { // 구현을 했는데 nil이 나올수도 있었다고 하자.
        self.name = name
    }
}
// 집 클래스
class Apartment {
    var buildingNumber: String
    var roomNumber: String
    var `guard`: Person?
    var owner: Person?
    
    init?(dong: String, ho: String) {
        buildingNumber = dong
        roomNumber = ho
    }
}

두 개의 클래스가 있다. 각각의 클래스는 Person의 경우 Apartment를 옵셔널로 갖기 때문에 생성자에서는 갖지 않는다.

// 각각의 Person, Apartment 인스턴스 생성
// 사용하는 입장에서는 init?으로 되어있으므로 옵셔널로 담는다.
let wansik: Person? = Person(name: "wansik")
let apart: Apartment? = Apartment(dong: "101", ho: "202")
let superman: Person? = Person(name: "superman")

위에 init?으로 되어 있으므로 사용하는 사람 입장에서는 일단 혹시 모르니 옵셔널로 담는 것이 옳다. 그리고 안전한 코딩을 위해서라면 init으로 되어 있더라도 옵셔널로 선언하는 것이 좋다.

이 상황에서 사람 객체를 넣었을 때, 해당 사람이 소유한 집의 경비원의 직업이 무엇인지 출력하는 함수를 짜보자고 하자.

func getJobOfGuard(owner: Person?) {
    if let owner = owner {
        if let home = owner.home {
            if let `guard` = home.guard {
                print("\(owner.name)의 집 경비원의 직업은 \(`guard`)입니다.")
            } else {
                print("\(owner.name)는 집은 있으나 경비원은 없습니다.")
            }
        } else {
            print("\(owner.name)은 집이 없어요.")
        }
    } else {
        print("owner가 없습니다.")
    }
}

getJobOfGuard(owner: wansik) // wansik은 집이 없어요.

이렇게 각각에 대해서 모든 처리를 해주어야 한다. 중간 단계가 귀찮아서 뒤의 else를 지우면 아예 출력이 없다.

func guardJob(owner: Person?) {
    if let owner = owner {
        if let home = owner.home {
            if let `guard` = home.guard {
                if let guardJob = `guard`.job {
                    print("우리집 경비원의 직업은 \(guardJob)입니다")
                } else {
                    print("우리집 경비원은 직업이 없어요")
                }
            }
        }
    }
}

guardJob(owner: wansik) // no output

그런데 내가 하고 싶은 것은, 윗단계에서 nil이 하나라도 있으면 경비원이 직업이 없다는 결과를 얻고 싶은 것이다. 주인이 없으면 경비원이 없는 것이고, 집이 없으면 경비원이 없는 것이기 때문이다. chain으로 결과값이 연결이 되는 형태인 것. 이런 상황에서 옵셔널이 연결되어 있는 상태를 풀 수 있다. 그것이 옵셔널 체이닝.

func getJobOfGuardWithOptionalChaining(owner: Person?){
    if let guardJob = owner?.home?.guard?.job {
        print("우리집 경비원의 직업은 \(guardJob)입니다")
    } else {
        print("우리집 경비원은 직업이 없어요")
    }
}

getJobOfGuardWithOptionalChaining(owner: wansik) // 우리집 경비원은 직업이 없어요

이렇게 처리가 가능하다! 자 여기서 이제 조심해야 하는 것은, 타입을 설정할 때 사용하는 !, ? 와는 개념이 다르다는 것이다. 타입을 설정할 때는 해당 타입을 옵셔널로 선언할 것인지, 아닌지에 대한 이야기를 하는 것이다.

반대로 인스턴스에 대해 !, ?를 사용하는 것은 옵셔널로 선언된 변수에 대해 이를 언래핑하는 방법에 관한 것이다. 원래 옵셔널로 선언된 변수에 대해서 언래핑하는 방법에는 강제 언래핑과 옵셔널 바인딩이 있었다.

강제 언래핑의 경우 강제로 옵셔널을 풀수는 있지만 (망치로 부숴) nil이 들어있는 경우 처리가 불가하여 안전성에 문제가 있다.

옵셔널 바인딩의 경우 애플에서 제안하는 방법으로, if let value = value1 { content }와 같은 방식으로 nil인 경우를 비껴나가면서 로직을 구성할 수 있었다. 하지만 위의 예시에서 보듯 연속되어서 옵셔널 값이 있는 경우 이를 벗기는데 있어서 굉장히 코드가 길어진다.

이러한 부분에 있어 옵셔널 체이닝을 사용해서 쉽게 값이 있는지 없는지를 확인할 수 있다. 다만 옵셔널 체이닝을 하더라도 여전히 nil은 나올 수 있기 때문에 바인딩은 여전히 수행해서 로직을 처리하는 것이 옳다.

이제 나중에 나오겠지만 이렇게 옵셔널 바인딩을 하더라도 스코프 안에서만 그 값을 사용할 수 있다. 그래서 guard를 사용한다. 일단 예시를 보면서 이해하자.

// 집이 없는 상황
wansik?.name // wansik
wansik?.home // nil : 아직 집이 없음
wansik?.job // nil

// 집이 없으니 home의 정보도 nil이다.
wansik?.home?.buildingNumber // nil
wansik?.home?.roomNumber // nil
wansik?.home?.owner // nil
wansik?.home?.guard // nil

// 집이 있는 상황
wansik?.home = apart // 집 샀다
wansik?.home?.buildingNumber // 101
wansik?.home?.roomNumber // 202
wansik?.home?.owner // nil
wansik?.home?.guard // nil

// 집 안에 경비원도 배치하자
wansik?.home?.guard = superman
wansik?.home?.guard?.name // superman
wansik?.home?.guard?.job // nil : 직업은 없어서 nil

결국 이해가 쉬우려면 이렇게 옵셔널 체이닝을 할 때 ?는 값이 있으면~ 으로 이해하면 된다.

nil 병합 연산자

  • 중위 연산자 ??
  • Optional ?? Value : 옵셔널 일 경우 Value를 넣어줘
  • 띄어쓰기에 주의해야 한다.
var guardJob: String
// 의미론적으로 보면 일단 집도 있고 경비원도 있는데 직업이 입력이 안되어있다면?
guardJob = wansik?.home?.guard?.job ?? "슈퍼맨"
print(guardJob) // 슈퍼맨

wansik?.home?.guard?.job = "경비원"
guardJob = wansik?.home?.guard?.job ?? "슈퍼맨"
print(guardJob) // 경비원

하다보니 문제가 있는데, 각각이 nil인지를 체크하는 것이 아니기 때문에 결국 개발자가 이 상황에 대해서 제대로 판단하고 사용해야 할 듯하다. 무조건적으로 이런 걸 사용하게 되면 결국 오류를 잡기가 어려워지기 때문에, 명시적으로 이해를 했을 때 사용하도록 하자.

Reference