Roen의 iOS 개발로그

Swift) Optional로 wrapping된 값과 non-optional 값을 == 연산자로 비교할 수 있는 이유

by Steady On

Optional?

Swift에는 Optional이라는 특이한 타입이 있다. 

Swift standard library의 공식 깃허브에서  Optional.swift를 찾아보면,

A type that represents either a wrapped value or the absence of a value.
wrapped 값 혹은 값의 부재를 나타내는 타입

이라고 정의되어 있다.

 

값의 부재

값의 부재, 즉 값이 없다는 의미이다. 다른 언어에서 값의 부재는 '없음, 빈 값'과 같은 의미로 none이나 null로 표시한다. 하지만, Swift에서는 이러한 값의 부재를 nil로 표시하는데, nil은 다른 언어의 none이나 null과는 개념이 다르다. 이를 설명하기 위해서 옵셔널의 구현 코드를 조금 발췌해 왔다.

@frozen
public enum Optional<Wrapped>: ExpressibleByNilLiteral {
	case none          // 요청된 타입과 일하는 타입 또는 값이 아닌 경우(값의 부재)
    case some(Wrapped) // 요청된 타입과 일치하는 타입의 값인 경우
    
    @_transparent
    public init(_ some: Wrapped) { self = .some(some) }
    
    /// 아래는 생략
}

기본적으로 Optional은 열거형으로 만들어져 있다. 열거형은 case에 따라 연관된 항목들을 묶어서 표현할 수 있는 타입이다. 코드를 살펴보면, Optional은 제너릭을 통해 "타입"과 "값"을 Wrapped라는 이름으로 받아온다. 그리고 초기화 함수(init)를 통해 wrapping한 값인 some이 반환된다. 이때 값이 없거나 제너릭을 통해 요구된 타입과 일치하지 않는 경우는 case none이 되면서 nil을 반환한다. 따라서 nil은 단순히 값이 없고, 비어있는 것을 의미하는 것이 아니라 "특정 "타입"에 대한 "값"의 부재"를 의미한다는 점이 none이나 null과의 차이점이다. 즉 바꿔 말하면, "내 눈에 흙이 들어가도 이 타입 아니면 나는 인정 못해!!" 같은거 랄까...

 

Unwrapping

Optional에서 타입도 맞는 올바른 값이 잘 들어갔다고 치자. 하지만, 우리는 한가지 문제에 또 맞닥뜨리게 된다. 바로 값이 wrapped 되어 있다는 것!(wrapped? 했다면 맨위에 올라가서 Optional의 정의를 다시보고 오자) Optional은 값의 부재가 아니면 wrapped value를 나타낸다. wrapped는 비유하자면 포장지 같은거다. Optional 값을 우리가 호출해서 쓰기전까지는 얘가 nil인지, 값이 잘 들어가 있는지 알 수가 없다. 그래서 wrapped(포장된) 값을 unwrapping(택배까기)를 시전해서 진짜 값이 들어있는지 봐야한다는 거다.

 

예를 들어 우리가 내일 손님을 집에 초대해서 떡볶이를 만들려고 한다. 이미 늦은 밤이라 마트는 문을 닫았고, 가래떡은 집에 없다 그래서 쿠X로 가래떡을 주문했다고 생각하자. 다음날 아침 택배박스가 집앞에 놓여있었다. 이 박스를 까보기 전까지는 안에 떡이 잘 들어있는지 아닌지 알 수가 없다(슈뢰딩거의 가래떡.....). 그래서 우리는 떡이 없을 경우에 대비하면서 이 박스를 까봐야 한다.

 

if, guard 문으로 안전하게 unwrapping 하기

첫번째 방법은 박스에 가래떡이 없을 경우에 대해 생각하고 대안을 짜는 것이다.

func 떡볶이만들기(_ 박스: 가래떡?) {
    guard let 박스 else { return }
    
    박스에서 가래떡을 꺼낸다.
    가래떡으로 떡볶이를 만든다.
}

guard문을 사용하는 경우 박스를 깠는데 가래떡이 아니면, 걍 떡볶이 만들기를 미련없이 포기해버리는 것이다. 그냥 빠르게 함수를 종료(Early Exit)한다. 근데 만약에 가래떡이 들어있다면, 박스라는 변수에 담긴 값은 Unwrapping 되었으므로 함수 내에서 실행되는 다음 실행문에서 더이상의 unwrapping 과정없이 마음놓고 사용할 수 있다.

 

그런데 다른 음식을 하려니까 친구가 곧 죽어도 떡볶이를 먹어야겠다고 한다. 그럼 가래떡이 안들어 있을 상황에 대비해서 다음 대안을 제시해야 한다.

func 떡볶이만들기(_ 박스: 가래떡?) {
    if let 박스 {
        박스에서 가래떡을 꺼낸다.
        가래떡으로 떡볶이를 만든다.
    } else {
        배달앱을 켠다.
        떡볶이를 배달 시킨다.
    }
}

