Swift

Hits

옵셔널(Optional)은 스위프트에서 도입된 새로운 개념으로서 언어 차원에서 프로그램의 안정성을 높이기 위해 사용하는 개념이다. 옵셔널의 개념을 한 문장으로 정의하자면 ‘nil을 사용할 수 있는 타입과 사용할 수 없는 타입을 구분하고, 사용할 수 있는 타입을 가리켜 옵셔널 타입(Optional Type)이라고 부른다’라고 할 수 있다.


여기서 말하는 nil이란, 값이 없음을 의미하는 특수한 값이다. 정수형의 0이나, 문자열의 ““과는 다른, 말 그대로 순수하게 하무 값도 없다는 것을 의미한다. 이런 특수성 때문에 nil은 종종 실제 값으로는 처리할 수 없는, 무엇인가 문제가 발생했을 때 이를 의미하기 위해 사용된다.

앞에서 잠깐 다룬 딕셔너리를 떠올려보자. 저장된 값을 읽으려면 딕셔너리 변수명 뒤에 대괄호와 함께 키 값을 넣어주어야 하는데, 종종 잘못된 키를 대입할 때가 있다. 오타이거나, 혹은 이미 삭제된 키를 사용하는 경우가 대표적이다. 이같은 상황에서 스위프트는 우리에게 뭔가 ‘너 지금 잘못했어. 그 키는 지금의 딕셔너리에는 존재하지 않아’라고 알려주고 싶어한다.

하지만 오류를 발생시키는 것은 그리 좋지 않다. 겨우 딕셔너리 키에 대한 잘못된 참조 하나 때문에 실행을 중단시키는 것은 매정할 뿐만 아니라, 스위프트는 최대한 안정성을 높이고자 노력하는 언어이다. 하지만 결과값을 공백을 반환할 수는 없다. 그랬다면 사용자가 자신이 실수한 것을 알지 못하고 정말 ‘그 키에는 공백이 저장되어 있었나보군’이라고 착각하게 될지도 모르기 때문이다. 이런 상황에서 스위프트는 오류도 발생시키지 않고 뭔가 문제가 있었다는 것도 알려주기 위해 nil이라는 값을 반환하는 것이다.

1
2
let capital = ["KR": "Seoul", "CN": "Beijing", "JP": "Tokyo"]
capital["ko"] // nil

이처럼 스위프트에서는 값을 처리하는 과정에 문제가 있을 경우 많은 부분에서 오류를 발생시키는 대신 결과값을 nil로 반환한다. 하지만 모든 타입이 nil을 반환할 수 있는 것은 아니며 오직 옵셔널 타입만 nil을 반환할 수 있다. 다시 말해, nil을 반환하려면 해당 값이 옵셔널 타입으로 정의되어 있어야 한다.

여기에서 중요한 것은 “오류가 발생할 가능성”이다. 경우에 따라서는 오류가 발생하지 않을 수도 있지만, 언젠가 오류가 발생할 수 있는 가능성이 아주 조금이라도 있다면 모두 옵셔널 타입으로 정의해야 한다. 이해를 돕기 위해 또다른 예를 살펴보자. 문자열 “123”을 숫자로 변환해야 한다고 가정해 보겠다.

일반적으로 사람의 인식에서는 “그저 큰따옴표만 떼면 된다”라고 생각할지도 모르겠지만, 프로그래밍에서 문자열 “123”을 숫자로 바꾸려면 다소 복잡한 변환 과정을 거쳐야 한다. 다행히 스위프트를 포함한 많은 프로그래밍 언어에서는 문자열을 숫자로 변환해주는 간편한 방법을 제공한다. 스위프트에서는 Int 객체의 생성자 옵션에 숫자로 변환할 문자열을 넣어주면 정수로 변환된다. 사용 형식은 다음과 같다.

1
Int(바꿀 문자열)

예를 들어, 문자열 “123”을 숫자로 바꾸려면 다음과 같이 작성하면 된다. 상수 num은 문자열 “123”이 아니라 정수 123이 대입된 Int 타입의 상수로 정의된다.

