Swift

Swift) ch8.옵셔널

hoonsbrand 2022. 6. 29. 00:26

Swift는 안전성(safe)을 굉장히 강조하는 언어이다.

Swift 공식 문서 의 첫 파트인 소개 부분에서도 안전에 대한 이야기가 옵셔널이라는 것과 함께 나온다.

 

도대체 옵셔널이 뭘까? 그리고 왜 Swift는 옵셔널이라는 문법을 사용하는 것일까?

 

 

 

 

옵셔널(Optional)이란?


옵셔널은 단어 뜻 그대로 '선택적인', 즉 값이 '있을 수도, 없을 수도 있음'을 나타내는 표현이다.

A라는 자동차가 있다고 해보자. 자동차 A에 대해 선루프 옵션을 선택한 차는 선루프가 있고, 선택하지 않은 차는 선루프가 없다. 

 

도로에 다니는 자동차 A

  • 선루프가 있는 자동차 A
  • 선루프가 없는 자동차 A

어떤 자동차 모델이 선루프 옵션이 있다면 도로에 다니는 이 자동차들은 선루프가 '있을 수도, 없을 수도' 있다는 것이다.

 

다시 언어로 돌아와서, 이는 '변수나 상수 등에 꼭 값이 있다는 것을 보장할 수 없다. 즉, 변수 또는 상수의 값이 nil 일 수도 있다'는 것을 의미한다.

Swift로 개발을 해봤으면 ? or ! 기호를 본 적이 있었을 것인데 이 기호들을 이용해 옵셔널을 선언해 줄 수 있다.

 

 

 

 

 

 

 

옵셔널의 선언


위의 코드는 'nil은 Int 타입에 할당될 수 없음' 이라는 에러가 발생한다.

 

위에서 age라는 변수를 선언한 방식은 기본적인 선언 방식이다. 즉, 옵셔널 방식으로 선언한 것이 아니므로 non-optional인 값을 할당해야 한다. (non-optional은 어려운 단어가 아닌, 지금까지 우리가 기본적으로 사용한 값을 의미한다. 'optional이 아님'을 뜻하므로)

 

 

 

 

왜 오류가 발생했을까?

위 코드에서는 age 변수를 Int 타입으로 선언했으나 age라는 변수에 Int 타입이 아닌 nil을 할당해 주었다.

글의 맨 윗부분에서 옵셔널은 '변수 또는 상수의 값이 nil 일 수도 있다'는 것을 의미한다고 했는데, 그렇다면 우리는 변수를 선언할 때 일반적인 non-optional 타입이 아닌 optional 타입으로 생성을 해주었어야 한다는 것을 알 수 있다.

 

 

 

 

 

 

옵셔널로 선언

타입의 이름 뒤에 '?'를 붙임으로써 간단하게 옵셔널 Int타입의 변수를 선언할 수 있다.

추가로 필자의 경우에는 옵셔널을 뜻하는 물음표(?)를 보면 무언가 물어보는 것 같이 생겨서 "이거 Int 아닐 수도있다?" 처럼 보였다.

더 이해하기 쉬웠던 것이 age에 nil을 할당한 것처럼 이 변수 안에는 Int 값이 들어갈 수도 있지만 nil이 들어갈 수도 있는 말을 줄여서 말해주는 것 같았기 때문이다.

 

옵셔널을 처음 보는 사람은 "이게 뭔지는 대충 알 거 같아, 근데 왜 필요한 건데?"라는 의문증을 품을 수 있다. 물론 나도 그랬는데 여러 가지 실습을 하면서 정말 정말 중요한 기능이구나 생각했다.

 

이해를 돕기 위해 계산기 어플을 예시로 들어보자.

어떤 계산기가 사용자가 누른 숫자와 기호를 이용해 식을 작성하고 그에 대한 결과를 Double 타입으로 상단에 있는 화면에 띄어준다고 해보자.
근데 만약 사용자가 아무런 숫자도 누르지 않고 '='라는 결과를 도출하는 기호를 눌렀다고 해보자. 0이었던 화면에 나온 숫자는 그대로
0일 것이고 에러도 나지 않는다.

