2014년 9월 2일 화요일

Swift - 옵셔널(Optional) 엑세스

스위프트(Swift)의 옵셔널(Optional)은 '값이 없음(No Value)' 이라는 것을 심어주기 위한 기능이다. 단순하게 사용하려 한다면 그저 nil 초기화 여부를 조사하기 위한 용도로써 쓸 수 있겠지만, 액세스라던가 옵셔널 체인(Optional Chain) 등등을 아무런 지식 없이 쓰다보면 컴파일 에러나 런타임 에러를 종종 보게 될 것 같다. 그래서 약간의 시험과 더불어 개념을 조금 더 파고 들어가 보고자 한다.

기본 지식으로 Optionals와 Implicit Unwrapped Optionals 는 알고 있다고 가정한다. (옵셔널(Optionals) 글을 참고하자)

우선 옵셔널 정수형 변수를 하나 만들었다고 치자.
let someValue: Int? = nil
이 변수의 값을 액세스 하는건 간단하다.
let otherValue1 = someValue     // Optional nil
let otherValue2 = someValue?    // Optional nil
위 두 코드는 otherValue1, otherValue2 이라는 변수를 만들어서 someValue의 값을 담으려는 의도의 코드이다. 물론 의도한 대로 동작하는 것 처럼 보이고 별 문제는 없을 것이다.

정확히 말하자면 위 otherValue1과 otherValue2는 타입을 명시하지 않았기 때문에 someValue의 타입을 그대로 받는다. 즉 옵셔널 Int 타입을 가지게 되면서 동시에 nil(즉 값이 없음)을 가지게 된다.

물론 nil이 아니라도 옵셔널 타입을 가지는 건 동일하다.
let someValue2: Int? = 100
let otherValue3 = someValue2?   // Optional Int(100)
위 경우 otherValue3는 Some(100) 이라는 'Optional 타입을 가지는 Int 형식의 100이라는 데이터를 가지는 변수'가 된다.

하지만 타입을 명시한 경우에는 좀 다르다. 계속 이어서 컴파일 에러가 발생하는 예제를 보자.
let errorCase: Int = someValue     // Compile Error
여기서 컴파일 에러가 발생하는 이유는 '옵셔널은 일반 변수와는 타입이 다르기 때문'이다. 따라서 위 코드의 경우는 타입을 옵셔널로 선언해주면 컴파일 에러가 없어진다.
let validCase1: Int? = someValue
let validCase2: Int? = someValue2
이렇게 하면 역시 validCase1은 의도대로 옵셔널 nil이 되고, validCase2 는 옵셔널 100이 된다.

하지만 옵셔널 타입은 옵셔널 타입이다. nil 체크를 할 수 있을 뿐이지 다른 용도로 쓸 수 없다. 이를 위해서 강제로 벗기기(Forced Unwrap) 이라는게 있다.

기본 옵셔널의 '?'와는 다르게 Forced Unwrap 즉 강제로 벗기기의 경우에는 값을 그대로 돌려준다는 차이가 있다.
let someValue2: Int? = 10
let otherValue3: Int = someValue2!      // Int(10)
otherValue3 는 someValue2를 엑세스 했지만 이번에는 옵셔널이 아닌 그냥 Int 타입이 된다. 왜냐하면 옵셔널 타입 변수에서 옵셔널을 강제로 벗겼기(?) 때문이다. 이렇게 옵셔널 타입 변수 뒤에 느낌표(!)를 붙이면 Forced Unwrapping이 발생하여 옵셔널을 벗거버리고 원래의 데이터에 엑세스가 가능해진다.

얼핏 보면 강제로 벗기기 방식만 쓰면 될 것 같다는 생각이 들 수도 있다. 하지만 이 둘은 애초에 용도가 다르다는 것을 기억하자. 계속해서 아래 예제는 여전히 someValue가 nil인 상태에서의 예제이다.
// Case of Runtime Error
let errorCase2 = someValue!
위 경우도 앞서 본 대로 느낌표(!)를 이용해 옵셔널 변수에 참조를 시도했다. 하지만 실행시켜 보면 분명 프로그램이 죽을 것이다. (플레이그라운드라면 Bad Access 류의 오류가 발생한다)

