Optional

yunyezl·2022년 5월 9일
0

Swift 개념 다지기

목록 보기
2/2
post-thumbnail

특정 변수에는 값이 존재할 수도 있고, 존재하지 않을 수도 있습니다. 이러한 상태를 표현하기 위해 Optional이라는 개념이 나타나게 되었습니다.
Optional로 선언된 변수에는 값이 존재하지 않을 수 있으므로, nil 값을 넣을 수 있다는 것이 일반 변수(Non-Optional Type)와의 가장 큰 차이점입니다. nil 값을 넣어줌으로써, 해당 변수에는 값이 존재하지 않음을 명시적으로 나타낼 수 있는 것입니다.

따라서 Optional 변수는 nil 값을 받을 수 있는 변수라고 정의할 수 있습니다. (반대로, Non Optional Type 변수의 경우에는 nil 값을 받을 수 없다는 의미로 해석할 수 있습니다.)

존재할 수도 있고, 존재하지 않을 수 있다? 이게 무슨 슈뢰딩거의 변수인가요?

간단한 예시를 통해 알아봅시다.
자료 구조 중 연결 리스트(Linked List)란 자료 구조에 대해 들어보셨나요?
연결 리스트의 경우, 값을 삭제하거나 삽입할 시 상당한 비용이 드는 배열의 단점을 보완하기 위해 별도의 메모리 공간을 이용하여 자신과 연결된 노드에 대한 주소값을 저장한다는 특징을 가지고 있습니다.

그렇다면, 이러한 연결 리스트의 마지막 노드를 찾는 방법은 무엇일까요?
그림을 봤을 때는 3이라는 값을 저장하고 있는 노드가 마지막 노드란 것은 자명한 사실이지만, 코드를 통해 이를 어떻게 구분할 수 있을까요?

마지막 노드와 다른 노드의 차이점에 대해 잠시 고민해보면 답을 찾을 수 있습니다.
마지막 노드의 경우 주소값을 저장하기 위한 공간에 어떠한 주소값도 저장하지 않고 있습니다.

따라서 우리는 다른 데이터의 주소값을 저장하기 위한 메모리 공간에 어떠한 값도 저장하지 않은 노드를 찾았을 경우, 그 노드를 마지막 노드라고 부를 수 있게 됩니다.

그렇다면 우리는 주소값을 저장하기 위한 공간에 주목해볼 수 있습니다. 이 공간은 Last 노드인지 아닌지에 따라 값이 존재하지 않기도 하고, 존재하기도 합니다. 어디서 많이 들었던 말 아닌가요?

맞아요! 여기서 앞서 봤던 Optional에 대한 개념을 사용할 수 있습니다.

다른 데이터의 주소값을 저장하는 해당 메모리 공간을 Optional한 공간이라고 표현할 수 있습니다.
이 공간은 해당 공간이 마지막 공간인지 아닌지에 따라 값이 존재하기도 하고, 존재하지 않을 수도 있기 때문입니다.

따라서 연결 리스트에 사용되는 노드를 아주 간단한 코드로 표현해보면 다음과 같이 작성해볼 수 있습니다.

class Node<T> {
	var data: T
	var next: Node?
    ...
}

다음 데이터의 주소값을 저장하는 next 변수의 경우 ? 키워드를 부여하여 해당 변수에 nil 값을 저장할 수 있습니다.
따라서, next 변수에 주소값이 들어있는지 nil 값이 들어있는 지에 따라 우리는 마지막 노드를 찾을 수 있게 됩니다.

그렇다면 Optional은 어떻게 구현되어있을까요?

Optional의 경우, Optional이란 이름의 enum 형태로 구현이 되어있습니다. 우리가 사용했던 ? 키워드는 Optional enum의 축약형이었습니다.
?는 단순 축약형일뿐, 실제로 Optional 타입은 enum이므로 다음과 같이 표현하는 것도 가능합니다.

class Node<T> {
	var data: T
	var next: Optional<Node>
    ...
}

Optional Enum은 어떤 타입이든 감쌀 수 있어야 하므로 Generic을 사용합니다.
Enum의 Case는 none, some(Wrapped) 두 가지 케이스로 이루어져있습니다.

@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
    ... 
}

Optional.none의 경우 ni과 같은 의미를 가지고, Optional.some(Wrapped)의 경우 Optional으로 감싸진 특정 값을 의미합니다.

