본문 바로가기
iOS/SWIFT

[Swift] Chapter 14. 옵셔널 체이닝과 빠른 종료

by 원만사 2021. 10. 9.
반응형

옵셔널 체이닝은 여러 값이 중첩된 형태를 띄어야 제 몫을 발휘한다. 이번 장에서 옵셔널 체이닝과 빠른 종료(Early Exit) 문법에 대해 알아보자.

 

14.1 옵셔널 체이닝


옵셔널 체이닝은 옵셔널에 속해 있는 nil일지도 모르는 프로퍼티, 메서드, 서브스크립션 등을 가져오거나 호출할 때 사용할 수 있는 일련의 과정이다. 옵셔널에 값이 있다면 프로퍼티, 메서드, 서브스크립트 등을 호출할 수 있고, 옵셔널이 nil이라면 프로퍼티, 메서드, 서브스크립트 등은 nil을 반환한다. 중첩된 옵셔널 중 하나라도 값이 존재하지 않는다면 결과적으로 nil을 반환한다.

옵셔널 체이닝은 프로퍼티나 메서드 또는 서브스크립트를 호출하고 싶은 옵셔널 변수나 상수 뒤에 물음표(?)를 붙여 표한한다. 옵셔널 체이닝의 결과 nil이 반환될 가능성이 있으므로 반환되는 값은 항상 옵셔널이다.

 

/*
	코드 14-1. 사람의 주소 정보 표현 설계
*/

class Room { // 호실
    var number: Int // 호실 번호
    
    init(number: Int) {
        self.number = 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
    }
}

옵셔널 체이닝에 대해 알아보기 위해 [코드 14-1]에서 기본 클래스를 설계하였다.

 

먼저, [코드 14-2]에서 yagom 이라는 사람의 인스턴스를 생성한다.

/*
	코드 14-2. yagom 인스턴스 생성
*/

let yagom: Person = Person(name: "yagom")

 

yagom이 사는 호실 번호를 알고 싶다. 옵셔널 체이닝과 강제 추출을 사용하여 프로퍼티에 접근해보면 [코드 14-3]과 같은 결과를 볼 수 있다.

 

/*
	코드 14-3. 옵셔널 체이닝 문법
*/

let yagomRoomViaOptionalChaining: Int? = yagom.address?.building?.room?.number
// nil
let yagomRoomViaOptionalUnwrapping: Int = yagom.address!.building!.room!.number
// 오류 발생!

 

yagom에는 아직 주소, 건물, 호실 정보가 없다. 옵셔널 체이닝을 사용하면 yagom의 address 프로퍼티가 nil이므로 옵셔널 체이닝 도중 nil이 반환된다. 그러가 강제 추출을 시도하면 nil인 address 프로퍼티에 접근하려 할 때 런타임 오류가 발생한다.

 

[코드 14-4]는 옵셔널 바인딩을 사용하여 yagom이 사는 호실 정보를 가져오는 코드를 표현한 것이다.

/*
	코드 14-4. 옵셔널 바인딩의 사용
*/

let yagom: Person = Person(name: "yagom")

var roomNumber: Int? = nil

if let yagomAddress: Address = yagom.address {
    if let yagomBuilding: Building = yagomAddress.building {
        if let yagomRoom: Room = yagomBuilding.room {
            roomNumber = yagomRoom.number
        }
    }
}

if let number: Int = roomNumber {
    print(number)
} else {
    print("Can not find room number")
}

 

[코드 14-4]를 [코드 14-5]처럼 옵셔널 체이닝으로 표현하면 훨씬 간단해진다.

/*
	코드 14-5. 옵셔널 체이닝의 사용
*/

let yagom: Person = Person(name: "yagom")

if let roomNumber: Int = yagom.address?.building?.room?.number {
    print(roomNumber)
} else {
    print("Can not find room number")
}

 

[코드 14-5]를 보면 옵셔널 체이닝 코드가 옵셔널 바인딩 기능과 결합할 수 있음을 알 수 있다. 옵셔널 체이닝의 결괏값은 옵셔널 값이기 때문에 옵셔널 바인딩과 결합할 수 있는 것이다.

