공식 문서로 공부하는 Swift (15) - 옵셔널 체이닝

ci·2020년 5월 31일
1

Optional Chaining

옵셔널 체이닝(optional chaining)은 프로퍼티, 메소드, 서브스크립트를 nil이 될 수 있는 옵셔널로 호출하고 질의하는 것이다. 만약 옵셔널이 값을 포함하고 있다면 성공적으로 호출된다. 옵셔널이 nil이면 nil을 반환한다. 여러 질의를 함께 연결할 수 있고, 연결에서 어느 하나라도 nil이면 전체 연결은 실패한다.

Swift의 옵셔널 체이닝은 Objective-C의 nil 메시징과 유사하지만, Swift는 어떤 타입에서든 작동하고 성공/실패를 확인할 수 있다.



강제 언래핑의 대안으로써 사용되는 옵셔널 체이닝

옵셔널 값 뒤에 물음표(?)를 붙임으로써 옵셔널 체이닝을 지정한다. 옵셔널 값 뒤에 느낌표(!)를 붙여서 값을 강제 언래핑 하는 것과 유사하다. 옵셔널 체이닝은 옵셔널이 nil이면 실패하지만, 강제 언래핑은 런타임 에러를 발생시킨다는 게 주요한 차이점이다.

옵셔널 체이닝이 nil 값을 호출할 수 있기 때문에 옵셔널 체이닝 호출의 결과는 항상 옵셔널 값이 되어야 한다. 질의를 던진 프로퍼티, 메소드, 서브스크립트의 결과 값이 옵셔널이 아닌 값을 반환하더라도 그렇다. 옵셔널 체이팅 호출이 성공했는지 혹은 nil 때문에 실패했는지를 확인하기 위해 옵셔널 반환 값을 사용할 수 있다.

옵셔널 체이닝 호출은 예상되는 결과 값과 동일한 타입에 옵셔널이 붙여진 값을 반환한다. 옵셔널 체이닝을 통하면 보통 Int를 반환하는 프로퍼티는 Int?를 반환한다.

다음 몇 가지 코드는 옵셔널 체이닝이 강제 언래핑과 어떻게 다른지를 보여 준다.

첫 번째로, PersonResidence 두 클래스를 정의한다.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

Residence 인스턴스는 기본 값이 1인 하나의 정수 프로퍼티 numberOfRooms를 갖고 있다. Person 인스턴스는 Residence? 타입의 residence 옵셔널 프로퍼티를 가진다.


만약 새로운 Person 인스턴스를 만들면, residence 프로퍼티는 기본적으로 nil로 초기화된다.

let john = Person()

만약 johnresidence에 있는 numberOfRooms에 접근하려고 한다면, 값을 강제 언래핑 하기 위해 residence 뒤에 붙어있는 느낌표가 런타임 에러를 발생시킬 것이다. 언래핑 할 residence가 없기 때문이다.

let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error

위의 코드는 john.residencenil이 아니고 roomCount가 정수 값을 포함하고 있어야 성공한다. 하지만 residencenil이기 때문에 항상 런타임 에러가 발생한다.


옵셔널 체이닝은 numberOfRooms 값에 접근하는 대안을 제공한다. 옵셔널 체이닝을 사용하기 위해 느낌표 대신 물음표를 붙인다.

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."

이는 Swift에게 옵셔널 residence 프로퍼티의에 대해 "체인"을 하도록 하고, residence가 있는 경우 numberOfRooms의 값을 검색하도록 한다.

numberOfRooms에 접근하려는 시도가 잠재적으로 실패할 수 있기 때문에 옵셔널 체이닝은 Int? 타입의 값을 반환한다. residencenil이라면 Int?nil이 된다. 이 옵셔널 Int는 정수를 언랩하는 옵셔널 바인딩을 통해 접근되고, roomCount 변수에 옵셔널이 아닌 값을 할당한다.

numberOfRooms이 옵셔널이 아닌 Int인 경우에도 마찬가지다. 옵셔널 체인을 통해 질의된다는 사실은 numberOfRooms에 대한 호출이 항상 Int 대신 Int?를 반환한다는 것을 뜻한다.


john.residenceResidence 인스턴스를 할당할 수 있고, 더 이상 nil이 아니게 된다.

john.residence = Residence()

john.residence는 이제 Residence 인스턴스를 포함하고 있고, 만약 옵셔널 체이닝으로 numberOfRooms에 접근한다면 1인 Int? 값을 반환할 것이다.

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "John's residence has 1 room(s)."


옵셔널 체이닝을 위한 모델 클래스 정의

옵셔널 체이닝을 한 레벨보다 깊은 프로퍼티, 메소드, 서브스크립트를 호출하는 데 사용할 수 있다. 이것은 상호 연괸된 타입의 복잡한 모델 내 서브프로퍼티로 깊게 파고들 수 있도록 한다. 이러한 서브프로퍼티의 프로퍼티, 메소드, 서브스크립트에 접근할 수 있는지 확인할 수 있다.

아래의 코드는 네 가지 모델 클래스를 정의한다.