이렇게 느낌표를 이용할 경우는 컴파일러가 직접 간섭하지는 않기 때문에 빌드 오류가 없다. 대신, 실행 시 nil 이라는 값을 엑세스 하려하니 당연히 Bad Access 같은 오류가 발생하게 된다. 이는 마치 포인터 변수의 메모리를 참조할 때와 비슷하다. NULL Pointer 를 엑세스 하려고 하면 100% 죽는다는 것을 생각해보자.

따라서 옵셔널 변수의 실제 데이터를 엑세스 할 때는 가급적이면 미리 nil 체크를 확인한 뒤에 느낌표(!)를 붙여서 값을 가져오는게 안전하다는 이야기가 된다.

옵셔널 체인(Optional Chaining)

'옵셔널 체인'은 옵셔널 타입의 오브젝트에 메소드나 프로퍼티 혹은 서브스크립트(subscript)를 사용하려 할 때 좀 더 간단하게 사용 할 수 있는 방법을 제공한다. 예를 들자면 아래와 같은 식이겠다.
let someValue: Int? = nil
let someMaxValue = someValue?.toIntMax()       // Optional nil

let someValue2: Int? = 10
let someMaxValue2 = someValue2?.toIntMax()     // Optional Int(10)
별 쓸모 없는 기능의 코드이지만 옵셔널 체인을 위한 예제이니 그러려니 하자.

어쨌거나 위 예제에서 someValue?.toIntMax() 의 경우는 someValue의 값이 존재하면(non-nil) toIntMax() 메소드를 이용해 값을 얻어 올 수 있고, 만약 값이 없으면(nil) 역시 옵셔널 형식의 nil을 받아오게 된다.

옵셔널 체인도 역시 위에서 봤던 것 처럼 일반 옵셔널이냐 혹은 강제로 벗기기냐에 따라 동작이 달라진다. 우선 예제를 위해 아래 클래스를 먼저 보자.
class Person {
    var name = "Noname"
    var age = 0
}

class PersonRecord {
    var person: Person?
}
두 가지 클래스 Person과 PersonRecord 를 선언했다. 여기서 PersonRecord는 Person 형식의 옵셔널 프로퍼티를 가지고 있다.

위 클래스를 이용해 단순하게 엑세스 테스트를 해 보자. 당연하겠지만, 위에서 봤던 것과 동일하게 옵셔널 타입의 멤버를 그냥 엑세스 하려하면 오류가 발생 할 것이다.
let record = PersonRecord()

let name = record.person.name
// Compile Error: 'Person?' does not have a member named 'name'
예상대로 에러가 발생했다. 하지만 에러 내용이 아예 '해당 멤버가 없다'는 식으로 보여준다. 이 에러 내용은 스위프트 스펙이 버전업 되면 바뀔 수도 있으니 참고하자.

이제 정상적인 참조를 해 보자. 앞서 본 예제와 같이 옵셔널 체인을 이용해 PersonRecord 내부의 Person형 프로퍼티의 프로퍼티를 참조하는 예제다.
let record = PersonRecord()
let name = record.person?.name  // Optional nil
만약 person이 생성되었다면 해당 person의 name 프로퍼티가 돌아올 것이다. 하지만 위의 경우 별도의 person 프로퍼티에 오브젝트를 생성하지 않았기 때문에 person은 nil이다. 그래서 위 예제의 경우는 최종적으로 nil이라는 옵셔널 타입의 값을 받아오게 된다.

옵셔널 체인도 데이터를 가져오기 위해 강제로 벗기면[...] 앞서 봤던 대로 런타임 에러(Runtime Error)를 구경 할 수도 있다.
let record1 = PersonRecord()
record1.person = Person()
let name1 = record1.person!.name     // String("Noname")

