Swift - 옵셔널 체이닝, guard
Swift 에서 옵셔널은 nil 값에 대해 안전한 처리를 가능하게 한다. 옵셔널로 선언된 변수나 상수는 항상 옵셔널 바인딩이나 nil 체크를 통해 값을 꺼내야 하며, 컴파일러가 항상 이것을 체크한다.
Swift 옵셔널
옵셔널 (Optional) 이란 말 그대로 선택적인, 즉 값이 있을 수도 없을 수도 있다는 것을 나타낸다. 즉 프로그래밍 언어에서는 상수나 변수가 값을 가질 수도, 없을 수도(null) 있다는 것을 의미하고, N
forstudy.tistory.com
옵셔널을 사용하면 오류 방지엔 뛰어나겠지만, 만약 옵셔널 안의 옵셔널 안의 옵셔널 안의 옵셔널 안의 상수를 꺼낸다고 할 때 무슨 마트료시카 인형을 여는 것 마냥 매우 많은 코드를 생성해야 하고 이는 개발자 입장에서 매우 귀찮은 일이다.
if let first = 옵셔널.first {
if let second = first.second {
if let third = second.third {
if let fourth = third.fourth {
print(fourth)
}
}
}
}
Swift 에선 역시 이런 귀찮음을 없애기 위해 옵셔널 체이닝 이란 기법을 넣었다. 옵셔널 체이닝은 매우 중요한 개념이기 때문에, 제대로 배워보도록 하자.
옵셔널 체이닝이란?
옵셔널 체이닝이란 옵셔널에 속해 nil 일지도 모르는 프로퍼티나, 메서드, 서브스크립션등을 가져오거나 호출할 때 사용할 수 있는 일련의 과정이다. 옵셔널에 값이 있다면 프로퍼티를 가져오거나 메소드를 호출할 수 있고, 옵셔널이 nil 이라면 프로퍼티나 메소드, 서브스크립트는 nil을 반환한다.
옵셔널?.옵셔널?.옵셔널?.메소드()
옵셔널?.옵셔널?.프로퍼티
즉 옵셔널을 반복 사용하여 꼬리를 물고 있는 모양이기 때문에 옵셔널 체이닝이라고 부르며, 모든 옵셔널 체인이 하나라도 nil 일 경우 결과적으로 nil 을 반환한다. 옵셔널 체이닝은 프로퍼티나 메소드 또는 서브스크립트를 호출하고 싶은 옵셔널 변수나 상수 뒤에 물음표를 붙여 표현한다.
물음표 대신 느낌표(!) 를 사용하여 옵셔널에서 값을 강제 추출 할 수도 있다.
옵셔널!.옵셔널!.프로퍼티
그러나 만약 옵셔널이 nil 인데 값을 강제로 추출하려고 시도하면 런타임 오류가 발생하므로 정말 조건 100% nil 이 아니라고 확신할 때만 쓰도록 하고 되도록 사용을 지양하자
옵셔널 체이닝에 대해 알아보기 위해 다음과 같이 클래스를 설계해보자.
class Room {
var number : Int
init(number:Int){
self.number = number
}
public func printRoom(){
print(number)
}
}
class Building {
var name: String
var room: Room?
init(name:String){
self.name = name
}
}
struct Address {
var province:String
var city:String
var street : String
var building : Building?
var detailAddress: String?
}
class Person {
var name: String
var address:Address?
init(name:String){
self.name = name
}
}
사람의 정보를 표현하기 위해 Person 클래스를 설계했다. Person 클래스는 이름이 있으며 주소를 옵셔널로 갖는다. 이제 Person 인스턴스를 생성하고 살고 있는 빌딩의 방 번호를 새로운 변수에 할당해보자.
let jongHo = Person(name: "Jong Ho")
let jongHoRoomNumber = jongHo.address?.building?.room?.number // nil
let jongHoRoomNumberForceExtract = jongHo.address!.building!.room!.number //런타임 에러 발생!
Person 인스턴스의 Address 는 nil 이므로, jongHoRoomNumber 이라는 상수에 nil 이 반환된 것을 볼 수 있다. 만약 옵셔널을 강제로 추출하려고 한다면 (!) 런타임 에러가 발생하고 프로그램이 종료된다.
위 옵셔널 체이닝의 추출 흐름은 아래와 같다.
왼쪽은 옵셔널 체이닝을 활용한 추출 흐름이며, 오른쪽은 강제로 추출한 흐름이다.
옵셔널 바인딩과 결합하기
그렇다면 옵셔널 체이닝 대신 옵셔널 바인딩을 사용해서 room Number를 구해보자
var jongHoRoomNumber : Int?
if let jongHoAddress = jongHo.address,let jongHoBulding = jongHoAddress.building, let jongHoRoom = jongHoBulding.room{
jongHoRoomNumber = jongHoRoom.number
}
print(jongHoRoomNumber)
이것을 옵셔널 체이닝으로 표현하면 아래와 같이 간단해진다.
if let roomNumber = jongHo.address?.building?.room?.number {
print(roomNumber)
} else {
print("방 번호를 찾을 수 없습니다")
}
위와 같이 옵셔널 체이닝은 값 또는 nil 을 반환할 수 있기 때문에, 옵셔널 바인딩과도 결합하여 사용할 수 있다.
옵셔널 체이닝으로 값을 할당하기
이전 예제에선 옵셔널 체이닝을 통해 새로운 상수나 변수에 값을 할당하는 것만 보았다. 하지만 옵셔널 체이닝을 사용하면 값을 할당할 수도 있다.
jongHo.address?.building?.room?.number = 1302
print(jongHo.address?.building?.room?.number)
물론 address 가 nil 이기 때문에 옵셔널 체이닝이 도중에 중단되기 때문에 실제로 number 프로퍼티에 값이 할당되지 않는다.
옵셔널 체이닝으로 메소드 호출하기
옵셔널 체이닝을 통한 메소드 호출 방법은 프로퍼티 호출과 동일하다. 만약 메소드의 반환값이 옵셔널이라면 이 또한 옵셔널 체인에서 사용 가능하다.
struct Profile {
var nickName : String?
var description : String?
func profileDescription() -> String? {
if let nickName = nickName,let description = description {
return "유저 닉네임: \(nickName) 자기소개: \(description)"
} else {
return nil
}
}
}
class User {
var name : String
var age : Int
var profile : Profile?
init(name : String, age : Int){
self.name = name
self.age = age
}
}
위와 같은 클래스가 있다고 할 때, 유저를 하나 만들어서 프로필 설명을 상수에 저장해보자.
let user = User(name: "종호", age: 27)
let userDescription = user.profile?.profileDescription()
print(userDescription) //nil
user.profile = Profile(nickName: "호종", description: "하하")
let userDescription2 = user.profile?.profileDescription()
print(userDescription2) //Optional("유저 닉네임: 호종 자기소개: 하하")
profileDescription() 함수는 옵셔널 문자열을 반환하기 때문에, 함수의 결과 또한 옵셔널 체이닝으로 사용할 수 있다.
let isDescriptionEmpty : Bool? = user.profile?.profileDescription()?.isEmpty //nil
guard 구문
guard 구문은 if문과 유사하게 Bool 타입의 값으로 동작하는 기능이지만, if 문과는 살짝 다르다. guard 문은 항상 else 구문이 뒤에 따라와야 하며, 조건문이 true 일 때 다음 코드를 실행하고 만약 false 면 else 문 안에 있는 코드를 실행한다. 이 else 구문엔 항상 자신보다 상위의 코드 블록을 종료하는 코드가 들어가게 된다. 따라서 특정 조건에 부합하지 않으면 항상 else 블록에서 return,break, continue, throw 등의 제어 전환 명령을 사용한다.
guard Bool 타입 값 else {
예외사항 실행문
제어문 전환 명령어
}
guard 문은 if 문과는 다르게 예외 사항에 대한 처리만 하면 된다. 똑같은 동작을 하는 if 구문과 guard 구문을 비교해보자. 아래는 짝수만 출력하는 반복문이다.
for i in 1...6 {
if(i & 1 != 1){
print(i)
} else {
continue
}
}
for i in 1...6 {
guard i & 1 != 1 else {
continue
}
print(i)
}
Bool 타입의 값으로 guard 문을 사용할 수 있지만, 옵셔널 바인딩도 사용이 가능하다.
func isNil(value : Int?) {
guard let extractValue = value else {
print("is nil")
return
}
print(extractValue) //<--extractValue 는 Int 타입으로 지역변수처럼 활용이 가능!
}
isNil(value: nil)
isNil(value: 5)
guard 문과 옵셔널 바인딩을 결합해서 사용하면, guard 문에서 선언한 변수나 상수를 그 다음 코드에서부터 지역변수처럼 활용이 가능하다! 위에서 extractValue 는 옵셔널 바인딩을 위해 선언한 상수지만, guard 문을 통과하면 지역변수처럼 활용이 가능하다.