Person 클래스는 전과 같은 방식으로 정의한다.

class Person {
    var residence: Residence?
}

Residence 클래스는 전보다 복잡하다. [Room] 타입의 빈 배열로 초기화되는 rooms 프로퍼티를 클래스에서 정의한다.

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("The number of rooms is \(numberOfRooms)")
    }
    var address: Address?
}

이 버전의 ResidenceRoom 인스턴스의 배열을 저장하기 때문에 numberOfRooms 프로퍼티는 계산 프로퍼티로 구현된다.

rooms 배열에 축약하여 접근하기 위해 읽기 전용 서브스크립트를 제공한다. Address? 타입의 프로퍼티인 address는 옵셔널 프로퍼티로 선언한다.


rooms 배열에서 사용되는 Room 클래스는 name 프로퍼티 하나를 갖는 간단한 클래스다.

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

Address 클래스는 세 개의 String? 타입의 옵셔널 프로퍼티를 갖고 있다.

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
        }
    }
}

Address 클래스는 String? 타입을 반환하는 buildingIdentifier() 메소드를 갖고 있다. 이 메소드는 buildingName이 값을 갖고 있는지 확인하고 반환한다. street도 값을 갖고 있다면 두 프로퍼티를 연결하여 반환한다. 이외에는 nil을 돌려 준다.



옵셔널 체이닝을 통해 프로퍼티에 접근하기

옵셔널 체이닝을 사용해 옵셔널 값을 갖는 프로퍼티에 접근하고 프로퍼티에 대한 접근이 성공적인지 확인할 수 있다.

새로운 Person 인스턴스를 생성하고 numberOfRooms 프로퍼티에 접근을 시도한다.

let john = Person()
if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// Prints "Unable to retrieve the number of rooms."

john.residencenil이기 때문에 옵셔널 체이닝 호출은 이전처럼 실패했다.


옵셔널 체이닝을 통해 프로퍼티의 값을 설정할 수 있다.

let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress

john.residenceaddress 프로퍼티를 설정하는 것은 실패한다. john.residencenil이기 때문이다.


이 할당은 옵셔널 체이닝의 일부이다. 할당 연산자(=)의 오른쪽 항은 실행되지 않는다. 위 예씨에서 someAddress에 접근할 때 어떤 사이드 이펙트도 발생하지 않기 때문에 someAddress가 평가되지 않는다는 걸 알아채기가 쉽지 않다. 아래 코드는 동일한 할당이지만, address를 만들기 위해 함수를 사용한다. 함수는 할당연산자의 우항이 평가되는 경우 값을 반환하기 전에 “Function was called”를 출력한다.

func createAddress() -> Address {
    print("Function was called.")

    let someAddress = Address()
    someAddress.buildingNumber = "29"
    someAddress.street = "Acacia Road"

    return someAddress
}
john.residence?.address = createAddress()

아무것도 출력되지 않기 때문에 createAddress()가 호출되지 않았음을 알 수 있다.



옵셔널 체이닝을 통해 메소드 호출하기

옵셔널 체이닝을 사용해 옵셔널 값에 대한 메소드를 호출하고, 메소드가 성공했는지 여부를 판단할 수 있다. 메소드가 반환 값을 정의하지 않은 경우에도 가능하다.


Residence 클래스의 printNumberOfRooms() 메소드는 numberOfRooms의 현재 값을 출력한다.

func printNumberOfRooms() {
    print("The number of rooms is \(numberOfRooms)")
}

이 메소드는 반환 값을 지정하지 않았다. 하지만 반환 타입이 없는 함수와 메소드는 암시적으로 Void 타입을 반환한다. 빈 튜플인 ()을 반환한다는 뜻이다.


만약 옵셔널 체이닝으로 옵셔널 값에 이 메소드를 호출한다면, 메소드의 반환 값은 Void가 아니라 Void?가 된다. 옵셔널 체이닝을 통해 호출했을 때 반환 값은 항상 옵셔널 타입이기 때문이다. 이는 if문을 사용해 printNumberOfRooms()을 호출하는 게 가능한지 확인할 수 있도록 한다. 메소드가 반환 값을 정의하지 않았더라도 말이다.

if john.residence?.printNumberOfRooms() != nil {
    print("It was possible to print the number of rooms.")
} else {
    print("It was not possible to print the number of rooms.")
}
// Prints "It was not possible to print the number of rooms."

if (john.residence?.address = someAddress) != nil {
    print("It was possible to set the address.")
} else {
    print("It was not possible to set the address.")
}
// Prints "It was not possible to set the address."

옵셔널 체이닝을 통해 프로퍼티에 값을 설정하려고 시도한 경우와 동일하다.



옵셔널 체이닝을 통해 서브스크립트에 접근하기

옵셔널 체이닝을 사용해 옵셔널 값에 대한 서브스크립트에 값을 설정하고, 검색하는 시도를 할 수 있다. 서브스크립트 호출이 성공적이었는지 확인할 수도 있다.

만약 옵셔널 체이닝을 통해 옵셔널 값의 서브스크립트에 접근할 때는 서브스크립트의 대괄호 전에 물음표를 붙인다.


