Skip to content

Latest commit

 

History

History
206 lines (138 loc) · 16 KB

Appendix.OptionalChain.md

File metadata and controls

206 lines (138 loc) · 16 KB

옵셔널 체인

옵셔널 타입의 문제점

클래스나 구조체에 옵셔널 타입이 설정되었을 때에 대해 이야기해봅시다. 스위프트에서 제공하는 옵셔널 타입은 nil이 할당될 수 있는 값을 말합니다. nil은 초기에 값 할당이 일어나지 않았을 때 대입되지만, 값을 처리하는 과정에서 오류가 발생했을 때 대입되는 값이기도 합니다.

옵셔널 타입은 항상 nil 여부를 검사하여 정상적인 값이 저장된 것을 확인한 후에 사용하는 것이 안전하므로 if 구문을 통해 옵셔널 타입을 처리하는 경우가 많습니다. 여기에서 옵셔널에 대한 이슈가 발생하는데, if 구문을 통해 값의 안전성 여부를 검사해야 한다는 것입니다. 코드가 길어지기 때문이죠. 어차피 if 구문을 통해 값의 정상 여부를 검사해야 안전하게 사용할 수 있는 것이라면 굳이 옵셔널 타입을 사용할 필요도 없다는 것이 옵셔널을 다루어본 많은 사람들의 의견입니다.

문제는 클래스나 구조체가 옵셔널 타입과 관련되었을 때 발생합니다. 클래스나 구조체 등의 객체는 내부적으로 프로퍼티나 메소드를 소유하는데, 이때 클래스나 구조체의 인스턴스가 옵셔널 타입으로 선언될 경우 프로퍼티와 메소드를 호출하기 위해서는 매번 if 구문을 통해 옵셔널 인스턴스의 정상값 여부를 검사해야 합니다. 다음 코드를 봅시다.

struct Human {
var name: String?
var man: Bool = true
}

var boy: Human? = Human(name: "홍길동", man: true)

인간을 나타내는 Human 구조체를 정의하고, 이름을 저장할 name 프로퍼티, 남성 여부를 저장할 man 프로퍼티를 각각 추가하였습니다. 이어서 이 구조체를 인스턴스로 생성하여 변수에 할당하되 옵셔널 타입의 변수 boy에 할당하고 있습니다.

일단 옵셔널 타입으로 선언된 이상, 변수 boy를 사용하려면 옵셔널 타입에 대한 안전성 검사가 필요합니다. boy 인스턴스로부터 name 프로퍼티를 참조하려면 이 역시 옵셔널 타입이므로 다시 안전성 검사가 필요합니다. 다음과 같은 구문을 통해 name 프로퍼티를 참조해야 하죠.

if boy != nil {
    if boy!.name != nil {
        print("이름은 \(boy!.name!)입니다")
    }
}

또는 옵셔널 비강제 해제 구문을 사용하여 다음과 같이 작성할 수도 있습니다.

if let b = boy {
    if let name = b.name {
        print("이름은 \(name)입니다")
    }
}

어느 방식이든 안정성을 담보하려면 if 구문의 처리를 피할 수 없습니다. 만약 Human 구조체를 다른 구조체나 클래스가 프로퍼티로 사용하되, 이를 옵셔널 타입으로 설정한다면 name 프로퍼티를 참조하기 위한 코드는 훨씬 더 복잡해집니다.

struct Company {
var ceo: Human?
var companyName: String?
}

var startup: Company? = Company(ceo: Human(name: "김대표", man: false
), companyName: "김밥해븐")

Human 타입의 구조체 인스턴스를 옵셔널 타입으로 할당받는 ceo 프로퍼티와, 마찬가지로 옵셔널 타입이지만 문자열을 입력받는 companyName 프로퍼티가 선언된 Company 구조체입니다. 이 구조체 역시 옵셔널 타입으로 변수 startup에 할당되었습니다. 이제 변수 startup을 이용하여 ceo 프로퍼티의 내부 프로퍼티인 name을 참조하기 위한 머나먼 여정(?)을 떠나봅시다.

가장 먼저 해야 할 일은 startup의 옵셔널 타입을 해제하는 것입니다.

if let company = startup {

}

startup에 정상 값이 할당되어 있다면 위 구문의 실행 결과로 Company라는 상수에는 옵셔널이 해제된 Company 타입의 인스턴스가 들어있게 됩니다. 다음으로 이 Company를 사용하여 ceo 프로퍼티의 옵셔널을 해제해야 합니다.