1
let num = Int("123")

다른 값을 넣어도 마찬가지이다. 문자열을 정수로 변환한 값을 얻을 수 있따. 그런데 만약 “123”이나 “456”처럼 숫자로 바꿀 수 있는 문자열 대신, 아래와 같이 문자열을 인자값으로 넣으면 어떻게 될까?

1
let num = Int("Swift")

무척 난감한 상황이다. 일반 문자열은 숫자로 변환할 수 없기 때문이다. 따라서 컴파일러는 이럴 때 “죄송합니다. 이 문자열은 숫자로 변환할 수 없습니다”라고 난감함을 표현해야 한다. 개발자가 상황을 이해하고 적절히 대응할 수 있는 구문을 보완할 수 있도록 말이다.

대부분 프로그래밍 언어에서는 이런 상황을 오류로 처리하거나 혹은 예외사항으로 처리한다. 오류 처리 구문을 사용할 수 있는 대표적인 언어인 자바는 이럴 때 NumberFormatException() 예외를 발생함으로써 개발자가 인지하고 이에 대응하는 코드를 작성할 수 있도록 돕는다. 비단 자바뿐만 아니라 PHP도 오류 코드를 발생시키고, 자바스크립트 역시 NaN을 반환하여 변환에 실패했음을 알려준다.

그러나 스위프트는 조금 다르다. 언어의 안정성을 위해 가급적 오류를 발생시키지 않으려고 노력한다. 오류가 발생하면 프로그램의 실행 흐름이 중단되고 경우에 따라 앱의 동작이 멈추거나 아예 꺼져버릴 수도 있으므로, 언어의 안정성을 위해서는 될 수 있으면 피해야 하는 상황일 수밖에 없다. 이런 맥락에서 스위프트는 문자열의 정수 변환이 실패하더라도 실행을 중지시키거나 오류를 발생시키지 않고 억지로 값을 반환하려고 노력한다. 반환하는 값이 그 무엇이 도든 간에 말이다.

하지만 아무 값이나 반환할 수는 없다. 특히 0을 반환해서는 안 된다. 누군가 Int(“0”)을 호출했을 때 정상적인 처리 결과로 정수 0이 반환된 경우와 구분할 수 없기 때문이다. 또한 공백을 사용해서도 안 된다. 공백은 일반적으로 문자열로 처리되는 경향이 있어, 반환 타입이 일치하지 않을 뿐만 아니라 공백 또한 실패를 의미하는 값은 아니기 때문이다. 이런 상황에서 개발자들은 실패를 의미하면서도 오류를 던지는 것이 아닌 값이 필요했다. 이런 목적에서 정의된 값이 바로 “값이 없음”을 뜻하는 nil이다.


다른 언어에서 Null이나 null로 표현되기도 하는 nil은 값이 없다는 것을 표현하기 위해 사용하는 일종의 특수 값이다. 이 값은 원래 오브젝티브-C에서 쓰이던 값이었는데, 스위프트로 넘어오면서 의미가 약간 달라졌다. 오브젝티브-C에서는 빈 메모리 주소를 가리키는 값이었지만 스위프트에서는 단순히 ‘값이 없음’을 의미하게 된 것이다. 스위프트에서는 뭔가 연산 과정에서 정상적으로 값을 처리하지 못하는 상황이 발생했을 때 제대로 된 결과값 대신 nil을 반환한다. 앞에서 예로 든 문자열을 숫자로 변환하는 과정에서도 잘못된 인자값으로 인해 문자열의 정수 반환이 실패했을 때, ‘인자값이 잘못되었으므로 이 변환 처리는 실패입니다. 따라서 아무 값도 반환되지 않습니다’라는 의미를 표현하기 위해 nil을 반환한다.

1
Int("Swift") ==> nil

그런데 스위프트에서는 nil의 사용에 제약을 걸어두었다. 바로 일반 자료형은 nil 값을 가질 수 없다는 것이다. 문자열이나 정수 등은 일반 자료형이기 때문에 ‘값이 없음’을 뜻하는 nil 값을 저장할 수 없다. 만약 일반 자료형에 억지로 nil 값을 대입하려고 하면 다음과 같은 오류가 발생한다.