아래 예시는 john.residence 프로퍼티의 rooms 배열 중 첫 번째 방의 이름을 검색한다. Residence 클래스에 정의된 서브스크립트를 사용한다. john.residence가 현재 nil이기 때문에 서브스크립트는 실패한다.

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}
// Prints "Unable to retrieve the first room name."

서브스크립트에서 호출하는 옵셔널 체이닝 물음표는 john.residence 뒤, 대괄호 앞에 위치한다. john.residence가 옵셔널 값이기 때문이다.


비슷하게, 옵셔널 체이닝과 서브스크립트를 통해 새 값을 설정할 수 있다.

john.residence?[0] = Room(name: "Bathroom")

residencenil이기 때문에 이는 실패한다.


만약 실제 Residence 인스턴스에 rooms 배열에 하나 이상의 Room 인스턴스를 가진 john.residence를 만들고 할당한다면, 옵셔널 체이닝으로 rooms 배열에 있는 아이템에 접근하기 위해 Residence 서브스크립트를 사용할 수 있다.

let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse

if let firstRoomName = john.residence?[0].name {
    print("The first room name is \(firstRoomName).")
} else {
    print("Unable to retrieve the first room name.")
}
// Prints "The first room name is Living Room."

옵셔널 타입의 서브스크립트에 접근하기

만약 서브스크립트가 옵셔널 타입의 값을 반환한다면, 대괄호 뒤에 물음표를 붙인다.

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]


체이닝의 다중 레벨 연결

프로퍼티, 메소드, 서브스크립트에 더 깊게 들어가기 위해 여러 단계에 걸쳐 옵셔널 체이닝을 연결할 수 있다. 하지만 여러 단계의 옵셔널 체이닝은 반환 값에 더 많은 단계의 optionality를 추가하지는 않는다.

다시 말하자면,

  • 만약 검색하고자 하는 타입이 옵셔널이 아니라면, 옵셔널 체이닝으로 인해 옵셔널이 될 수 있다.
  • 검색하고자 하는 타입이 이미 옵셔널이라면, 옵셔널 체이닝으로 인해 더 옵셔널이 되지는 않는다.

그러므로,

  • 옵셔널 체이닝을 통해 Int를 검색할 경우 Int?가 반환된다. 몇 단계의 연쇄를 거치든 상관 없다.
  • 옵셔널 체이닝을 통해 Int?를 검색하면 Int?가 반환된다. 여러 단계를 거쳤다고 해서 Int?????와 같이 반환되지는 않는다.

아래 예시는 johnresidence 프로퍼티 안에 있는 address 프로퍼티의 street 프로퍼티에 접근하는 것을 보여 준다. 여기에는 두 단계의 옵셔널 체이닝이 사용됐다.

if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address.")
}
// Prints "Unable to retrieve the address."

john.residence은 현재 유효한 값을 포함하고 있다. 하지만 john.residence.address가 현재 nil이기 때문에 john.residence?.address?.street를 호출하는 것은 실패한다.


street 프로퍼티의 타입이 String?이기 때문에 john.residence?.address?.street의 반환 값은 항상 String?이다.

만약 실제 Address 인스턴스를 john.residence.address에 설정하고 street 프로퍼티에 값을 할당하면, 다중 레벨 옵셔널 체이닝을 통해 street 프로퍼티에 접근할 수 있다.

let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence?.address = johnsAddress

if let johnsStreet = john.residence?.address?.street {
    print("John's street name is \(johnsStreet).")
} else {
    print("Unable to retrieve the address.")
}
// Prints "John's street name is Laurel Street."


옵셔널 값을 반환하는 메소드에 체이닝하기

이전 예시는 옵셔널 체이닝을 통해 옵셔널 타입의 프로퍼티 값을 검색하는 방법을 보여 줬다. 옵셔널 타입 값을 반환하는 메소드를 호출하고 그 메소드의 반환 값에 연쇄하기 위해 옵셔널 체이닝을 사용할 수 있다.


아래 예시는 Address 클래스의 buildingIdentifier() 메소드를 옵셔널 체이닝을 통하여 호출한다. 이 메소드는 String? 타입의 값을 반환한다. 위에서 묘사한 것처럼, 옵셔널 체이닝 이후 이 메소드 호출의 결과 타입 역시 String?이다.

if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
    print("John's building identifier is \(buildingIdentifier).")
}
// Prints "John's building identifier is The Larches."

만약 메소드의 반환 값에서 추가적인 옵셔널 체이닝을 하고자 할 경우, 메소드의 소괄호 뒤에 물음표를 붙인다.

if let beginsWithThe =
    john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
    if beginsWithThe {
        print("John's building identifier begins with \"The\".")
    } else {
        print("John's building identifier does not begin with \"The\".")
    }
}
// Prints "John's building identifier begins with "The"."

위의 예시에서 옵셔널 체이닝 물음표를 소괄호 뒤에 표기했다. buildingIdentifier() 메소드 그 자체가 아니라 메소드가 반환하는 결과 값이 옵셔널 값이기 때문이다.

0개의 댓글