let number: Int? = Optional.some(42)
let noNumber: Int? = Optional.none
print(noNumber == nil)
// Prints "true"

Optional 변수는 특정 값을 Optional enum으로 감싼 형태이기 때문에, 사용하기 위해서는 해당 값을 꺼내주어야 합니다. 바로 사용할 수 없습니다.

다음과 같이 imagePath의 경로를 저장하는 딕셔너리 변수가 있다고 합시다.

let imagePaths = ["star": "/glyphs/star.png",
                  "portrait": "/images/content/portrait.jpg",
                  "spacer": "/images/shared/spacer.gif"]

이 딕셔너리 변수를 imagePaths[”star”]와 같이 접근하면, 바로 String 타입의 값을 리턴할까요?
그렇지 않습니다. Optional<String> 형태의 값을 리턴합니다. 존재하지 않는 key 에 접근할 경우 nil값을 뱉기 위함입니다.

그렇다면,

어떻게 Optional로 감싸져있는 String 값을 가져올 수 있을까요? 또, 잘못된 key에 접근해서 해당 변수가 nil 값을 갖고 있다면 그 것을 어떻게 판별할 수 있을까요?

Swift는 Optional enum으로 감싸져있는 값을 안전하게 꺼내올 수 있는 다음과 같이 몇 가지 방법을 제공합니다.

(! 키워드로 값을 강제로 꺼내오는 방법도 존재하지만, 해당 값이 nil 값을 가질 때 오류를 뱉으므로 해당 방법은 권장되지 않습니다. 해당 값이 반드시 값을 가지고 있다는 확신이 있을 때만 사용되나, Optional로 값을 감싼 것 자체가 해당 값이 nil이 될 수 있음을 내포하므로 적절하지 않습니다.)

Unwrapping Optional - 값을 꺼내오는 여러 방법들

Optional Binding

Optional로 감싸진 값을 조건부로 새로운 변수에 할당하는 방법입니다. (할당하지 않고 _ 키워드로 변수를 생략할 수도 있습니다.)

여기서 말하는 조건은, Optional 변수가 nil이 아닌 경우입니다.
Optional 변수가 nil이 아닌 경우, Optional로 감싸져있던 값을 꺼내서 새로운 변수에 할당하여 사용할 수 있습니다.

optional binding 제어 구조는 if let, guard let 그리고 switch를 사용하는 방법이 있습니다.

  • if let
    if let starPath = imagePaths["star"] {
        print("The star image is at '\(starPath)'")
    } else {
        print("Couldn't find the star image")
    }
  • guard let
    guard let startPath = imagePaths["star"] else { return }
  • switch
    if let, guard let에 비해 다소 낯설 수 있으나 switch문으로도 optional binding이 가능합니다.
    Optional은 enum 타입이라고 했던 것 기억나시나요? none과 some 케이스가 있었죠. 이 점에 대해 조금만 생각해보면 switch 문을 사용할 수 있는 것은 당연한 것입니다.
    코드를 작성할 때 enum 값을 정의한 후 특정 값에 enum case를 할당하고 switch 문을 통해 enum case별로 로직을 설정하는 경우는 굉장히 흔합니다.
    Optional 역시 enum이므로, switch를 통해 분기 처리를 해줄 수 있으며 값을 꺼내올 수 있는 것입니다.
    var t1: Int? 
    
    switch t1 {
    case .some(let data):
        print("t1 is \(data)")
    case .none:
        print("t1 is nil")
    }
    // print
    // t1 is nil
    다음과 같은 방식으로도 접근할 수 있지만 값을 꺼내오는 느낌은 아닙니다.
    var t1: Int? = 1
    
    switch t1 {
    case 1:
        print("t1 is \(t1)")
    default:
        print("t1 is nil")
    }
    
    // print
    // t1 is Optional(1)

주로 사용하는 방식은 if letguard let 방식일 것입니다. 언뜻 보면 비슷해보이지만 미묘하게 다른 쓰임새를 가지고 있습니다.

if let의 경우 Optional 변수에 값이 존재할 때, 해당 if block 내에서만 새로운 변수를 사용할 수 있습니다.

guard let의 경우 guard를 사용하는 해당 블록 내에서 완전히 접근 가능하게 선언됩니다.

또한 if let은 else 문을 생략할 수 있지만 guard let은 else 문을 생략할 수 없습니다.
추가로, guard else 문 내에 return, break, continue, throw 등의 제어문 전환 명령어를 반드시 넣어주어야 합니다.

