티스토리 뷰

반응형

이번 글에서는 Swift 공식 문서의 16번째 단원인 Optional Chaining을 읽고 정리한 글을 쓰려고 한다.


Apple Swift 공식 문서 16단원 - Optional Chaining

Optional Chaining

Optional chaining(옵셔널 체이닝)은 현재 nil일 가능성이 있는 옵셔널 타입의 프로퍼티, 메서드, 서브 스크립트를 쿼리하고 호출하는 프로세스이다. 만약 옵셔널 타입에 값이 할당되어 있으면 프로퍼티, 메서드, 서브 스크립트의 호출이 성공하는 것이고 nil이라면 nil을 반환하게 된다. 여러 개의 쿼리를 함께 연결할 수 있으며 연결된 체인 중 하나라도 nil이라면 전체 체인이 정상적으로 실패하게 된다.


Optional Chaining as an Alternative to Forced Unwrapping

옵셔널 타입의 프로퍼티, 메서드, 서브 스크립트의 값이 nil이 아니라면 이름 뒤에 ?를 배치하여 옵셔널 체이닝을 지정할 수 있다. 이는 값을 강제로 언래핑하기 위해 옵셔널 타입에 !를 붙여주는 것과 비슷하다. 차이점은 값이 nil일 때 옵셔널 체이닝은 정상적으로 실패하지만 강제 언래핑은 런타임 오류를 발생시킨다는 것에 있다.

 

옵셔널 체이닝이 nil 값을 호출할 수 있다는 것을 반영하기 위해 만약 프로퍼티, 메서드, 서브 스크립트가 옵셔널 값이 아닌 값을 반환하더라도 옵셔널 체이닝의 결과는 항상 옵셔널 값이다. 반환된 옵셔널 값으로 옵셔널 체이닝의 호출이 성공했는지 실패했는지를 확인할 수 있다.

따라서 옵셔널 체이닝 호출의 결과는 예상되는 반환 값과 동일한 타입이지만 옵셔널로 래핑 된 상태로 나타난다. 예를 들어 결과로 Int형이 예상되지만 옵셔널 체이닝을 사용하게 되면 Int?형이 반환되는 것이다.

 

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

위의 코드로 정의된 두 클래스로 강제 언래핑한 경우와 옵셔널 체이닝을 사용한 경우의 차이를 살펴보자. 우선 Person 클래스에는 residence라는 Residence 클래스의 인스턴스를 갖는 프로퍼티가 정의되어있다.

 

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

만약 위와 같이 코드를 작성한다면 Person 클래스의 프로퍼티에는 default값이 정의되어 있지 않지만 옵셔널 타입이기 때문에 nil로 초기화된다. 이 상태에서 위와 같이 강제 언래핑을 하게 되면 런타임 에러를 발생시킨다. 물론 위의 코드에서 Person의 인스턴스인 john의 residence 프로퍼티에 적절한 값을 할당시킨 뒤 똑같이 코드를 실행하면 에러를 발생시키진 않을 것이다.

 

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."

이젠 옵셔널 체이닝을 사용해보자. 옵셔널 체이닝을 사용하려면 아까와는 다르게 ?를 사용해주면 된다. 위의 코드에서는 residence의 값이 존재하는지 확인하고 있다면 numberOfRooms의 값을 반환하고 그렇지 않다면 실패 메시지를 출력하게 된다. 하지만 여기서 성공하더라고 Int형이 반환되는 것이 아닌 Int?형이 반환된다는 점에 유의하자.


Defining Model Classes for Optional Chaining

한 레벨 이상의 깊이에 존재하는 메서드, 프로퍼티, 서브 스크립트에도 옵셔널 체이닝을 사용할 수 있다. 복잡한 구조를 가진 모델에서도 하위 프로퍼티들에 접근해서 값이 있는지를 확인할 수 있다. 이때 체인 중 하나라도 nil을 반환하면 실패하게 된다.

 

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("The number of rooms is \(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
        }
    }
}