이처럼 옵셔널 체이닝을 통해 한 단계뿐만 아니라 여러 단계로 복잡하게 중첩된 옵셔널 프로퍼티나 메서드 등에 매번 nil 체크를 하지 않아도 손쉽게 접근할 수 있다. 또한 옵셔널 체이닝을 통해 값을 받아오기만 하는 것이 아니라 반대로 값을 할당해줄 수도 있다.

 

/*
	코드 14-6. 옵셔널 체이닝을 통한 값 할당 시도
*/

yagom.address?.building?.room?.number = 505
print(yagom.address?.building?.room?.number) // nil

현재 yagom의 address 프로퍼티가 없으며 그 하위의 building 프로퍼티도 room 프로퍼티도 없다. 그렇기 때문에 [코드 14-6]의 옵셔널 체이닝은 동작 도중에 중지될 것이다. number 프로퍼티는 존재조차 하지 않으므로 505가 할당되지 않는 것은 물론이다.

 

/*
	코드 14-7. 옵셔널 체이닝을 통한 값 할당
*/

yagom.address = Address(province: "충청북도", city: "청주시 청원구", street: "충청대로", building: nil, detailAddress: nil)
yagom.address?.building = Building(name: "곰굴")
yagom.address?.building?.room = Room(number: 0)
yagom.address?.building?.room?.number = 505

print(yagom.address?.building?.room?.number) // 505

위와 같이 옵셔널 체인에 존재하는 프로퍼티를 실제로 할당해준 후 옵셔널 체이닝을 통해 값이 정상적으로 반환되는 것을 확인할 수 있다.

옵셔널 체이닝을 통해 메서드와 서브스크립트(17.1절) 호출도 가능하다.

먼저, 옵셔널 체이닝을 통한 메서드 호출이다. 호출 방법은 프로퍼티 호출과 동일하다. 만약 메서드의 반환 타입이 옵셔널이라면 이 또한 옵셔널 체인에서 사용 가능하다. [코드 14-8]에서 Address 구조체에 메서드 코드를 추가하고 옵셔널 체인을 통해 호출해보자.

 

/*
	코드 14-8. 옵셔널 체이닝을 통한 메서드 호출
*/

struct Address { // 주소
    var province: String // 광역시/도
    var city: String // 시/군/구
    var street: String // 도로명
    var building: Building? // 건물
    var detailAddress: String? // 건물 외 상세 주소
    
    init(province: String, city: String, street: String) {
        self.province = province
        self.city = city
        self.street = street
    }
    
    func fullAddress() -> String? {
        var restAddress: String? = nil
        
        if let buildingInfo: Building = self.building {
            restAddress = buildingInfo.name
        } else if let detail = self.detailAddress {
            restAddress = detail
        }
        
        if let rest: String = restAddress {
            var fullAddress: String = self.province
            
            fullAddress += " " + self.city
            fullAddress += " " + self.street
            fullAddress += " " + rest
            
            return fullAddress
        } else {
            return nil
        }
    }
    
    func printAddress() {
        if let address: String = self.fullAddress() {
            print(address)
        }
    }
}

yagom.address?.fullAddress()?.isEmpty // false
yagom.address?.printAddress() // 충청북도 청주시 청원구 충청대로 곰굴

 

우리가 서브스크립트를 가장 많이 사용하는 곳은 Array와 Dictionary이다. 옵셔널의 서브스크립트를 사용하고자 할 때는 대괄호([])보다 앞에 물음표(?)를 표기해주어야 한다. 이는 서브스크립트 외에도 언제나 옵셔널 체이닝을 사용할 때의 규칙이다.

 

/*
	코드 14-9. 옵셔널 체이닝을 통한 서브스크립트 호출
*/

let optionalArray: [Int]? = [1, 2, 3]
optionalArray?[1] // 2

var optionalDictionary: [String: [Int]]? = [String: [Int]]()
optionalDictionary?["numberArray"] = optionalArray
optionalDictionary?["numberArray"]?[2] // 3

 

14.2 빠른 종료


