스위프트 옵셔널 이해하기

옵셔널 개요

Swift에서 변수를 선언할때는 기본적으로 옵셔널이 아닌(non-optional)값을 지정해야 한다. 다시말해, 변수에는 반드시 nil 이 아닌(non-nil) 값을 할당 해야만 한다. 만일, 옵셔널이 아닌 변수에 nil 을 설정하려 하면 컴파일러는 nil값을 할당 할 수 없다고 오류를 발생시킬 것이다.

1
2
var message: String = "Swift is awesome!" // OK
message = nil // compile-time error

클래스에서 속성을 선언할 때에도 마챦가지 이다.

1
2
3
4
class Messenger {
var message1: String = "Swift is awesome!" // OK
var message2: String // compile-time error
}

하지만, Objective-C 에서는 아래와 같이 nil을 설정해도 에러가 발생하지 않는다.

1
2
NSString *message = @"Objective-C will never die!";
message = nil;
1
2
3
4
class Messenger {
NSString *message1 = @"Objective will never die!";
NSString *message2;
}

하지만, 이것은 스위프트에서 초기값 없이 속성 또는 변수를 선언할수 없다는 것을 의미하지 않는다. 스위프트는 옵셔널 타입을 사용해 값의 부재를 알릴 수 있다. 즉, “?” 오퍼레이터를 타입선언 뒤에 추가하면 된다. 아래 예를 참고하라.

1
2
3
4
class Messenger {
var message1: String = "Swift is awesome!" // OK
var message2: String? // OK
}

왜 옵셔널(Optionals) 인가?

스위프트는 애플의 주장대로 타입-세이프한 언어라는것을 보여주는 사례중 하나이다. 앞서본 예제와 같이 옵셔널은 런타임시에 발생할 수 있는 에러들을 컴파일시에 체크할 수 있도록 한다. 옵셔널의 장점을 더 잘 이해할 수 있도록 아래의 Objective-C 메소드를 살펴 보자.

1
2
3
4
5
6
7
8
9
- (NSString *)findStockCode:(NSString *)company {
if ([company isEqualToString:@"Apple"]) {
return @"AAPL";
} else if ([company isEqualToString:@"Google"]) {
return @"GOOG";
}
return nil;
}

findStockCode 메소드는 Apple과 Google의 주식코드를 리턴하고, 그 외의 경우에는 nil을 리턴한다. 이 메소드는 아래와 같이 호출될 수 있다.

1
2
3
4
NSString *stockCode = [self findStockCode:@"Facebook"]; // nil is returned
NSString *text = @"Stock Code - ";
NSString *message = [text stringByAppendingString:stockCode]; // runtime error
NSLog(@"%@", message);

이 코드는 문제없이 컴파일이 되겠지만 실행했을때는 런타임 오류를 발생하게 된다. 이 코드를 스위프트로 작성하면 아래와 같이 되며, 컴파일시에 오류를 발견할 수 있게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func findStockCode(company: String) -> String? {
if (company == "Apple") {
return "AAPL"
} else if (company == "Google") {
return "GOOG"
}
return nil
}
var stockCode:String? = findStockCode("Facebook")
let text = "Stock Code - "
let message = text + stockCode // compile-time error
println(message)

stockCode는 문자열 또는 nil값일 수 있다. 옵셔널 문자열인 String?은 업랩(Unwrapped)되지 않았으므로, 이것을 수정할 수 있도록 에러를 발생한다. 이렇게 컴파일시에 nil 체크를 수행함으로써 잠재적인 에러를 탐지할 수 있다는것이 옵셔널의 장점이다.

null pointer exception

옵셔널 꺼내기(Unwrapping Optionals)

위의 코드가 컴파일 될 수 있도록 하려면 아래와 같이 수정하면 된다.

1
2
3
4
5
6
var stockCode:String? = findStockCode("Facebook")
let text = "Stock Code - "
if stockCode {
let message = text + stockCode!
println(message)
}

스위프트에서 느낌표는 강제 언래핑(Unwrapping)을 할때 사용된다. if문으로 stockCode에 값이 있다는것을 확인했으므로, 위와 같이 옵셔널 변수에 !를 붙여 값을 꺼낼 수 있다.

하지만, 아래와 같이 stockCode에 대해 nil-체크를 하지 않고 Unwrapping을 하게되면 런타임 오류를 만나게 될것이다.

1
2
3
var stockCode:String? = findStockCode("Facebook")
let text = "Stock Code - "
let message = text + stockCode! // runtime error