계산기 안에서는 어떤 일이 일어날까?
결과가 Double이 되어야 하는 상황에서 기호들과 연산을 할 숫자들이 들어오지 않았다면 각종 조건문 등에 의해 nil을 반환할 것이다.
nil을 반환함으로써 계산기 앱은 오류가 나지 않고 아무런 일도 일어나지 않은 것처럼 보이는 것이고, 이는 Double을 옵셔널 Double로 선언했기 때문에 nil이 들어와도 정상적으로 실행된 것이다.

 

다시 말해, 변수 안에 값이 있다는 것을 보장할 수 없다면 옵셔널을 사용한다. 

 

 

 

 

 

 

 

 

 

 

 

옵셔널 추출


 

옵셔널로 선언한 age변수에 3을 할당한 후 출력을 해 보았다.

출력의 결과는 Optional(3)이라는 값이 나온다. 아직 옵셔널 값이라는 뜻인데 이 옵셔널 값은 옵셔널이 아닌 값과 같이 사용할 수 없다.

 

 

 

옵셔널 값인 age에 1을 더해보려고 한다.

한 오류가 발생하는데 옵셔널 Int는 unwrapped(벗겨져야 한다)되어야 한다는 메시지가 나온다.

타입에 민감한 Swift는 Optional 값과 non-Optional 값을 철저하게 구분한다. 그래서 Optional 값인 age와 non-Optional 값인 1은 함께 사용할 수 없는 것이다.

 

 

 

 

 

 

이 말이 무슨 뜻일까?

 

wrapping

  • 옵셔널을 뜻한다.
  • age 변수를 옵셔널로 선언했는데, age라는 변수를 옵셔널이라는 빨간 박스 안에 넣어주는 작업이다.

 

unwrapping

  • 옵셔널 추출을 의미한다.
  • 빨간 박스 안에 넣어준 값을 실질적으로 사용하기 위해선 박스를 벗겨야 한다.

 

wrapping과 unwrapping을 중고거래를 예시로 들어 이해해보자!


판매자

애플 펜슬을 중고거래로 판매하기 위해 등록을 하고 구매자와 연락이 닿아 포장을 해야 한다.
애플 펜슬을 박스 안에 담는데, 이 과정을 wrapping 하는 걸로 생각을 하면 된다.
근데 만약 판매자가 나쁜 마음을 먹고 빈 박스를 보내려면?
-> 박스는 옵셔널이므로 애플 펜슬을 담을 수도 있지만 담지 않을 수도 있다(nil).


구매자

만약 아이패드와 함께 쓸 애플 펜슬이 필요해서 중고거래를 진행하려고 한다 해보자.
택배거래를 통해 박스가 집에 도착을 했다. 이 박스 안에 있는 애플 펜슬을 아이패드와 함께 사용하기 위해서는 박스를 unwrapping 해주어야 한다. 
위에서 값을 추출하지 않은 age 옵셔널을 non-Optional 인 1과 연산이 안되고 에러가 생긴 이유를 여기서 알 수 있다.
박스 안에 들은 애플 펜슬을 개봉하지 않고 어떻게 아이패드와 사용할 수 있겠는가? 그래서 unwrapping을 통한 값 추출이 필요한 것이다.

 

 

 

 

 

 


 

 

강제 추출 (Forced Unwrapping)


옵셔널 강제 추출(Forced Unwrapping)

  • 느낌표(!) 키워드를 이용한다.
  • 옵셔널 값을 추출하는 가장 간단하지만 가장 위험한 방법이다. -> 런타임 오류가 일어날 가능성이 가장 높기 때문.
  • 옵셔널을 만든 의미가 무색해지는 방법이기도 하다.
런타임 오류?
쉽고 짧게 설명하자면 컴파일 단계에서 생기는 것이 아니라 프로그램을 실행 시 발생하는 오류이다.
프로그래머 입장에서 컴파일 단계에서 오류가 생기지 않으면 알아차리기 힘들 수 있기 때문에 매우 피하고 싶은 오류일 것이다.
이 오류를 잡지 못한 채로 앱을 출시했다고 해보면, 사용자가 앱을 사용하는 중에 에러가 생길 가능성이 있기 때문이다.

 

 

var myName: String? = nil

// 옵셔널이 아닌 변수에는 옵셔널 값이 들어갈 수 없다. 추출해서 할당해주어야 한다!
var hoonsbrand: String = myName!

print(hoonsbrand)           // 런타임 오류!


