[최종 수정일 : 2018.08.31]
원문 : 애플 개발 문서 Swift 4.2 Language Guide - Optional Chaining
옵셔널 체이닝(Optional Chaining)
옵셔널 체이닝(Optional chaining)은 현재 옵셔널이 nil
이 될수도 있는, 프로퍼티, 메소드, 서브스크립트를 조회하고 호출하는 과정입니다. 옵셔널이 값을 포함한 경우, 프로퍼티, 메소드, 서브스크립트 호출은 성공합니다; 옵셔널이 nil
인 경우, 프로퍼티 메소드, 서브스크립트 호출은 nil
을 반환합니다. 여러개를 함께 체이닝(chained) 할수 있고, 체인에서 어떤 링크가 nil
인 경우에, 전체 체인은 우아하게 실패합니다.
주의
Swift에서 옵셔널 체이닝은 Objective-C에서의nil
에 메시지 보내는 것과 비슷하지만, 모든 타입에 대해서 동작하고, 성공 또는 실패를 확인 할 수 있습니다.
강제 언래핑 대신 옵셔널 체이닝하기(Optional Chaining as an Alternative to Forced Unwrapping)
옵셔널이 non-nil
인 경우에, 호출하고자 하는 프로퍼티, 메소드, 서브스크립트의 옵셔널 값 뒤에 물음표(?
)를 붙여서 옵셔널 체이닝을 지정합니다. 이것은강제 언래핑하기 위해 옵셔널 값 뒤에 느낌표(!
)를 붙이는 것과 매우 비슷합니다. 주요 차이점은 옵셔널이 nil
일때 강제 언래핑은 런타임 오류가 발생하는 반면에(whereas) 옵셔널 체이닝은 옵셔널이 nil
일때, 우아하게 실패합니다.
이러한 사실에 비추어 옵셔널 체이닝은 nil
값에서 호출될수 있으며, 프로퍼티, 메소드, 서브스크립트가 옵셔널이 아닌 값을 반환 할지라도 옵셔널 체이닝 호출의 결과는 항상 옵셔널 값입니다. 이러한 옵셔널 반환 값을 옵셔널 체이닝 호출이 성공(옵셔널에 값이 포함)했거나, 체인(chain)에서 nil
값에 의해 성공하지 못했는지(옵셔널 값이 nil
을 반환) 검사하는데 사용할 수 있습니다.
특히(specificaily), 옵셔널 체이닝 호출의 결과는 예상된 반환 값과 같은 타입이지만, 옵셔널로 래핑(wrapped)됩니다. 옵셔널 체이닝을 통해서 접근할때 일반적으로 Int
를 반환하는 프로퍼티는 Int?
을 반환할 것입니다.
다음 몇개의 코드 부분은 옵셔널 체이닝이 강제 언래핑과 어떻게 다른지와 성공 여부를 검사할수 있는지 보여줍니다.
먼저, 2개의 클래스 Person
과 Residence
를 정의하였습니다.
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
Residence
인스턴스는 하나의 Int
프로퍼티 numberOfRooms
를 가지고 있으며, 기본 값은1
입니다. Person
인스턴스는 Residence?
타입의 옵셔널 프로퍼티residence
를 가지고 있습니다.
새로운 Person
인스턴스를 생성하는 경우, residence
프로퍼티는 옵셔널 이므로, 기본적으로 nil
로 초기화 됩니다. 아래 코드에서, john
은 nil
값인residence
프로퍼티를 가지고 있습니다.
let john = Person()
사람(person)의 residence
의 numberOfRooms
프로퍼티 값을 강제 언래핑하기 위해, residence
뒤에 느낌표(!
)를 붙여서 사용하는 경우, 언래핑한 residence
의 값이 없기 때문에, 런타임 오류가 발생합니다.
let roomCount = john.residence!.numberOfRooms
// this triggers a runtime error
john.residence
가. non-nil
값을 가질때 위의 코드는 성공하고 적절한 방의 개수를 포함하는 roomCount
는 Int
값으로 설정할 것입니다. 하지만, 위에서 설명한것처럼, 이 코드는 residence
가 nil
일때, 항상 런타임 오류가 발생합니다.
옵셔널 체이닝은 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
프로퍼티에서 체인(chain)
하고 residence
가 존재하는 경우에, numberOfRooms
의 값을 가져오는 것을 말합니다.
numberOfRooms
에 접근을 시도하는 것이 실패할 수 있기 때문에, 옵셔널 체이닝은 Int?
또는 옵셔널
타입의 값을 반환하려고 합니다. Int
residence
가 nil
일때, 위의 예제처럼, 옵셔널 Int
는 nil
이 될것이며, numberOfRooms
에 접근할 수 없다는 사실을 알려줘야 합니다. 옵셔널 Int
는 정수형으로 언래핑하고 옵셔널이 아닌 값을 roomCount
변수에 할당하기 위해, 옵셔널 바인딩으로 사용됩니다.
심지어 numberOfRooms
가 옵셔널이 아닌 int
일지라도 그렇게 된다는 사실을 주의합니다. 사실상 옵셔널 체인으로 조회하는 것은 numberOfRooms
호출이 항상 Int
대신에. Int?
를 반환하는 것을 의미합니다.
Residence
인스턴스를 john.residence
에 할당할 수 있으며, 더 이상 nil
값이 아닙니다.
john.residence = Residence()
john.residence
는 nil
보다는 실제 Residence
인스턴스를 포함합니다. 이전과 같은 옵셔널 체이닝으로 numberOfRooms
에 접근하려고 하면, 기본값 1
을 포함하는 numberOfRooms
의 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)."
옵셔널 체이닝에 대한 모델 클래스 정의하기(Defining Model Classes for Optional Chaining)
프로퍼티, 메소드 서브스크립트 호출에 한 단계 이상의 옵셔널 체이닝을 사용할 수 있습니다. 이것은 연관된 타입의 복잡한 모델에서 하위프로퍼티로 내려가는 것이 가능하고, 이러한 하위프로퍼티에서 프로퍼티, 메소드, 서브스크립트를 접근할수 있는지 검사하는게 가능합니다.
아래 코드 부분은 다음 예제에서 사용할 4개의 모델 클래스를 정의 하며, 여러 단계(multilevel)의 옵셔널 체인의 예제를 포함합니다. 이 클래스는 Person
과 위에서 Room
과 Address
클래스를 추가하고 관련된 프로퍼티, 메소드, 서브스크립트로 Residence
모델을 확장하였습니다.
Person
클래스는 이전과 같은 방식으로 정의되어 있습니다.
class Person {
var residence: 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?
}
Residence
의 이번 버젼은 Room
인스턴스의 배열을 저장하기 때문에, numberOfRooms
프로퍼티는 저장 프로퍼티가 아닌 게산(computed) 프로퍼티로 구현됩니다. 계산된 numberOfRooms
프로퍼티는 단순히 rooms
배열에서 count
프로퍼티의 값을 반환 합니다.
rooms
배열을 쉽게 접근하도록, Residence
의 이번 버젼은 요청된 인덱스에 있는 rooms
배열의 room
을 사용하는 읽기-쓰기(read-write) 서브스크립트를 제공합니다.
또한, 이번 버젼의 Residence
는 단순히 숙소(residence)에 있는 방의 갯수를 출력하는 메소드 printNumberOfRooms
를 제공합니다.
마지막으로, Residence
는 Address?
의 타입인 옵셔널 프로퍼티 address
를 정의합니다. 이 프로퍼티에 대한 Address
클래스 타입은 아래에 정의되어 있습니다.
rooms
배열에 대해 사용된 Room
클래스는 한개의 프로피티 name
가 있는 간단한 클래스 이고, 초기화는 방의 이름에 맞게 프로퍼티를 설정합니다.
class Room {
let name: String
init(name: String) { self.name = name }
}
이 모델에서 마지막 클래스는 Address
입니다. 이 클래스는 String?
타입인 3개의 옵셔널 프로퍼티를 가지고 있습니다. 처음 프로퍼티 2개 buildingName
과 buildingNumber
는, 특정 건물을 주소처럼 식별하는 방법입니다. 세번째 프로퍼티 street
는 주소에 대한 거리 이름으로 사용됩니다.
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
둘다 값을 가지고 있는 경우에, buildingNumber
과 street
를 연결시키거나, 그렇지 않으면 nil
을 반환합니다..
옵셔널 체이닝으로 프로퍼티 접근하기(Accessing Properties Through Optional Chaining)
강제 언래핑 대신 옵셔널 체이닝(Optional Chaining as an Alternative to Froced Unwrapping) 에서 설명했던 것처럼, 옵셔널 체이닝을 옵셔널 값으로 된 프로퍼티에 접근하는데 사용할 수 있고, 프로퍼티 접근이 성공하는지 검사할 수 있습니다.
새로운 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.residence
가 nil
이기 때문에, 이전처럼, 옵셔널 체이닝 호출은 실패합니다.
옵셔널 체이닝으로 프로퍼티의 값을 설정할 수 있습니다.
let someAddress = Address()
someAddress.buildingNumber = "29"
someAddress.street = "Acacia Road"
john.residence?.address = someAddress
예제에서, john.residence
가 현재 nil
이기 때문에, john.residence
의 address
프로퍼티를 설정하는 것은 실패할 것입니다.
할당하는 것은 옵셔널 체이닝의 일부이며, =
연산자의 오른쪽에 있는 코드는 처리되지 않는 것을 의미합니다. 이전 예제에서, 상수에 접근하는 것은 부작용이 없기 때문에, someAddress
가 처리되지 않는 것을 보는것은 쉽지 않습니다. 아래 목록은 동일한 할당을 하지만, 주소를 생성하는 함수를 사용합니다. =
연산자의 오른쪽이 처리가 되었는지 확인할 수 있는, 그 함수는 값을 반환하기 전에 함수가 호출됨(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()
함수가 호출되지 않음을 알 수 있습니다.
옵셔널 체이닝으로 메소드 호출하기(Calling Methods Through Optional Chaining)
옵셔널 값에서 메소드를 호출하기 위해 옵셔널 체이닝을 사용할 수 있고, 메소드 호출이 성공했는지 확인할 수 있습니다. 반환 값을 정의하지 않은 메소드도 가능합니다.
Residence
클래스에서 printNumberOfRooms()
메소드는 numberOfRooms
의 현재 값을 출력합니다. 메소드는 다음과 같습니다.
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
이 메소드는 반환 값을 지정하지 않았습니다. 하지만, 반환 타입이 없는 함수와 메소드는 암시적으로 Void
반환타입을 가지며, 반환 값 없는 함수(Functions Without Return Values)에 설명되어 있습니다. 이것은 ()
의 값 또는 빈 튜플을 반환하는 것을 의미합니다.
옵셔널 값에서 옵셔널 체이닝으로 메소드를 호출하는 경우, 옵셔널 체이닝을 이용해서 호출할때의 반환 값은 항상 옵셔널 타입이기 때문에, 메소드의 반환 타입은 Vod
가 아니라 Void?
가 될것입니다. 이것은 if
문으로 printNumberOfRooms()
메소드 호출이 가능한지 검사 하는것이 가능하며 심지어는 반환 값이 정의되지 않은 메소드에서 가능합니다. 메소드 호출이 성공했는지 보기 위해, printNumberOfRooms
호출의 반환 값이 nil
인지 비교합니다.
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."
옵셔널 체이닝을 통해서 프로퍼티를 설정하는 경우에도 마찬가지입니다. 위 예제에서 옵셔널 체이닝으로 프로퍼티 접근하기(Accessing Properties Through Optional Chaining)는 residence
프로퍼티가 nil
일지라도, john.residence
에 대한 address
값을 설정하려고 합니다. 옵셔널 체이닝으로 Void?
타입의 반환된 값을 프로퍼티에 설정하려고 하며, 프로퍼티가 정상적으로 설정되었는지 보기 위해 nil
인지 비교하는 것이 가능합니다.
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."
옵셔널 체이닝으로 서브스크립트 접근하기(Accessing Subscripts Through Optional Chaining)
옵셔널 값으로 된 서브스크립트의 값을 가져오고 설정하고 서브스크립트 호출이 성공인지 확인하기 위해, 옵셔널 체이닝을 사용할 수 있습니다.
주의
옵셔널 값에서 옵셔널 체이닝으로 서브스크립트에 접근할때, 서브스크립트 대괄호 앞에(before) 물음표(?
)를 붙입니다. 옵셔널 체이닝의 물음표는 항상 옵셔널 표현식 바로 뒤에 표시해야 합니다.
아래 예제는 Residence
클래스에서 정의된 서브스크립트 john.residence
프로퍼티의 rooms
배열에서 첫번째 방의 이름을 가져오려고 합니다. 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")
서브스크립트 설정이 실패하기 때문에, residence
는 현재 nil
입니다.
john.residence
에 Residence
인스턴스를 생성하고 할당하는 경우, rooms
배열에서 하나이상의 Room
인스턴스에서, 옵셔널 체이닝으로 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."
옵셔널 타입의 서브스크립트 접근하기(Accessing Subscripts of Optional Type)
서브스크립트가 옵셔널 타입(Swift의 Dictionary
타입의 서브스크립트 키)의 값을 반환하는 경우 물음표(?
)는 옵셔널 반환 값에 연결하기 위해(chain) 서브스크립트의 닫힌 대괄호 뒤에(after) 위치시킵니다.
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]
위 예제에서 String
키(key)와 Int
값(values)의 배열을 맵핑하는 두개의 key-value 쌍을 포함하는 딕셔너리 testScores
가 정의되었습니다. 예제에서 "Dave"
배열의 첫번째 항목에 91
을 설정하기 위해; "Bev"
배열의 첫번째 항목에 1
증기시키기 위해; "Brian"
의 키에 대한 배열의 첫번째 항목을 설정하기 위해 옵셔널 체이닝을 사용합니다. testScores
딕셔너리는 "Dave"
와 "Dev"
키를 포함하고 있기 때문에, 처음 두 호출이 성공합니다. testScores
딕셔너리는 "Brian"
에 대한 키를 포함하지 않기 때문에, 세번째 호출은 실패합니다.
여러 단계의 체이닝을 연결하기(Linking Multiple Levels of Chaining)
모델에서 프로퍼티, 메소드, 서브스크립트를 찾아가기 위해(drill down) 옵셔널 체이닝의 여러 단계를 함께 연결할 수 있습니다. 하지만, 옵셔널 체이닝의 여러 단계는 반환된 값에 더 이상의 옵셔널 단계를 추가하지 않습니다.
달리 말하면:
- 갸져오려는 타입이 옵셔널이 아닌 경우에, 옵셔널 체이닝에 의해 옵셔널이 될것 입니다.
- 가져오려는 타입이 이미(already) 옵셔널인 경우에, 체이닝에 의해 더 이상(more) 옵셔널이 아니게 됩니다.
따라서:
- 옵셔널 체이닝에 의해
Int
값을 가져오려고 하는 경우, 얼마나 많은 체이닝이 사용되었는지와 상관없이,Int?
는 항상 반환됩니다. - 비슷하게, 옵셔널 체이닝으로
Int?
값을 가져오려고 하는 경우, 얼마나 많은 체이닝이 사용되었는지와 상관없이,Int?
는 항상 반환됩니다.
아래 예제는 john
의 residence
프로퍼티에 address
프로퍼티에 street
프로퍼티에 접근하기 위해 시도합니다. 여기에서 둘다 옵셔널 타입인 residence
와 address
프로퍼티 체인으로, 옵셔널 체이닝의 두 단계가 사용됩니다.
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
의 값은 현재 유효한 Residence
인스턴스가 포함되어 있습니다. 하지만, john.residence.address
는 현재 nil
입니다. 이 때문에, john.residence?.address?.street
호출이 실패합니다.
위 예제에서, street
프로퍼티의 값을 가져오기 위해 시도하는 것을 주의합니다. 프로퍼티의 타입은 String?
입니다. 옵셔널 체이닝의 2단계는 프로퍼티의 옵셔널 타입을 기반으로 추가적으로 적용됨에도 불구하고, 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."
이 예제에서, john.residence
의 값은 현재 유효한 Address
인스턴스를 포함하고 있기 때문에, john.residence
의 address
프로퍼티에 접근하는것은 성공할 것입니다.
옵셔널 값을 반환하는 메소드에서의 체이닝(Chaining on Methods with Optional Return Values)
이전 예제는 옵셔널 체이닝을 사용하여 옵셔널 타입의 프로퍼티의 값을 어떻게 가져오는지를 보았습니다. 옵셔널 체이닝을 옵셔널 타입의 값을 반환하는 메소드를 호출하고, 필요에 따라, 메소드의 반환값을 연결(chain)하기 위해 사용할 수 있습니다.
아래 예제는 옵셔널 체이닝을 통해서 Address
클래스의 buildingIdentifier()
메소드를 호출합니다. 이 메소드는 String?
타입의 값을 반환합니다. 위에서 설명했던 것처럼, 옵셔널 체이닝 뒤에 메소드의 궁극적인(ultimate) 반환 타입은 String?
입니다.
if let buildingIdentifier = john.residence?.address?.buildingIdentifier() {
print("John's building identifier is \(buildingIdentifier).")
}
// Prints "John's building identifier is The Larches."
이 메소드의 반환 값에 추가적으로 옵셔널 체이닝을 수행하길 원하는 경우, 옵셔널 체이닝 물음표(?
)는 메소드의 괄호 뒤(after)에 위치시킵니다.
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()
메소드의 반환 값으로 체이닝 되고,buildingIdentifier()
메소드 자체가 아니기 때문에, 옵셔널 체이닝 물음표(?
)를 괄호 뒤에 위치시킵니다.
'Swift > Language Guide' 카테고리의 다른 글
Extensions (0) | 2018.09.18 |
---|---|
Nested Types (0) | 2018.09.18 |
Type Casting (0) | 2018.09.18 |
Error Handling (0) | 2018.09.18 |
Deinitialization (0) | 2018.09.18 |
Initialization (0) | 2018.09.17 |
Inheritance (0) | 2018.08.30 |
Subscripts (0) | 2018.08.30 |