위의 코드는 좀 더 복잡한 관계를 가진 클래스들을 정의한 것이다. Residence의 프로퍼티는 Room 클래스, Address 클래스의 인스턴스를 사용하는 것들이 정의되어 있다. Room 클래스는 간단한 구조라 크게 볼 건 없고 Address 클래스는 조금 복잡하다. Address 클래스는 String? 타입의 세 가지 옵셔널 프로퍼티가 있다. buildingIdentifier()라는 함수도 정의되어있는데 buildingName, buildingNumber에 값이 있으면 둘 다 반환하고 buildingName만 값이 있으면 그것만 반환하고 둘 다 값이 없다면 nil을 반환하는 함수이다.

 


Accessing Properties Through Optional Chaining

그럼 이제 아까 정의한 네 개의 클래스들을 사용해 옵셔널 체이닝으로 프로퍼티에 접근하는 방법을 알아보도록 하자.

 

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."

위의 코드처럼 Person 클래스의 인스턴스를 만들고 numberOfRooms 프로퍼티에 접근하려고 하면 아직은 아무것도 할당되어 있지 않기 때문에 옵셔널 체이닝이 실패하게 된다.

 

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

그럼 이제 위와 같이 john의 address 프로퍼티에 someAddress를 할당해보자. 물론 아직도 john.redience는 nil이다. 할당은 옵셔널 체이닝의 일부이며 = 연산자의 오른쪽에 있는 코드는 평가되지 않음을 의미한다. 이 말은 위와 같이 할당을 하면 옵셔널 체이닝처럼 처리되기 때문에 성공했는지 실패했는지 알 수가 없다는 말이다.

 

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

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

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

위와 같이 할당을 하게 되면 할당하는 것은 동일하지만 옵셔널 체이닝이 성공했는지 실패했는지에 대한 여부를 출력문을 보고 판단할 수 있다.


Calling Methods Through Optional Chaining

옵셔널 체이닝을 사용하여 옵셔널 값에 대한 메서드를 호출하고 해당 메서드 호출이 성공했는지 확인할 수 있다. 해당 메서드가 반환 값을 정의하지 않는 경우에도 이를 수행할 수 있다.

 

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

위의 메서드는 numberOfRooms의 현재 값을 출력하는 메서드이다. 이 메서드는 반환 타입이 없지만 예전에 Function단원에서 본 것처럼 반환 값이 없는 함수는 암시적 반환 값이 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."

위의 코드와 같이 반환값이 nil인지 비교하여 메서드를 호출할 수 있는지에 확인할 수 있다.

 

let john = Person()

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

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."

옵셔널 체이닝을 통해 프로퍼티를 설정하려는 경우에도 마찬가지로 사용할 수 있다. 위의 코드에서 residence 프로퍼티가 nil이라도 john.residence에 값을 설정하려고 한다. 옵셔널 체이닝을 사용하여 프로퍼티를 설정하려고 하면 Void? 타입의 값이 반환되므로 nil과 비교하여 성공 여부를 확인할 수 있다.

 


Accessing Subscripts Through Optional Chaining

옵셔널 체이닝을 사용하여 옵셔널 값의 서브 스크립트를 검색 및 할당하고 해당 서브스크립트 호출이 성공했는지 확인할 수 있다. 옵셔널 체이닝을 통해 옵셔널 값의 서브스크립트에 액세스 할 때 ?는 뒤가 아니라 서브스크립트의 대괄호 앞에 붙는다. 옵셔널 체이닝에서 ?는 항상 옵셔널 표현식 바로 뒤에 붙는다.

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."

위의 코드는 Residence 클래스에 정의된 서브 스크립트를 사용해서 john의 residence 프로퍼티의 rooms Array에서 첫 번째 방의 이름을 검색하려고 한다. 하지만 현재 john의 residence 프로퍼티는 nil 이므로 서브스크립트 호출이 실패한다. 여기서 볼 수 있는 것은 ?의 위치이다. 서브스크립트 대괄호 앞에 ?가 위치한 것을 볼 수 있다.

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