// if 구문 등 조건문을 이용해서 조금 더 안전하게 처리해볼 수 있다.
if myName != nil {                      // myName이 nil이 아니면,
    print("My name is \(myName!)")      // myName을 강제 추출한다.
} else {
    print("myName == nil")              // myName이 nil일 경우
}

// myName == nil

런타임 오류의 가능성을 항상 내포하기 때문에 옵셔널 강제 추출 방식은 사용하는 것을 지양해야 한다.

 

 

 

 

 


 

 

옵셔널 바인딩 (Optional Binding)


위의 코드에서 사용한 if 구문을 통해 myNamenil인지 먼저 확인하고 옵셔널 값을 강제로 추출하는 방법은 다른 프로그래밍 언어에서 NULL 값을 체크하는 방식과 비슷하다. 앞서 설명한 것처럼 옵셔널을 사용하는 의미도 사라진다.

그래서 Swift는 조금 더 안전하고 세련된 방법으로 옵셔널 바인딩 (Optional Binding)을 제공한다.

 

옵셔널 바인딩 (Optional Binding)

  • 옵셔널에 값이 있는지 확인할 때 사용
  • 만약 옵셔널에 값이 있다면 옵셔널에서 추출한 값을 일정 블록 안에서 사용할 수 있는 상수나 변수로 할당하여 
    non-Optional인 형태로 사용할 수 있도록 해준다.
  • 옵셔널 바인딩은 if 또는 while 구문 등과 결합하여 사용한다.

 

var myName: String? = "Hoonsbrand"

// 옵셔널 바인딩을 통한 임시 상수 할당
if let name = myName {
    print("My name is \(name)")
} else {
    print("myName == nil")
}
// My name is Hoonsbrand



// 옵셔널 바인딩을 통한 임시 변수 할당
if var name = myName {
    name = "Hoon"       // 변수이므로 내부에서 변경이 가능하다.
    print("My name is \(name)")
} else {
    print("myName == nil")
}
// My name is Hoon




// 옵셔널 바인딩을 사용한 여러 개의 옵셔널 값의 추출
var yourName: String? = nil

// friend에 바인딩이 되지 않으므로 실행되지 않는다.
if let name = myName, let friend = yourName {
    print("We are friend! \(name) & \(friend)")
}

yourName = "Dean"

if let name = myName, let friend = yourName {
    print("We are friend! \(name) & \(friend)")
}
// We are friend! Hoonsbrand & Dean

 

if let이란?

옵셔널 결과에 따라 각각 그에 상응하는 행동에 대한 의미가 있다.
만약 non-Optional인 name 변수에 Optional인 myName의 값을 할당할 수 있으면 그에 대한 행동을 하고,
만약 myName의 값이 nil이라면 else 구문이 실행된다.

강제 추출 부분에서의 != nil과 무엇이 다를까?

if 구문이라 그런지 비슷한 로직으로 보여서 왜 바인딩을 해야 하는지 모를 수 있다. 나 또한 그랬다.
하지만 분명한 차이점은 강제 추출을 하냐 안 하냐로 볼 수 있을 것 같다. (느낌표를 사용한 추출)
위에서 언급한 것처럼 강제 추출은 가장 위험한 방법이라고 했다. 
!= nil을 통해 nil 값이 아닌 걸 확인해 주더라도 프로그램이 실행될 때는 어떠한 변수가 생길지 모른다. 
그 상태에서 강제 추출은 언제나 런타임 오류 발생의 가능성을 내포하고 있다.

강제 추출을 하지 않는 바인딩과 같은 방법들이 있는데 굳이 강제 추출을 할 이유가 없다!

 

 

 

 

옵셔널 바인딩은 옵셔널 체이닝과 환상의 결합을 이룬다.

바로 다음으로 옵셔널 체이닝을 알아보자!

 

 

 

 


 

 

 

 

 

옵셔널 체이닝 (Optional Chaining)


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

 

 

 