일반 자료형에 nil을 대입하려고 했을 때의 오류

함수에서 값을 반환할 때에도 마찬가지이다. 함수는 반환 타입이 정해져 있기 때문에 항상 그 타입에 맞는 값을 반환해야 하는데, 처리 과정이 실패했을 경우에는 nil을 반환하게 된다. 하지만 일반 자료형에는 nil 값을 할당할 수 없다는 스위프트의 특성 때문에 nil을 반환하면 오류가 발생한다.

이때 사용하는 타입이 바로 옵셔널 타입이다. 옵셔널 타입으로 선언된 자료형은 nil 값을 저장할 수 있다. 만약 nil 값을 저장해야 하거나 혹은 함수의 반환값에 nil이 포함될 가능성이 있다면, 다시 말해 오류가 발생할 가능성이 있다면 반환 타입을 반드시 옵셔널 타입으로 설정해야 한다. 따라서 Int(<바꿀문자열>) 구문의 반환 타입은 옵셔널 타입으로 정의된다.

사실 옵셔널 타입은 별도로 존재하는 자료형이 아니다. 스위프트에서 모든 기본 자료형들은 그에 대응하는 옵셔널 타입이 존재한다. 다시 말해 정수형에 대응하는 옵셔널 타입과, 문자열에 대응하는 옵셔널 타입이 모두 있다는 뜻이다. 또한, 클래스나 구조체를 이용하여 만든 객체도 옵셔널 타입으로 바꿀 수 있다. 함수를 통해서 반환 가능한 모든 타입들 역시 옵셔널 타입으로 변경할 수 있다.

어떤 자료형을 사용하는지에 따라 대응하는 옵셔널 타입은 다르다. 정수 타입을 옵셔널 타입으로 변경하면 Optional Int 타입이 되고, 문자열을 옵셔널 타입으로 바꾸면 Optional String이 된다. Int(<바꿀문자열>) 구문의 반환타입은 Optional Int이다.

그런데 스위프트에서 옵셔널 타입이 실제로 가질 수 있는 값의 종류는 오직 두 가지 뿐이다. 하나는 nil이 아닌 값, 또다른 하나는 nil 값이다. nil이 아닌 값은 실제 실행 결과에서 오류가 발생하지 않았을 때 반환되며, 실제 실행 과정에서 오류가 발생했을 때에는 nil이 반환된다.

헷갈릴지도 모르겠지만 여기에서 말하는 ‘nil이 아닌 값’은 “ABC” 또는 123 등의 구체적인 값이 아니라 정말 nil이 아닌 값 그 자체이다. 이것은 반환하려는 실제 값이 옵셔널이라는 객체로 둘러 싸인 상태를 의미한다. 다시 말해 처리 과정에 문제가 있었다면 nil이 반환되고, 문제가 없어 처리가 성공했다면 옵셔널 객체로 감싸진 결과값이 반환된다.

옵셔널 타입이 가질 수 있는 값의 경우

결국, 옵셔널 타입이란 반환하고자 하는 값을 옵셔널 객체로 다시 한 번 감싼 형태를 의미한다. 하지만 성공했을 때에는 일반 값을 반환하고, 그렇지 않으면 특수하게 처리된 예외나 NaN 값을 반환하는 방식으로 나누어 반환하는 다른 프로그래밍의 반환 방식과는 다르다. 스위프트에서는 일단 오류가 발생할 가능성이 있기만 하면, 성공적으로 처리했더라도 일단 옵셔널 타입으로 감싸서 반환하기 때문이다.

문자열 “123”을 숫자로 변환한 값을 반환하고자 한다면 실제로 변환된 값 123을 직접 반환하는 것이 아니라 옵셔널 타입으로 값을 감싼 Optional(123)을 반환한다. 만약 숫자로 변환하지 못할 문자열(예를 들어, “안녕하세요”와 같은)이 입력되어 정상적인 변환이 불가능한 상황이라면 Optional(“안녕하세요”)가 아니라 nil 값을 반환한다. 이때 Optional(123)과 nil은 모두 옵셔널 타입이다.