if let company = startup {
    if let ceo = company.ceo {

    }
}

마지막으로 ceo 프로퍼티의 하위 프로퍼티인 name 역시 옵셔널 타입이므로 해제 과정을 거쳐야 회사의 대표이사 이름을 참조해낼 수 있습니다.

if let company = startup {
    if let ceo = company.ceo {
        if let name = ceo.name {
            print("대표이사의 이름은 \(name)입니다")
        }
    }
}

이러한 과정을 피하고 코드를 더욱 간결하게 작성하기 위해 강제 해제 연산자 !를 사용하여 if 구문 없이 인스턴스의 옵셔널 타입을 해제할 수도 있지만, 이것은 언제든 발생할 가능성이 있는 런타임 오류라는 위험요소를 내포하고 있으므로 추천할 만한 방법은 아닙니다. 강제 해제 연산자를 사용하는 어느 중간 과정이 하나라도 nil을 포함하고 있다면 여지없이 런타임 오류가 발생하기 때문입니다.

if let name = startup!.ceo!.name {
    print("대표이사의 이름은 \(name)입니다")
}

여러분이 앱을 만들기 위해 학습하게 될 코코아 터치 프레임워크에서는 이처럼 옵셔널 타입을 중첩해서 사용해야 하는 경우가 많습니다. 단계적으로 객체를 만들어 다음 단계로 접근해야 하는데, 이때 각 단계별 결과물이 옵셔널 타입인 경우가 대부분이기 때문입니다.

이처럼 옵셔널 타입이 중첩되어 있을 땐 매번 if 구문을 중첩해서 작성하는 것은 코드를 작성해야 하는 입장에서 상당한 부담이 됩니다. 작성한 코드를 쉽게 파악하기도 어려울뿐더러 실제로 구현해야 하는 논리 흐름에 집중하기보다는 객체의 오류 가능성을 차단하고자 몇 배나 되는 코드를 작성해야 하는 것도 문제죠. 이러한 옵셔널의 치명적인 단점을 극복하고 복잡한 코드를 간단하게 줄여주는 방법으로 도입된 것이 바로 옵셔널 체인입니다.

옵셔널 체인

옵셔널 체인(Optional Chain)은 옵셔널 타입으로 정의된 값이 하위 프로퍼티나 메소드를 가지고 있을 때, 이 요소들을 if 구문을 쓰지 않고도 간결하게 사용할 수 있는 코드를 작성하기 위해 도입되었습니다. 옵셔널 체인의 기본 페러다임은 오브젝티브-C의 특성 중 하나로부터 비롯되는데, 그것은 바로 오브젝티브-C 언어에서 nil인 객체에 메시지를 보내도 아무런 오류가 발생하지 않는다는 점입니다.

여기에서 말하는 메시지는 일반 객체지향 언어에서의 메소드를 의미합니다. 오브젝티브-C가 스몰토크 언어 기반이므로 메소드를 호출하는 것 대신 메시지를 보내는 것으로 처리될 뿐입니다. 객체지향식으로 이야기하자면, 오브젝티브-C에서는 nil인 객체의 메소드나 프로퍼티를 호출하더라도 오류가 발생하지 않습니다. 단순히 아무 일도 일어나지 않을 따름이죠. 일반적으로 자바나 C# 등과 같은 객체지향 언어에서 존재하지 않는 객체의 메소드나 프로퍼티를 호출하면 NullPointException이 발생하는 것과 대조적입니다.

이처럼 옵셔널 체인은 객체가 nil인 상황에서 안전성 검사를 하지 않고 메소드나 프로퍼티를 호출하더라도 오류가 발생하지 않을 수 있는 문법을 옵셔널 스타일을 이용하여 구현합니다. 옵셔널 타입을 정의할 때 ? 연산자를 사용했었는데, 옵셔널 타입을 참조할 때도 이 연산자를 사용합니다. 그다음에 이어서 필요한 프로퍼티나 메소드를 참조하는 거죠. 이를테면 앞의 예제에서 옵셔널 타입으로 선언된 startup 변수 하위의 ceo 프로퍼티를 참조하려면 다음과 같이 호출하는 방식입니다.

startup?.ceo