빠른 종료의 핵심 키워드는 guard이다. guard 구문은 if 구문과 유사하게 Bool 타입의 값으로 동작하는 기능이다. guard 뒤에 따라붙는 코드의 실행 결과가 true일 때 코드가 계속 실행된다. if 구문과는 다르게 guard 구문은 항상 else 구문이 뒤에 따라와야 한다. 만약 guard 뒤에 따라오는 Bool 값이 false라면 else의 블록 내부 코드를 실행하게 되는데, 이때 else 구문의 블록 내부에는 꼭 자신보다 상위의 코드 블록을 종료하는 코드가 들어가게 된다.

 

guard Bool 타입 값 else {
	예외사항 실행문
	제어문 전환 명령어
}

 

Bool 타입의 값으로 guard 구문을 동작시킬 수 있지만 옵셔널 바인딩의 역할도 할 수 있다. guard 뒤에 따라오는 옵셔널 바인딩 표현에서 옵셔널의 값이 있는 상태라면 guard 구문에서 옵셔널 바인딩된 상수를 guard 구문이 실행된 아래 코드부터 함수 내부의 지역상수처럼 사용할 수 있다.

 

/*
	코드 14-11. guard 구문의 옵셔널 바인딩 활용
*/

func greet(_ person: [String: String]) {
    guard let name: String = person["name"] else {
        return
    }
    
    print("Hello \(name)!")
    
    guard let location: String = person["location"] else {
        print("I hope the weather is nice near you")
        return
    }
    
    print("I hope the weather is nice in \(location)")
}

var personInfo: [String: String] = [String: String]()
personInfo["name"] = "Jenny"

greet(personInfo)
// Hello Jenny!
// I hope the weather is nice near you

personInfo["location"] = "Korea"

greet(personInfo)
// Hello Jenny!
// I hope the weather is nice in Korea

[코드 14-11]에서 guard를 통해 옵셔널 바인딩 된 상수는 greet(_:) 함수 내에더 지역상수처럼 사용된 것을 볼 수 있다.

 

조금 더 구체적인 조건을 추가하고 싶다면 쉼표(,)로 추가조건을 나열해주면 된다. 추가된 조건은 Bool 타입 값이어야 한다. 또, 쉼표로 추가된 조건은 AND 논리연산과 같은 결과를 준다. 즉, 쉼표를 &&로 치환해도 같은 결과를 얻을 수 있다는 뜻이다.

 

/*
	코드 14-13. guard 구문에 구체적인 조건을 추가
*/

func enterClub(name: String?, age: Int?) {
    guard let name: String = name, let age: Int = age, age > 19,
          name.isEmpty == false else {
        print("You are too young to enter the club")
        return
    }
    
    print("Welcome \(name)!")
}

 

guard 구문의 한계는 자신을 감싸는 코드 블록, 즉 return, break, continue, throw 등의 제어문 전환 명령어를 쓸 수 없는 상황이라면 사용이 불가능하다는 점이다. 함수나 메서드, 반복문 등 특정 블록 내부에 위치하지 않는다면 사용이 제한된다.

/*
	코드 14-14. guard 구문이 사용될 수 없는 경우
*/

let first: Int = 3
let second: Int = 5

guard first > second else {
    // 여기에 들어올 제어문 전환 명령이 딱히 없다. 오류!
}

 

 

 

스위프트 프로그래밍: Swift 5 - 교보문고

객체지향, 함수형, 프로토콜 지향 패러다임까지 한 번에! | 스위프트를 제대로 이해하고 싶은 개발자를 위한 책스위프트는 iOS와 macOS용 애플리케이션 개발에 주로 사용하는 프로그래밍 언어입니

www.kyobobook.co.kr

 

반응형

'iOS > SWIFT' 카테고리의 다른 글

[Swift] Chapter 16. 모나드  (0) 2021.10.10
[Swift] Chapter 15. 맵, 필터, 리듀스  (0) 2021.10.10
[Swift] Chapter 13. 클로저  (0) 2021.10.09
[Swift] Chapter 12. 접근제어  (0) 2021.10.08
[Swift] Chapter 11. 인스턴스 생성 및 소멸  (0) 2021.10.08

댓글