if문을 사용하는 경우는 unwrapping된 값이 nil인 경우의 대안을 else문으로 짤 수 있다. 그러나 if문으로 unwrapping된 값은 if 블록 안에서만 unwrapping 상태로 사용할 수 있고, 블록 밖에서 사용하기 위해서는 다시한번 unwrapping과정을 거쳐야한다.

 

??로 default 값 주기

가래떡을 주문하고 났더니 불현듯 냉동실에 있는 떡국떡이 생각났다. 친구는 떡국떡으로 만든 떡볶이도 괜찮다고 한다. 그래서 내일 가래떡이 안 들어 있다면 떡국떡으로 떡볶이를 만들려고 한다. 이런경우는 ?? 연산자를 사용해서 nil인 경우에는 그냥 이값을 대신해서 써달라고 할 수 있다.

func 떡볶이만들기(_ 박스: 가래떡?) {
    let 떡 = 박스 ?? 떡국떡
    
    떡으로 떡볶이를 만든다.
}

 

!(강제 언래핑)

그리고 마지막으로 냉동실에 떡국떡도 없다. 하지만, 나는 박스에 떡이 있든 없는 떡볶이를 만드는 것을 강행하겠다고 할 수 있다. 이런 경우는 ! 연산자를 사용해서 무조건(대책없이) 박스를 까버리는 것이다.

func 떡볶이만들기(_ 박스: 가래떡?) {
    let 떡 = 박스!
    
    떡으로 떡볶이를 만든다.
}

떡이 없는데 떡볶이를 하려면 어떻게 되겠는가?(오뎅볶이..?) 현실에서야 그냥 떡볶이맛 오뎅볶이지만, 이게 앱에 그대로 가져간다고 생각하면, 우리의 앱은 crash가 나고 멈춰버릴 것이다. 그래서 강제 언래핑을 해야하는 경우는 반드시 값이 들어 있다는 것을 확인 하는 과정이 있어야 한다.

func 떡볶이만들기(_ 박스: 가래떡?) {
    고객센터에 전화해서 박스 안에 가래떡을 넣었는지 확인한다. // -_-;;; 진상
    
    let 떡 = 박스!
    
    떡으로 떡볶이를 만든다.
}

이렇게 했는데도 희박한 확률로 떡이 안들어 있을 수도 있으니 웬만하면 쓰지말자.

 

이렇게 Optional 값의 포장을 푸는 방법은 세가지가 있는데, 신기하게도 우리는 포장을 풀지 않고도 이 박스가 가래떡이 포장된 박스인지 == 연산자로 비교할 수가 있다. 그 근거는 다음과 같다.

 

wrapped value와 non-optional을 ==으로 비교하기

아까 위에서 언급한 Swift 표준 라이브러리의 Optional 구현코드를 보면 353번째 줄에 이런 extension이 있다.

extension Optional: Equatable where Wrapped: Equatable {
    @_transparent
    public static func ==(lhs: Wrapped?, rhs: Wrapped?) -> Bool {
        switch (lhs, rhs) {
        case let (l?, r?):
            return l == r
        case (nil, nil):
            return true
        default:
            return false
        }
    }
}

코드를 해석해보면, ==의 왼쪽 값을 lhs, 오른쪽 값을 rhs라는 매개변수명으로 받아온다. 그리고 변수의 타입을 Optional로 받는다. 그러니까 비교하자면, 가래떡이 포장된 박스안에 들은게 가래떡과 같냐를 박스를 까고 떡과 떡을 바로 비교하는게 아니라 일단 비교할 가래떡을 포장을 한번 해서 얘네가 같은지 보는 느낌이라고나 할까...?(실제로 그런식으로 쓰여 있음)

 

An instance that is expressed as a literal can also be used with this operator. In the next example, an integer literal is compared with the optional integer `numberFromString`. The literal `23` is inferred as an `Int` instance and then wrapped as an optional before the comparison is performed.
literal로 표현 되어진 인스턴스도 이 연산자(==)를 사용할 수 있다. 다음 예시에서, integer literal은 optional integer`numberFromString`와 비교되어진다. 23은 Int의 인스턴스로 나타내지지만, 비교가 수행되기 전에 optional로서 wrapped 된다.
if 23 == numberFromString {
	print("It's a match!")
}
// Prints "It's a match!"

(거봐 내말이 맞지?) 아무튼 그렇다고 한다.

 

마무리

사실 여지껏 배운 다른 언어에는 없는 개념이라 솔직히 처음엔 이해하기도 힘들었고 왜쓰는지도 몰랐는데, 앱을 만들면서는 예상치 못한 값이나 타입에 대비할 수 있는 고마운 녀석이라고 많이 느끼는 것 같다. 

 

참고자료

https://github.com/apple/swift/blob/main/stdlib/public/core/Optional.swift

 

GitHub - apple/swift: The Swift Programming Language

The Swift Programming Language. Contribute to apple/swift development by creating an account on GitHub.

github.com

https://www.swift.org/

 

Swift.org

Swift is a general-purpose programming language built using a modern approach to safety, performance, and software design patterns.

www.swift.org

 

블로그의 정보

Roen의 iOS 개발로그

Steady On

활동하기