문자열의 정수 변환 처리 결과

처리가 성공적일 경우, 옵셔널 타입으로 반환된 값을 열어보면 실제 값이 옵셔널 타입으로 둘러싸여 있는 것을 볼 수 있다. 이를 옵셔널 래핑(Optional Wrapping)이라고 한다. 이렇게 받은 값은 옵셔널 언래핑(Optional Unwrapping)이라고 불리는 특수한 처리 과정을 통해 옵셔널 타입을 해제하고 실제 값을 추출하여 사용해야 한다. 단, 처리 결과가 실패여서 옵셔널 타입의 값이 nil이라면 옵셔털 타입을 해제해서는 안 된다.

문자열의 정수 변환 처리 결과

질문 : 차라리 모든 값을 옵셔널 타입으로 선언하고 사용하면 안 되나요? 일반 타입과 옵셔널 타입으로 나누어 사용하는 건 헷갈리고 불편할 것 같아서요.

모든 값을 옵셔널 타입으로 선언하는 것은, 일반 자료형에 nil 값을 대입할 수 잇도록 허용하는 것과 다를 바가 없다. 그런데 모든 값이 nil을 가질 수 있다는 것은 또 다른 문제를 발생시킨다. 값을 사용할 때마다 일일이 nil인지 아닌지를 체크하여 사용해야 한다. 이는 프로그래밍 로직을 복잡하게 만들 뿐만 아니라 처리 과정 또한 어렵게 만든다. 그러므로 꼭 필요한 경우에만 제한적으로 옵셔널 타입을 적용하는 것이 좋다.

1. 옵셔널 타입 선언과 정의

일반 자료형을 옵셔널 타입으로 만드는 방법은 단순하다. 우리가 사용하는 자료형 뒤에 퀘스천 마크(물음표)만 붙이면 된다. 'String?' 은 Optional String 타입을 의미하고, 'Int?'는 Optional Int 타입을 의미한다. String 타입에는 nil을 대입할 수 없지만 String? 타입에는 nil을 대입할 수 있다. 다음 예제들은 다양한 옵셔널 타입을 보여주고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 옵셔널 Int 타입
var optInt: Int?

// 옵셔널 String 타입
var optString: String?

// 옵셔널 Double 타입
var optDouble: Int?

// 옵셔널 Array 타입
var optArr: [String]?

// 옵셔널 Dictionary 타입
var optDic: Dictionary<String, String>?
var optDic2: [String:String]?

// 옵셔널 Class 타입
var optClass: AnyObject?

일반 자료형을 선언만 하고 초기화하지 않으면 아예 아무것도 할당되지 않지만, 옵셔널 타입으로 자료형을 선언하면 자동으로 nil로 초기화된다. 물론, 옵셔널 내부에 있는 자료형에 nil 값이 부여된다는 것은 아니다. 옵셔널 타입 자체에 nil이 부여된다는 것이다.

옵셔널 타입으로 선언된 변수나 상수에 실제 값을 할당하는 방법은 일반 타입의 그것과 동일하다. 다음은 옵셔널 타입의 변수와 상수에 값을 할당하는 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 옵셔널 Int 타입
var optInt: Int?
optInt = 3

// 옵셔널 String 타입
var optString: String?
optString = "Swift"

// 옵셔널 Array 타입
var optArr: [String]?
optArr = ["C", "JAVA", "Objective-C", "SmallTalk"]

// 옵셔널 Dictionary 타입
var optDic: [String:String]?
optDic = ["국어": 94, "수학": 88, "영어": 96]

옵셔널 타입의 변수에 값을 할당할 때에는 옵셔널 타입임을 인지할 필요가 거의 없다. 일반 변수처럼 값을 할당하면 옵셔널 객체 내부에 값이 대입되기 때문이다. 즉, 값을 대입할 때에는 옵셔널이 아닌 일반 변수처럼 생각하고 다루어도 무방하다.



이전 내용 보기 iOS 면접 질문 목록 보기 다음 내용 보기

Leave a comment