만약 startup이 옵셔널 타입이 아니라 단순한 Company 타입이었다면 우리는 ceo 프로퍼티를 참조하기 위해 startup.ceo로 호출했을 겁니다. 옵셔널 체인은 이 기본 형태의 구문에서 옵셔널 타입의 객체 바로 뒤에 ? 연산자만 붙여준 형태라고 할 수 있습니다. 이렇게 작성된 옵셔널 체인 구문에 의해 startup이 정상적으로 Company 인스턴스를 저장하고 있다면 그 인스턴스의 ceo 객체가 반환되겠지만, 설령 startup 변수에 nil 값이 할당되어 있더라도 잘못된 참조에 의한 오류는 발생하지 않습니다. 그저 아무 일도 일어나지 않을 따름이죠.

조금 더 이야기를 발전시켜 봅시다. 앞에서 구현해 본 것처럼 ceo 프로퍼티의 하위 프로퍼티인 name을 참조하고자 하면 앞에서 작성된 구문을 계속 이어서 작성해 나가면 됩니다. 여기서 ceo 프로퍼티 역시 옵셔널 타입으로 선언되어 있으므로 뒤에 ? 연산자를 붙여주면 되죠. 결국 name 프로퍼티를 참조하려면 다음과 같은 형태가 됩니다.

startup?.ceo?.name

여러 개의 객체가 계층적으로 선언되어 객체의 프로퍼티가 하위 프로퍼티를 가지고 있고, 그 아래에 다시 하위 프로퍼티가 있을 때, 각 프로퍼티들이 옵셔널로 선언되어 있다 하더라도 이렇게 옵셔널 연산자를 이용하여 옵셔널 속성을 연결해서 처리할 수 있습니다. 이처럼 한 번 옵셔널 연산자로 처리된 구문에 계속해서 옵셔널 연산자를 붙여 코드를 작성해 나갈 수 있다는 의미에서 옵셔널 체인(Optional Chain)이라는 명칭이 사용되었습니다.

앞에서 작성했던 여러 번의 중첩된 if 구문을 옵셔널 체인을 이용하여 작성해보면 다음과 같습니다.

if let name = startup?.ceo?.name {
    print("대표이사의 이름은 \(name)입니다")
}

맨 마지막 값 자체는 옵셔널 체인에 해당하지 않습니다. 옵셔널 체인으로 처리할 수 있는 것은 하위 속성이나 메소드를 호출해야 할 때 입니다. 마지막 값은 다시 하위 속성이나 메소드를 호출하는 것이 아니라 직접 사용해야 하는 값이므로 옵셔널에 대한 검사가 필요합니다. 하지만 값을 참조하는 것이 아니라 할당해야 한다면 옵셔널 체인을 이용하여 다음과 같이 간편하게 구문을 작성할 수 있습니다.

startup?.ceo?.name = "김사장"

이때 만약 startup 변수나 ceo 프로퍼티가 빈 값이라면 아무런 값도 할당되지 않은 채로 구문은 종료됩니다. nil 객체의 프로퍼티에 값을 할당해줄 수는 없기 때문입니다. 하지만 오류는 결코 발생하지 않으므로 안전하게 값을 할당할 수 있습니다.

일반적으로 옵셔널 체인에는 다음과 같은 특징이 있습니다.

1) 옵셔널 체인으로 참조된 값은 무조건 옵셔널 타입으로 반환된다.

2) 옵셔널 체인 과정에서 옵셔널 타입들이 여러 번 겹처 있더라도 중첩되지 않고 한 번만 처리된다.

차례로 이야기해 보겠습니다. 우선 옵셔널 체인으로 참조된 값은 반드시 옵셔널 타입으로 반환 됩니다. 옵셔널 체인 구문에서 마지막에 오는 값이 옵셔널 타입이 아닌 일반 값일지라도 옵셔널 체인을 통해 참조했다면 이 값은 옵셔널 타입으로 변경됩니다.

print(startup?.ceo?.man)
// Optional(false)

Human 구조체에서 일반 타입으로 선언된 man 프로퍼티이지만, 참조한 결과는 옵셔널 타입으로 확인됩니다. 이는 옵셔널 체인을 통해 이 프로퍼티를 참조했기 때문이며 만약 옵셔널 체인을 사용하지 않고 단계적으로 옵셔널 타입을 해제해서 참조했다면 일반 타입의 값으로 반환되었을 겁니다. 옵셔널 체인을 사용하면 반드시 옵셔널 타입으로 반환되는 이유는 옵셔널 체인이라는 구문 자체가 nil을 반환할 가능성을 내포하고 있기 때문입니다.