저는 함수 블록 내에서 optional 값에 따라 early exit를 해야하는 경우 주로 guard 문을 활용하고 옵셔널 변수의 값이 nil 인지, 아닌지에 따라 개별 로직이 존재하는 경우에는 if let을 활용합니다.
특성에 맞게 적절하게 사용하면 프로그램의 가독성을 높여줄 수 있겠죠!

+) Why guard? 왜 guard 라는 이름을 사용할까요?
guard문은 다음과 같은 형태로 되어있습니다.

guard 조건 else { 
	// 조건이 false인 경우
	return || throw || ...
}

guard 키워드는 말 그대로 특정 조건에 부합하지 않은 상황으로부터 방어를 해줍니다.
해당 조건에 맞지 않을 경우 이후 상황으로의 진입을 막아버림으로써 에러 발생 가능성으로부터 guard를 해주는 것이죠.
따라서 특정 조건에 따라 이후의 코드에 접근하는 것을 막기 위한 용도의 코드를 작성하고자 한다면, if문보다는 guard 문을 사용하는 것이 가독성을 높여줄 수 있을 것입니다.

Swift Programming Lauguage에서 early exit 로직의 경우 guard를 사용하여 예시를 들고 있기도 하구요. 😀

Optional Chaining

Optional Chaining이란 옵셔널일 수 있는 값의 postfix에 ? 키워드를 붙여, 최종적인 값을 가져오거나 해당 값에 nil이 포함될 경우에는 nil을 리턴하는 방식입니다.
값에 연쇄적으로 접근할 수 있으며, 연결된 어떤 값 중 하나라도 nil이 존재하면 최종적으로 nil을 반환합니다.

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

Person 객체가 있고 Person 객체는 residence를 가지고 있을 수도, 가지고 있지 않을 수도 있습니다.

어떤 Person이 존재하고 이 Person이 몇 개의 방을 가지고 있는 지를 알고 싶다고 해봅시다.
우리는 방의 갯수에만 관심이 있고, 이 사람이 거주지가 존재하는지 아닌지에는 관심이 없습니다. 다시 말해 residence가 존재하지 않을 때의 추가적인 로직이 필요없는 경우입니다.

이 경우 Optional Chaining을 사용할 수 있습니다.

let john = Person()
john.residence = Residence()

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}

//John's residence has 1 room(s).

만약 residence 인스턴스를 할당하지 않았다고 했을 때에는 else 블록을 호출합니다.
numberOfRooms의 값은 옵셔널이 아니지만, 연결된 값 중 하나라도 nil이 존재할 때 최종적인 값은 nil이 되기 때문입니다.

nil coalescing (nil 병합 연산자)

nil 병합은 ?? 오퍼레이터를 사용하여 값을 꺼내오는 방식으로, default value를 설정해주는 것이 가장 큰 특징입니다. 다음과 같은 구조 입니다.

Optional_Type_Value ?? Default_Value

왼쪽에는 unwrap할 대상의 Optional 값이 오고, 왼쪽에는 unwrap한 값이 nil 값일 경우 nil 값을 대신해서 넣어줄 기본값이 들어갑니다.

func getDefault() -> Int {
    print("Calculating default...")
    return 42
}

let goodNumber = Int("100") ?? getDefault()
// goodNumber == 100

let notSoGoodNumber = Int("invalid-input") ?? getDefault()
// Prints "Calculating default..."
// notSoGoodNumber == 42

String 변수를 Int 형으로 변환하는 예제입니다. String은 언제나 Int가 되지는 않으므로, String을 Int로 변환할 경우 Int값은 Optional 변수로 감싸져나옵니다. Int로 변환할 수 없는 경우에는 nil 값을 뱉을 것입니다.

이 때 nil 값을 사용하고 싶지 않고, nil 값 대신 기본값을 넣어주고 싶다면 nil 병합 연산자를 이용할 수 있습니다.


기타

여기까지 안전하게 Optional Type Value를 언래핑하는 방법에 대해 알아보았습니다.
이 외에도 force unwrapping, Implicitly Unwrapped Optional 를 활용하여 값을 꺼내올 수도 있는데요, 이번 포스팅에서는 값을 안전하게 가져오는 방법들에 대해서만 알아보도록 할게요 :)

오늘도 읽어주셔서 감사합니다! 😀
오류가 있다면 언제든지 지적해주세요.

profile
나의 언어로 설명할 수 있을 때까지 😎

0개의 댓글