let record2 = PersonRecord()
let name2 = record2.person!.name     // Runtime Error
처음 record1 에는 person에 오브젝트를 할당하고 느낌표(!)를 이용해 액세스를 했다. 그리고 그 결과로 초기값인 "Noname" 이라는 문자열(옵셔널이 아님)을 얻게 된다.

하지만 record2에는 person은 nil 상태이다. 따라서 name2를 가져오려고 하는 타이밍(즉 런타임)에 Bad Access 오류가 발생하게 된다. 즉 앱이 뒈진다. -_-;

결론

옵셔널을 엑세스 할 때는 옵셔널 타입을 선언할 때와 비슷하게 '?' 와 '!' 키워드를 이용해 할 수 있다는 것을 알게 되었다. 그리고 이 둘의 차이에 대해서도 옵셔널하게 엑세스하느냐 아니면 실제 데이터에 엑세스하느냐로 확연하게 구분할 수 있게 되었다.

옵셔널 값을 안전하게 엑세스하려면 아래와 같이 if 문으로 nil 체크를 하면 된다.
if someOptionalValue != nil {
    let safeValue: SomeType = someOptionalValue!
    doSomething(safeValue)
}
물론 아래 코드와 같이 스위프트에서 제공하는 좀 더 간단한 방법을 이용 할 수도 있다.
if let safeValue = someOptionalValue {
    doSomething(safeValue)
}
위 두 예제는 똑같은 의미의 코드이다. 뭐 다들 알 만한 이야기였다.

잡담

나만 그렇게 생각 할 지도 모르겠지만, 옵셔널 개념은 굉장히 귀찮다. 왜 옵셔널 선언이 별도로 있고 Implicit 뭐시기 옵셔널이라는 무식한 이름의 개념은 왜 있고 왜 옵셔널 참조 할 때도 '?'나 '!'를 붙여야 하는지 말이다.

현대적인 언어 입장에서 볼 때 옵셔널은 필요없다. 모든 변수를 아예 모두 옵셔널 타입으로 만들어 버리면 되니까. 그러면 엑세스 할 때 일일이 '?' 나 '!'를 붙일 필요도 없다. '!'가 아예 사라져도 될 듯 하다.

물론 스위프트의 옵셔널은 '퍼포먼스를 중시함과 동시에 사고를 미연에 방지하기 위함' 이라는 의도를 가지고 있다고 본다. 뭐 의도는 좋다고 치자. 그래도 내가 Python 도 종종 쓰다보니 이런 생각을 하는 지는 모르겠지만 '귀찮아 죽겠네' 라는 감탄사는 계속 튀어나온다.

그리고 Implicit Unwrapped Optionals는 세상에서 사라졌으면 좋겠다는 마음 뿐이다. -_-;;

[관련글] Swift - 옵셔널(Optionals)
[관련글] 스위프트(Swift) 가이드

댓글 4개 :

익명 :

감사합니다. 이해하기 쉽게 써주셨네요. 저는 그냥 흥미로 살펴보고 있는 언어인데 optional은 정말 귀찮아보이네요. ^^;

Steve :

출판된 어떤 책보다 뛰어난 글 내용입니다. 다른 책을 여러번 읽어도 이해가 안가던데, 이 글보고 이해하고 갑니다. 좋은글 감사합니다.
화이팅!

김덕후 :

잘 보고 갑니다.
덕분에 좀 더 명확하게 이해를 하고 쓸 수 있게 되었어요.

애플은 왜 이런 nil에 대한 부분을 개발자한테 떠넘겼는지 모르겠네요.
ARC가 도임되기 전에 예전의 메모리 관리야 퍼포먼스 관리를 위해서 그랬다고 치겠지만...

이건 그런것 같지도 않고.. 그냥 타인의 소스를 봤을 때 코드상의 의도를 조금 더 분명히 하는 의도정도로 밖에 안보이는데 말이죠.

SeungChul Han :

Objective C에 익숙한상태에서 처음에 우습게 Swift를 배워야겠다 생각했는데 옵셔널에서 막혀서 당황했었네요ㅎㅎ

작성자님의 설명에 이해하고 감탄하고 갑니다. 감사합니다~