반환 타입은 항상 가능한 모든 타입을 포함할 수 있는 자료형이어야 합니다. 그리고 nil이 반환될 가능성이 있는 모든 객체는 옵셔널 타입으로 반환되어야 하죠. 이같은 기본 룰에 따라 옵셔널 체인의 결과값은 마지막 값의 옵셔널 타입 여부와 관계없이 옵셔널 타입으로 반환됩니다.

옵셔널 체인의 두 번째 특성을 살펴봅시다. 중첩된 참조 구문에서 옵셔널 체인이 여러 번 반복되면 그만큼 옵셔널 타입이 중첩되는 것이 아닐까 생각할 수 있습니다. 예를 들어, 위의 예에서 name 프로퍼티에 값이 할당되어 있을 때 옵셔널 체인을 두 번 사용하면 옵셔널 타입을 다시 옵셔널 타입으로 감싸게 된다는 것이죠.

startup?.ceo?.name => Optional(Optional("나대표 "))

하지만 옵셔널 체인은 이러한 방식으로 동작하지 않습니다. 옵셔널 타입을 몇 번 중첩하더라도 결국 반환할 수 있는 값은 nil 또는 정상값 두 개로 나누어지므로 단순히 하나의 옵셔널 객체로 감싼 값일 뿐입니다.

Optional(Optional(Optional(123))) = Optional(123)

이 때문에 옵셔널 체인으로 처리된 값은 그 과정에서 몇 번의 옵셔널 체인이 반복되더라도 하나의 옵셔널 객체로만 반환됩니다.

옵셔널 체인은 프로퍼티뿐만 아니라 메소드에서도 사용할 수 있습니다. 메소드에서는 주로 반환값이 구조체나 클래스, 또는 열거형 등으로 구성되어 그 내부에 있는 프로퍼티나 메소드를 사용해야 할 때 옵셔널 체인을 사용하면 효율적입니다. 다음 예제를 봅시다.

struct Company {
    var ceo: Human?
    var companyName: String?
    
    func getCEO() -> Human? {
        return self.ceo
    }
}

앞에서 사용했던 Company 구조체에 getCEO라는 메소드를 추가했습니다. 이 메소드는 Human 타입의 값을 반환하죠. 다만 내부적으로 self.ceo 프로퍼티를 반환하는 만큼 그에 맞는 옵셔널 타입으로 반환하도록 정의되어 있습니다. 이 메소드를 거쳐서 ceo의 name 값을 참조해봅시다.

var someCompany: Company? = Company(ceo: Human(name: "Tim Cook", man: true), companyName: "Apple")

let name = someCompany?.getCEO()?.name
if name != nil {
    print("대표이사의 이름은 \(name!)입니다.")
}

메소드의 경우도 프로퍼티와 크게 다르지 않습니다. 사실 메소드 자체를 옵셔널 체인으로 사용하는 것이 아니라 메소드의 결과값을 옵셔널 체인으로 사용하는 것입니다. 이 때문에 메소드의 괄호 다음에 옵셔널 체인 연산자를 붙이고 있죠. 메소드 자체를 옵셔널 체인 형식으로 사용하는 것은 옵셔널 메소드일 때만 가능합니다. 이것은 이후 프로토콜에서 옵셔널 메소드에 대해 학습할 때 다룹니다.

사실 옵셔널 체인은 옵셔널 강제 해제 구문과 매우 흡사합니다. 외형상 차이점이라면 ? 연산자와 ! 연산자의 차이 정도가 되겠죠.

  • 옵셔널 체인 구문: someCompany?.getCEO()?.name

  • 옵셔널 강제 해제: someCompany!.getCEO()!.name

하지만 그 결과는 사뭇 다릅니다. 옵셔널 체인의 결과값은 옵셔널 타입이지만 강제 해제 연산자의 결과값은 일반 타입이라는 차이점 이외에도 옵셔널 체인이 적용된 객체가 nil이라도 오류가 발생하지 않는 데 비해 옵셔널 강제 해제를 사용하면 객체가 nil일 경우 런타임 오류가 발생합니다.

옵셔널 객체가 반드시 nil이 아니라고 확신 할 수 있다면 옵셔널 강제 해제 구문을 통하여 구문을 간단하게 줄여서 사용하겠지만, 그렇지 않더라도 옵셔널 체인 구문을 사용하면 그에 못지 않게 간결한 구문으로 필요한 코드를 작성할 수 있습니다. 옵셔널 체인은 여러분이 실제로 앱을 만들어나가는 과정에서 계속해서 사용하게 될 중요한 문법입니다.