옵셔널 바인딩

강제 언래핑(Forced Unwrapping)은 다른 언어에서 null-체크를 하는것과 그리 다르지 않다. 또한, 잠재적으로 런타임 오류를 발생시킬 수 있다. 옵셔널 바인딩은 옵셔널이 값을 가지고 있는지 아닌지를 체크하고, 값이 있을 경우 그것을 꺼내어 임시로 특정 상수(또는 변수)에 값을 보관하는 방법을 말한다.

예제보다 이것을 더 쉽게 설명할 수 없으므로, 바로 앞에 사용했던 코드를 옵셔널 바인딩을 적용하여 변경해 보겠다.

1
2
3
4
5
6
var stockCode:String? = findStockCode("Facebook")
let text = "Stock Code - "
if let tempStockCode = stockCode {
let message = text + tempStockCode
println(message)
}

“if let”(또는 if var 도 사용 가능) 구문은 두가지 의미를 내포하고 있다. 쉽게말해, “만일 stockCode가 값을 가지고 있다면, 이것을 꺼내서 (1)tempStockCode에 값을 대입하고, (2) 조건문 블록을 실행하라” 는 것을 의미 한다. 그렇지 않다면 조건문은 실행되지 않는다.
tempStockCode는 새로운 상수이므로 끝에 느낌표(!)를 사용할 필요가 없다.

위 코드는 아래와 같이 좀 더 축약 하여 코딩할 수 있다.

1
2
3
4
5
let text = "Stock Code - "
if var stockCode = findStockCode("Apple") {
let message = text + stockCode
println(message)
}

이 코드에서 stockCode 는 옵셔널이 아니란점에 주목하기 바란다.

옵셔널 체이닝

옵셔널 체이닝을 설명하기 전에 이전의 예제 소스를 아래와 같이 수정하겠다. 우리는 code와 price라는 옵셔널 속성을 가진 Stock 이라는 클래스를 새로 만들고, findStockCode 함수는 문자열 대신 Stock클래스를 리턴 하도록 수정할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Stock {
var code: String?
var price: Double?
}
func findStockCode(company: String) -> Stock? {
if (company == "Apple") {
let aapl: Stock = Stock()
aapl.code = "AAPL"
aapl.price = 90.32
return aapl
} else if (company == "Google") {
let goog: Stock = Stock()
goog.code = "GOOG"
goog.price = 556.36
return goog
}
return nil
}

아래 코드는 주식코드를 찾고, 100주를 사는데 필요한 금액을 출력한다.

1
2
3
4
5
6
if let stock = findStockCode("Apple") {
if let sharePrice = stock.price {
let totalCost = sharePrice * 100
println(totalCost)
}
}

findStockCode()의 반환값은 옵셔널이므로, 옵셔널 바인등을 사용해 nul-체크를 하고 stock의 price 역시 옵셔널 이므로 다시 한번 if let~ 문을 사용하였다.

위 코드는 에러를 발생시키지 않지만, 중첩된 “if let” 문을 사용하는 대신 옵셔널 체이닝을 사용해 더 간단히 할 수 있다. 옵셔널 체인은 “?.” 오퍼레이터를 사용해 다수의 옵셔널을 체인으로 묶을 수 있다. 아래 코드는 옵셔널 체인을 사용해 수정한 코드이다.

1
2
3
4
if let sharePrice = findStockCode("Apple")?.price {
let totalCost = sharePrice * 100
println(totalCost)
}

이제 코드가 더욱 간결해 졌다. 옵셔널 체인에 관한 더 다양한 예제는 애플의 스위프트 가이드 문서에서 찾을 수 있다.

스위프트와 오브젝티브-C의 상호 운용성

스위프트는 Objective-C의 API들과 잘 상호 작용하도록 설계되었다. UIKit이나 다른 프레임워크의 API들과 상호작용이 필요한 부분에서도 쉽게 옵셔널들을 만날 수 있다. 다음 코드는 테이블뷰를 구현할때 마주칠수 있는 옵셔널의 예이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func numberOfSectionsInTableView(tableView: UITableView?) -> Int {
// Return the number of sections.
return 1
}
func tableView(tableView: UITableView?, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section.
return recipes.count
}
func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel.text = recipes[indexPath.row]
return cell
}

ps. 원문중 번역이 생략되거나 축약한 부분도 있습니다.
더 자세한 부분은 아래의 원문과 애플의 스위프트 공식 문서를 참고하시기 바랍니다.

원문 : A Beginner’s Guide to Optionals in Swift