Swift 정리 - 8. 옵셔널


Swift의 핵심 기능인 optional에 대해 알아보자.


Optional은 Swift의 세 가지 특징(Safe, Fast, Expressive) 중 Safe를 문법으로 보장하는 기능이다. 옵셔널은 값이 없을 수 있는 상황에서 사용한다. 옵셔널은 두 가지 경우를 표현할 수 있다.

  1. 값이 있다. -> 값이 있는 경우 옵셔널을 unwrap 해서 해당 값에 접근할 수 있다.
  2. 값이 없다.

그렇다면 이런 값이 있는지, 없는지에 대한 상태를 표시해야 하는 이유는 무엇일까? 흔히 Java나 C 등 다른 언어를 사용할 때 데이터가 null일 경우를 대비해 코딩을 한다. 가끔 null처리를 하지 않아 에러가 발생하는 경우도 있었을 것이다. Swift는 이런 경우에서 옵셔널을 사용해서 이렇게 미연에 일어날 수 있는 실수를 방지해준다.

또 데이터 타입 자체를 옵셔널로 설정하면 해당 데이터 타입의 값이 있는지, 없는지를 나타낼 수 있기 때문에 옵셔널 코드를 작성하는 것만으로도 데이터 값에 대한 정보를 줄 수 있다. 즉 데이터에 null이 전달될 수 있어요~ 하고 코드 상으로 바로 알 수 있는 것이다. 이렇게 문법적 표현만으로도 의미를 전달할 수 있기 때문에 편리하다.

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber is inferred to be of type "Int?", or "optional Int"

위의 예시는 문자열을 숫자로 바꾸는 이니셜라이저를 사용한 것이다. 이 이니셜라이저가 실패할 수도 있기 때문에, Int 대신 온셔널 Int를 리턴한다. 즉 Int?를 리턴한다. 물음표는 옵셔널을 나타내며, 위 예시에서는 Int 값이 있을 수도 있고, 아니면 아예 아무 값이 없다 임을 표현한다.

값이 없다는 건 정말 아예 값이 없다는 것이다. 가령 “” 이런 빈 문자열도 값이 있는 것이다. 이는 ‘빈 문자열’이라는 값을 나타내는 것이지, 값이 없는 것은 아니다. 이런 개념은 함수형 프로그래밍 패러다임에서 자주 등장하는 모나드 개념과 일맥상통한다.

nil

옵셔널 변수에 값이 없다는 것을 nil이라는 값을 할당해서 표현할 수 있다.

var serverResponseCode: Int? = 404
// serverResponseCode contains an actual Int value of 404
serverResponseCode = nil
// serverResponseCode now contains no value

nil을 옵셔널이 아닌 상수, 변수에 할당할 수 없다.

만약 옵셔널 변수를 선언하고 초깃값을 제공하지 않은 경우 이 변수는 자동으로 nil이 된다.

var surveyAnswer: String?
// surveyAnswer is automatically set to nil

옵셔널은 enum

옵셔널은 열거형으로 구현되어 있다.

public enum Optional<Wrapped> : ExpressibleByNilLiteral {
  case none
  case some(Wrapped)
  public init(_ some: Wrapped)
  /// 중략
}

위에서 볼 수 있듯이, 옵셔널은

  1. none : 값이 없음
  2. some : 연관 값으로 Wrapped를 가짐

라는 두 가지 케이스가 존재한다는 것이다. 따라서 옵셔널에 값이 있다면 some의 연관 값인 Wrapped에 할당되어 있는 형태를 띄게 된다.

옵셔널이 열거형이기 때문에 switch 문을 통해 케이스마다 확인해서 값이 있는지 없는지를 확인할 수 있다.

func hasValue(_ optionalValue: Any?) -> Bool {
    switch optionalValue {
    case .none:
        return false
    case .some(_):
        return true
    }
}

옵셔널 추출

switch문으로 옵셔널에 값이 있는지 없는지 확인하는 것은 불편하다. some 케이스의 연관 값을 추출하는 것을 옵셔널 추출, Optional Unwrapping이라고 한다. 즉 옵셔널의 값을 옵셔널이 아닌 값으로 추출하는 방식이라는 뜻이다.

If Statements and Forced Unwrapping

옵셔널 강제 추출, Forced Unwrapping은 옵셔널을 가장 간단하게 추출할 수 있지만 가장 위험한 방법이다. 또 옵셔널을 사용하는 의미가 없어지는 방법이기도 하다. 만약 옵셔널이 값을 가지고 있다는 사실이 확실하다면, 옵셔널 이름 뒤에 느낌표를 붙여서 값을 추출할 수 있다. 이는 “나 이 옵셔널이 무조건 값이 있다는걸 아니까 제발 사용해”라는 뜻이다.

