Swift 공식문서: Optional Chaining에 대해 알아보자

옵셔널 체이닝

이상하게 옵셔널 체이닝이 너무 너무 헷갈려서 공식문서를 보고 정리해보았다.

옵셔널 체이닝이란, 옵셔널의 프로퍼티, 메서드, 서브스크립트를 불러올 때 사용되는 방법이다. 옵셔널이 값을 가지면 옵셔널의 프로퍼티, 메서드, 서브스크립트 불러오기 성공, 옵셔널이 nil이라면, nil을 리턴한다. 복수의 queries 가 체인처럼 이어져서 그 중 하나라도 nil이라면 nil을 리턴한다.

강제 언래핑과의 비교

프로퍼티, 메서드, 서브스크립트를 가져오고 싶은 옵셔널값 뒤에 ? 를 붙여서 사용한다. 이것은 강제 언래핑 ! 이랑 비슷하다. (! 자리에 ? 쓴다)

하지만 중요한 차이점은 옵셔널이 nil일 때 강제 언래핑의 경우 강제로 벗기면 런타임 에러가 나지만,

옵셔널 체이닝을 사용하여 벗기면 우아하게 nil을 리턴하며 실패를 한다는 것이다.

옵셔널 체이닝은 항상 옵셔널값 리턴

옵셔널 체이닝 실패시 nil을 리턴한다는 점에서, 옵셔널 체이닝은 항상 옵셔널값을 리턴한다는 것을 알 수 있다.

→ 심지어 옵셔널값의 프로퍼티,메서드 등이 옵셔널값이 아니라도 옵셔널값으로 리턴한다

예) Int를 리턴하는 프로퍼티에 옵셔널 체이닝으로 접근시 Int? 리턴


간단한 첫번째 예시

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

let john = Person()

현재 옵셔널 타입으로 선언된 residence에 값이 없는 상태

//방법1) 강제 언래핑 !
let roomCount = john.residence!.numberOfRooms 
print(roomCount) //residence가 nil이라면 런타임 에러

//방법2) 옵셔널 체이닝 ?
if let roomCount = john.residence?.numberOfRooms {
    print(roomCount)
} else {
    print("값 가져오기 실패")
}

강제 언래핑

: residence 값을 강제로 벗겨서 프로퍼티인 roomCount 값 가져와라

→ 강제로 벗겨서 만약 값이 없다면 런타임 에러가 일어나는 대참사

옵셔널 체이닝

: 만약 residence 값이 있다면 프로퍼티인 roomCount 값 가져오고, 없다면 nil을 리턴해라 → 값이 없어도 우아하게 실패해서 괜찮음

: nil리턴 가능성이 있으니 항상 옵셔널 값을 리턴함

리턴한 옵셔널값을 옵셔널 바인딩으로 상수에 할당

그런데 왜 print(roomCount)는 옵셔널 값이 아닌걸까?

옵셔널 체이닝이 성공하여 값이 리턴되면, let roomCount에 할당되어 옵셔널 바인딩 형태로 옵셔널을 까준다.

따라서 옵셔널 체이닝 성공시 → 바인딩도 성공해서 최종적으로는 옵셔널값이 아닌 값이 나오는 것이다.

john.residence = Residence()

residence 프로퍼티에 값을 할당하고 다시 옵셔널 체이닝 시도시 정상적으로 값을 리턴한다.


조금 복잡한 두번째 예시

옵셔널 체이닝으로 값 가져오기, 값 할당이 가능하다.

class Person {
    var residence: Residence?
}

class Residence {
    var rooms: [Room] = []
    var numberOfRooms: Int {
        return rooms.count
    }
    subscript(i: Int) -> Room {
        get {
            return rooms[i]
        }
        set {
            rooms[i] = newValue
        }
    }
    func printNumberOfRooms() {
        print("방개수: \(numberOfRooms)")
    }
    var address: Address?
}

class Room {
    let name: String
    init(name: String) {
        self.name = name
    }
}

class Address {
    var buildingName: String?
    var buildingNumber: String?
    var street: String?
    func buildingIdentifier() -> String? {
        if let buildingNumber = buildingNumber, let street = street {
            return "\(buildingNumber) \(street)"
        } else if buildingName != nil {
            return buildingName
        } else {
            return nil
        }
    }
}

헷갈려서 그림을 그려보았다

  1. Person 클래스의 인스턴스 john이 있고, Address 클래스의 인스턴스 someAdress가 있을 때

john의 address는 1) 옵셔널 체이닝으로 할당해줄 수도 없고, 2) 옵셔널 체이닝으로 가져올 수 없다.

그 이유는 현재 john.residence 가 nil이기 때문이다.

john의 address를 꺼내오려면 꺼내오는 길목에 residence라는 단계를 거쳐야하는데 nil이라서 꺼내기를 실패하게 된다.

let john = Person()
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "사과로"
john.residence?.address = someAddress //john의 주소를 할당해주기 실패
if let johnsAddress = john.residence?.address {
    print("주소: \(johnsAddress)")
} else {
    print("주소 가져오기 실패")
}
//주소 가져오기 실패
  1. Person 클래스의 인스턴스 john이 있고 Residence 클래스의 인스턴스 johnsHouse가 있을 때

john의 residence에 johnsHouse 할당해준 후, room 프로퍼티도 추가해주고 나서

옵셔널 체이닝으로 numberOfRooms를 꺼내올 수 있다.

let john = Person()
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "거실"))
johnsHouse.rooms.append(Room(name: "거실"))
john.residence = johnsHouse
if let roomCount = john.residence?.numberOfRooms {
    print("방 개수: \(roomCount)")
} else {
    print("방 개수 가져오기 실패")
}
  1. Person 클래스의 인스턴스 john이 있고 Residence 클래스의 인스턴스 johnsHouse가 있고

Address 클래스의 인스턴스 johnsAddress가 있을 때 비로소 john의 street를 꺼내올 수 있다.

let john = Person()

let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "거실"))
johnsHouse.rooms.append(Room(name: "거실"))
john.residence = johnsHouse

let johnsAddress = Address()
johnsAddress.buildingName = "사과빌딩"
johnsAddress.street = "사과로"
john.residence?.address = johnsAddress
if let johnsStreetAddress = john.residence?.address?.street {
    print("도로명주소: \(johnsStreetAddress)")
} else {
    print("도로명주소 가져오기 실패")
}

위 코드는 사실 아래의 코드처럼 옵셔널 바인딩을 중첩으로 사용해야하는데 너무 번거로워서 옵셔널 체이닝이 만들어진 것이다.

if let johnsHouse = john.residence {
    if let johnsAddress = johnsHouse.address {
        if let johnsStreet = johnsAddress.street {
            print("도로명주소: \(johnsStreet)")
        } else {
            print("도로명주소 가져오기 실패")
        }
    }
}

좋은 웹페이지 즐겨찾기