위와 같이 옵셔널 체이닝과 서브 스크립트를 사용하여 새 값을 설정할 수 있다. 물론 위의 코드에서도 john의 residence는 nil이므로 실패한다.

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."

위의 코드처럼 rooms Array에 하나 이상의 Room 인스턴스가 존재하는 실제 Residence 클래스의 인스턴스를 생성하고 john.residence에 할당하는 경우 Residence의 서브 스크립트와 옵셔널 체이닝을 사용하여 rooms Array의 실제 항목에 접근할 수 있다.

 

Accessing Subscripts of Optional Type

서브 스크립트가 옵셔널 값을 반환하는 경우 (딕셔너리 타입) 서브 스크립트를 닫는 대괄호 뒤에 ?를 추가하여 옵셔널 반환 값을 연결한다.

 

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]

위의 코드는 testScores라는 2개의 key-value 쌍을 가진 딕셔너리를 정의한 것이다. 옵셔널 체이닝을 사용한 것을 보면 키 값을 나타내는 서브스크립트 뒤에 ?를 붙여준 것을 볼 수 있다. 이는 서브 스크립트가 옵셔널 값을 반환하기 때문인데 실제로 딕셔너리에 없는 키 값에 접근해도 오류가 나는 것이 아닌 그냥 실패만 한다.


Linking Multiple Levels of Chaining

여러 수준의 옵셔널 체이닝을 함께 연결하여 모델 내에서 프로퍼티, 메서드, 서브 스크립트로 접근할 수 있다. 하지만 여러 수준의 옵셔널 체이닝은 반환된 값에 더 만은 수준의 옵셔널을 추가하지 않는다.

 

다른 말로 하자면

  • 검색하려는 타입이 옵셔널이 아닌 경우 옵셔널 체이닝으로 인해 옵셔널 타입이 된다.
  • 검색하려는 타입이 이미 옵셔널인 경우 체이닝 때문에 더 많은 옵셔널이 되지는 않는다.

그러므로

  • 옵셔널 체이닝을 통해 Int값을 검색하려고 하면 체인 수준에 관계없이 항상 Int?가 반환된다.
  • 마찬가지로 Int?를 검색하려고 하면 체인 수준에 관계없이 항상 Int?가 반환된다.
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse

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 프로퍼티의 address 프로퍼티의 street 프로퍼티에 접근하려고 한다. 여기서 서로 다른 수준의 프로퍼티를 연결하게 되며 둘 다 옵셔널 타입이다. 여기서는 john.residence에는 유효한 값이 있지만 john.residence.address의 값은 nil이다. 이 때문에 옵셔널 체이닝은 실패하게 된다. 여기서 street 프로퍼티의 타입은 String?인데 만약 옵셔널 체이닝이 성공하더라고 String? 타입으로 반환된다.

 

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."

위의 코드처럼 실제 값을 할당하고 동일한 옵셔널 체이닝을 수행하면 잘 동작하는 것을 볼 수 있다.


Chaining on Methods with Optional Return Values

지금까지는 옵셔널 체이닝을 통해 옵셔널 타입의 프로퍼티를 검색하는 방법에 대해 알아봤다. 하지만 옵셔널 체이닝을 사용하여 옵셔널 타입의 값을 반환하는 메서드를 호출할 수도 있다.

 

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

 

위의 코드는 String? 타입의 값을 반환하는 buildingIdentifier() 메서드를 옵셔널 체이닝을 통해 호출한 것이다. 물론 옵셔널 체이닝을 사용하여 호출해도 반환 값의 타입은 String?이다.

 

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"."

위의 코드처럼 메서드의 반환 값에 대해 옵셔널 체이닝을 하려면 메서드의 괄호 뒤에 ?를 붙여주면 된다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함