if convertedNumber != nil {
    print("convertedNumber has an integer value of \(convertedNumber!).")
}
// Prints "convertedNumber has an integer value of 123."

또 좀 더 안전하게 if 문을 사용해서 옵셔널이 값을 가지고 있는지 아닌지 알 수 있다.

if convertedNumber != nil {
    print("convertedNumber contains some integer value.")
}
// Prints "convertedNumber contains some integer value."

Optional Binding

앞선 방법은 옵셔널을 사용하는 의미가 무색해지는 방법이다. Swift는 이런 방법 말고 안전한 방법인 optional binding을 제공한다. Optional binding을 사용해서 옵셔널이 값이 있는지를 확인하고, 만약 값이 있다면 이 값을 임시 상수나 변수에 할당할 수 있다.

옵셔널 바인딩은 옵셔널이 값이 있는지를 확인하고, 이 값을 상수나 변수로 할당하기 위해 if, while문과 사용될 수 있다.

image

if let actualNumber = Int(possibleNumber) {
    print("The string \"\(possibleNumber)\" has an integer value of \(actualNumber)")
} else {
    print("The string \"\(possibleNumber)\" couldn't be converted to an integer")
}
// Prints "The string "123" has an integer value of 123"

위 코드에서 만약 if 즐이 성공했다면, actualNumber 라는 상수는 if 블럭 내에서만 사용 가능하다. 여기서 이미 값이 옵셔널이 아닌 값으로 할당되었기 때문에 느낌표를 사용해서 값에 접근할 필요가 없다. 옵셔널 바인딩에는 상수와 변수를 모두 사용할 수 있다. 즉 첫 번째 줄에서는 상수 actualNumber에 옵셔널 바인딩을 했지만, var로 선언할 수도 있는 것이다.

또한 하나의 if 문장 안에 콤마로 구분해서 가능한 많은 옵셔널 바인딩과 불리언 조건을 포함시킬 수 있다. 만약 옵셔널 바인딩 중 하나라도 값이 nil이 된다면 조건은 거짓으로 판명나게 되므로 if의 조건은 false가 된다.

if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
    print("\(firstNumber) < \(secondNumber) < 100")
}
// Prints "4 < 42 < 100"

if let firstNumber = Int("4") {
    if let secondNumber = Int("42") {
        if firstNumber < secondNumber && secondNumber < 100 {
            print("\(firstNumber) < \(secondNumber) < 100")
        }
    }
}
// Prints "4 < 42 < 100"

Implicitly Unwrapped Optionals

위에서 봤듯이 옵셔널은 상수나 변수가 “값이 없을 수 있음”을 표현해준다. 가끔 첫 번째 값이 설정된 후에 옵셔널이 항상 값을 가질 것이라고 정해주는 게 더 편할 때가 있다. 이런 경우에는 값이 항상 있을 것이라고 가정되기 때문에 매번 접근할 때마다 값이 있는지 확인하고 unwrapping하는 과정을 없애는 것이 더 낫다.

이런 종류의 옵셔널들은 암시적 추출 옵셔널으로 정의한다. 옵셔널을 표시할 때는 타입 뒤에 물음표를 붙였는데, 암시적 추출 옵셔널은 타입 뒤에 느낌표를 붙여 표시한다.

암시적 추출 옵셔널은 옵셔널이 처음 정의된 직후에 옵셔널에 값이 존재하고 이후에 접근될때도 항상 값이 존재하는 상황에서 유용하다. 이런 암시적 추출 옵셔널은 Unowned References and Implicitly Unwrapped Optional Properties에서 확인할 수 있듯이 클래스 초기화에서 주로 사용된다.

let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // requires an exclamation point

let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // no need for an exclamation point

만약 암시적 추출 옵셔널이 nil이고 이 옵셔널의 wrapped 값에 접근하려고 한다면 런타임 에러가 발생하게 된다.

암시적 추출 옵셔널은 일반 옵셔널과 마찬가지로 nil 여부를 확인할 수 있고 옵셔널 바인딩을 할 수 있다.

if assumedString != nil {
    print(assumedString!)
}
// Prints "An implicitly unwrapped optional string."

if let definiteString = assumedString {
    print(definiteString)
}
// Prints "An implicitly unwrapped optional string."