옵셔널 체이닝

  • 옵셔널을 반복 사용하여 옵셔널이 자전거 체인처럼 서로 꼬리를 물고 있는 모양이기 때문에 옵셔널 체이닝이라고 부른다.
  • 자전거 체인에서 한 칸이라도 없거나 고장 나면 체인 전체가 동작하지 않듯이 중첩된 옵셔널 중 하나라도 값이 존재하지 않는다면
    결과적으로 nil을 반환한다.
  • 호출하고 싶은 옵셔널 변수나 상수 뒤에 물음표(?)를 붙여 표현한다.
  • 옵셔널이 nil이 아니라면 정상적으로 호출될 것이고, nil이라면 결괏값으로 nil을 반환할 것이다.
  • 결과적으로 nil이 반환될 가능성이 있으므로 옵셔널 체이닝의 반환된 값은 항상 옵셔널이다.
느낌표(!)

물음표 대신에 느낌표(!)를 사용할 수도 있는데 이는 옵셔널에서 강제 추출을 하는 효과가 있다고 위에서 언급한 바 있다.
물음표 사용과 가장 큰 차이점은 역시 값을 강제 추출하기 때문에 옵셔널에 값이 없다면 런타임 오류가 발생한다는 점이다.
또 다른 차이점은 옵셔널에서 값을 강제 추출해 반환하기 때문에 반환 값이 옵셔널이 아니라는 점이다.

하지만! 정말 100% nil이 아니라는 확신을 하더라도 사용을 지양하는 편이 좋다.

 

 

 

 

 

 

 

class OfficeRoom {                      // 사무실 호실
    var number: Int                     // 사무실 호실 번호
    
    init(number: Int) {
        self.number = number
    }
}

class Building {                        // 건물
    var name: String                    // 건물 이름
    var officeRoom: OfficeRoom?         // 사무실 호실 정보
    
    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 클래스 설계, 이름이 있으며 주소를 옵셔널로 갖는다.

Address

  • Address 구조체로 설계
  • 주소에는 광역시/도, 시/군/구, 도로명이 필수며, 건물 정보가 있거나 건물이 아니면 상세주소를 기재할 수 있음

Building

  • Building 클래스로 설계
  • 이름이 있으며, 사무실 호실의 정보를 갖는다.

OfficeRoom

  • OfficeRoom 클래스로 설계, 각 사무실 호실은 번호를 갖는다.

 

 

 

 

 

옵셔널 체이닝을 통한 값 할당 시도

// hoonsbrand 인스턴스 생성
let hoonsbrand: Person = Person(name: "hoonsbrand")

hoonsbrand.address?.building?.officeRoom?.number = 105
print(hoonsbrand.address?.building?.officeRoom?.number)     // nil

그 후 hoonsbrand 인스턴스를 생성하여 사무실 호실에 105호를 할당해주었다.

하지만 옵셔널 체이닝을 통해 값을 추출해보려고 하면 nil이 나온다.

그 이유는 아직 hoonsbrand의 address 프로퍼티가 없으며 그 하위의 building, officeRoom 프로퍼티도 없기 때문이다.

그렇기 때문에, 옵셔널 체이닝은 동작 도중에 중지될 것이다. number 프로퍼티는 존재조차 하지 않으므로 105호가 할당되지 않는 것은 당연한 것이다.

 

 

 

 

 

 

옵셔널 체이닝을 통한 값 할당

hoonsbrand.address = Address(province: "서울특별시", city: "강남구", street: "강남대로",
                             building: nil, detailAddress: nil)

hoonsbrand.address?.building = Building(name: "훈스빌딩")
hoonsbrand.address?.building?.officeRoom = OfficeRoom(number: 0)
hoonsbrand.address?.building?.officeRoom?.number = 105
print(hoonsbrand.address?.building?.officeRoom?.number)     // Optional(105)



// 옵셔널 체이닝은 중간에 하나라도 nil이 있으면 nil을 반환한다.
hoonsbrand.address?.building = Building(name: "훈스빌딩")
hoonsbrand.address?.building?.officeRoom = nil
hoonsbrand.address?.building?.officeRoom?.number = 105
print(hoonsbrand.address?.building?.officeRoom?.number)     // nil

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

그 후 officeRoom 단 하나의 프로퍼티에만 nil을 할당해주었을 뿐인데 nil을 반환하는 모습을 볼 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 


해당 게시글은 야곰님의 "스위프트 프로그래밍 3판"을 참고하여 작성하였습니다.

스위프트 프로그래밍(3판): 객체지향, 함수형, 프로토콜 지향 패러다임까지 한 번 ... - 야곰 - Google 도서