옵셔널은 안전성을 높이기 위해 사용되는 개념이다. 즉 nil을 사용할 수 있는 타입과 사용할 수 없는 타입으로 구분하고, 사용할 수 있는 타입을 가리켜 옵셔널 타입이라고 한다.
여기서 말하는 nil은 값이 없음을 의미하는 특수한 값이다. 정수의 0이나 문자열의 ""과는 다른 말 그대로 순수하게 아무 값도 없다는 것을 의미한다. nil은 실제 값으로는 처리할 수 없는, 무엇인가 문제가 발생했을 때 이를 의미하기 위해 사용된다.
딕셔너리를 예로 들자면 잘못된 키를 입력했을 때 오류가 발생된다면 프로그램이 안정적이지 못할 것이다. 하지만 결과값으로 공백을 반환한다면 사용자가 그 키에는 공백이 저장되어 있다고 착각을 할 수 있기 때문에 좋은 방법이 아니다. 이 때 스위프트는 뭔가 문제가 있다는 사실을 알려주기 위해서 nil을 사용한다.
let capital = ["KR" : "Seoul", "EN" : "London", "JP" : "Tokyo"]
capital["ko"] = nil
이처럼 값을 처리하는 과정에서 문제가 있을 경우 오류 대신 nil을 반환한다. 하지만 모든 타입이 nil을 반환할 수 있는 것은 아니며 오직 옵셔널 타입만 nil을 반환할 수 있다. 즉 nil을 반환하려면 해당 값이 옵셔널 타입으로 정의되어 있어야 한다.
여기서 중요한 것은 "오류가 발생할 가능성"이다. 오류가 발생할 가능성이 조금이라도 있다면 모두 옵셔널 타입으로 정의해야 한다. 만약 문자열을 정수형으로 바꾸려 한다면
Int(바꿀 문자열)
let num = Int("123")
위의 경우에는 num에 123이 대입될 것이다.
let num = Int("swift")
하지만 지금 작성한 코드는 오류가 발생할 것이다.
대부분 다른 프로그래밍 언어에서는 이 상황을 오류나 예외사항으로 처리한다. 그러나 스위프트는 언어의 안정성을 위해 가급적 오류를 발생시키지 않으려고 노력한다. 오류가 발생하면 실행 흐름이 중단되고 경우에 따라 앱의 동작이 멈추거나 꺼질 수 있기 때문이다. 따라서 이런 실패 케이스에도 억지로 값을 반환하려고 한다.
하지만 아무값이나 반환할 수 없다. 특히 0을 반환해서는 안 될 것이다. 이런 케이스에 정의된 값이 바로 "값이 없음"을 나타내는 nil이다.
그런데 스위프트에서는 nil의 사용에 제약이 있다. 바로 일반 자료형은 nil을 가질 수 없다는 것이다. 문자열이나 정수등은 일반 자료형이기 때문에 "값이 없음"을 의미하는 nil을 저장할 수 없다.
함수에서도 마찬가지이다. 함수는 반환 타입이 정해져 있기 때문에 항상 그 타입에 맞는 값을 반환해야 하는데, 처리 과정이 실패했을 경우 nil을 반환한다. 하지만 일반 자료형에는 nil값을 할당할 수 없다는 특성 때문에 nil을 반환하면 오류가 발생한다.
이 때 사용되는 타입이 옵셔널 타입이다. 옵셔널 타입이 가질 수 있는 값은 오직 두 가지 뿐이다. nil이 아닌 값, nil값이다. 오류가 발생했다면 nil이, 오류가 발생하지 않았다면 nil이 아닌 값이 반환된다.
결국 옵셔널 타입이란 반환하고자 하는 값을 옵셔널 객체로 다신 한번 감싼 형태인 것이다. 앞서 "123"을 숫자로 변환한 값을 반환하고자 할 때 123을 직접 반환하는 것이 아니라 옵셔널 타입으로 감싼 Optional(123)을 반환한다.
Int(123) -> Optional(123)
Int("swift") -> nil
처리가 성공적이 경우 옵셔너로 둘러싸여 있는데 이를 옵셔널 래핑이라 한다. 이렇게 받은 값은 옵셔널 언래핑을 통해 실제 값을 추출해서 사용할 수 있다.
옵셔널 타입 선언과 정의
일반 자료형을 옵셔널 자료형으로 만드는 방법은 자료형 뒤에 물음표만 붙이면 된다. 아래는 그 예제이다.
// 옵셔널 Int 타입
var optInt: Int?
// 옵셔널 String 타입
var optStr: String?
// 옵셔널 Double 타입
var optDouble: Double?
// 옵셔널 Array 타입
var optArr: [String]?
// 옵셔널 Dictionary 타입
var optDict: Dictionary<String, String>?
var optDict2: [String : String]?
// 옵셔널 Class 타입
var optClass: AnyObject?
일반 자료형을 선언만 하고 초기화하지 않으면 아무것도 할당되지 않지만, 옵셔널 타입으로 자료형을 선언하면 자동으로 nil로 초기화 된다. 옵셔널 타입으로 선언된 변수나 상수에 실제 값을 할당하는 방법은 일반 타입과 동일하다.
아래는 옵셔널 타입의 변수와 상수에 값을 할당하는 방법이다.
// 옵셔널 Int 타입
var optInt2: Int?
optInt2 = 3
// 옵셔널 String 타입
var optStr2: String?
optStr2 = "swift"
// 옵셔널 Array 타입
var optArray: [String]?
optArray = ["C", "Java", "Python", "Swift"]
// 옵셔널 Dictionary 타입
var optDic: Dictionary<String, Int>?
optDic = ["국어" : 94, "수학" : 88, "영어" : 96]
값을 대입할 때에는 옵셔널이 아닌 일반 변수처럼 생각하고 다루어도 무방하다.
옵셔널 값 처리
옵셔널 타입의 결과값은 그 자체로 아무것도 할 수 없다. 옵셔널 타입은 애초에 연산을 지원하지 않는다. 옵셔널 Int와 Int, 옵셔널 String 과 String 간의 결합도 불가능하다.
Int("123") + Int("123")
Int("123") + 123
위의 두 케이스 모두 연산이 불가능하다. 연산을 위해서는 옵셔널 객체를 해제해야 하는데 내부의 값을 추출하는 과정을 옵셔널 해제라고 한다. 다른말로는 옵셔널 언래핑 이라고 한다.
옵셔널 해제는 명시적 해제와 묵시적 해제로 나누어진다. 명시적 해제는 강제적 해제, 비강제적 해제로 나누어지고, 묵시적 해제는 컴파일러에 의한 자동 해제와 연산자를 사용한 자동 해제로 나뉜다.
옵셔널 강제 해제
옵셔널을 강제 해제하는 방법은 옵셔널 타입의 값 뒤에"!"만 붙여주면 된다. 이 "!"를 강제 해제 연산자라고 한다.
var optInt3: Int? = 3
print("옵셔널 자체의 값 : \(optInt3)")
print("!로 강제 해제한 값 : \(optInt3!)")
// 결과값
// 옵셔널 자체의 값 : Optional(3)
// !로 강제해제한 값 : 3
이처럼 옵셔널 자체의 값은 Optional()로 감싸지고, 값이 있을 때 강제 해제를 하면 내부의 값이 추출된다.
Int("123")! + Int("123")! // 246
Int("123")! + 30 // 153
이렇게 옵셔널을 해제하여 연산이 가능하다. 하지만 nil일 때"!"을 사용하면 오류가 발생한다. 즉 강제 해제 연산자를 사용할 때에는 옵셔널 값이 nil값인지 체크가 필요하다. 이후 nil이 아닐 때만 강제 추출해야 한다.
var str = "123"
var intFromStr = Int(str)
if intFromStr != nil {
print("변환된 값은 \(intFromStr!)입니다.")
}else{
print("값 변환에 실패했습니다.")
}
// 결과값
// 변환된 값은 123입니다.
"123"은 Int 타입으로 변환이 가능하므로 intFromStr에는 Optional(123)이 담겨 있다. 따라서 if 조건절에서 nil이 아니므로 if 구문이 실행된다.
위 조건절에서 intFromStr 과 nil 사이의 "!=" 연산자 앞뒤로 공백이 있는 것을 알 수 있다. 이것은 가독성을 위해서가 아니다. 문법 오류를 방지하기 위한 목적이다. 만약 공백을 두지 않는다면 아래 처럼 두 가지로 해석할 수 있다.
intFromStr!=nil
// 해석1
(intFromStr)!=(nil)
// 해석2
(intFromStr!)=nil
해석1은 원래 의도대로 intFromStr 변수와 nil의 비교이다. 해석2는 intFromStr 변수의 옵셔널 강제 해제 + nil 값의 할당이다. 즉 해석의 차이가 발생하여 구문이 모호해질 수 있다. 컴파일러는 이를 확실하게 해석하지 못해 오류가 발생한다. 따라서 공백을 주어야 한다.
옵셔널 바인딩
위에서는 nil 체크 조건절을 통해 안전하게 옵셔널을 해제할 수 있었다. 이번에는 비 강제적인 옵셔널 해제 구문으로 바꾸어 작성하는 방식이다. 이를 옵셔널 바인딩 이라고 부른다.
var str = "Swift"
if let intFromStr = Int(str){
print("변환된 값은 \(intFromStr)입니다.")
}else{
print("값 변환에 실패했습니다.")
}
// 결과값
// 값 변환에 실패했습니다.
intFromStr이 상수로 선언되었고 이 상수는 옵셔널이 아닌 일반 타입이다. 만약 Int(str) 의 결과값이 nil이라면 else 구문이 실행된다. 아래는 또 다른 예시이다.
func intSTr(str: String){
guard let intFromStr = Int(str) else {
print("값 변환에 실패하였습니다.")
return
}
print("변환된 값은 \(intFromStr)입니다.")
}
위는 guard 문을 사용해 옵셔널 바인딩을 구현한 예제이다. 사용 용법은 앞선 if let 과 비슷하지만, guard 구문은 조건에 맞지 않으면 무조건 함수의 실행을 종료시키므로, 실행 흐름상 옵셔널 값이 해제되지 않으면 더 이상 진행이 불가능 할 정도로 큰일이 생길 때만 사용하는 것이 좋다.
옵셔널과 관련해서 딕셔너리를 살펴볼 것이다.
var capital = ["KR" : "Seoul", "EN" : "London", "JP" : "Tokyo"]
print(capital["KR"])
print(capital["KR"]!)
// 결과값
// Optional("Seoul")
// Seoul
딕셔너리에 키로 접근하면 그 결과값은 옵셔널 타입으로 반환된다. 그 이유는 딕셔너리에 키로 사용될 수 있는 값은 Hashable 프로토콜이 구현된 모든 자료형이기 때문이다. 키 사용 제한이 거의 없으므로 존재하지 않는 키를 사용할 가능성이 있다. 이 경우 주어진 키에 값이 비어있거나 입력된 키가 아예 없다는 것을 표현하기 위해 nil 을 반환해야 한다. 따라서 위 코드를 아래와 같이 개선할 수 있다.
var capital = ["KR" : "Seoul", "EN" : "London", "JP" : "Tokyo"]
if(capital["KR"] != nil){
print(capital["KR"])
}
// 혹은
if let val = capital["KR"] {
print(val)
}
// 결과값
// Seoul
// Seoul
컴파일러에 의한 옵셔널 자동 해제
let optInt = Int("123")
if (optInt == 123) {
print("optInt == 123")
}else{
print("optInt != 123")
}
// 결과값
// optInt == 123
강제 해제하지 않은 옵셔널 값은 Optional(123)이므로 정수값 123과는 다르다. 따라서 else 구문이 실행되야 하지만 그렇지 않다. 그이유는 옵셔널 타입으로 감싼 변수나 상수는 그 값을 사용하기 위해 반드시 "!"연산자를 사용하여 옵셔널 객체를해제해야 한다. 하지만 굳이 해제하지 않더라도 괜찮을 때가 있다. 비교 연산자를 사용하는 경우가 그에 해당된다. 한쪽 타입이 옵셔널, 다른 한쪽 타입이 일반타입이라면 자동으로 옵셔널을 해제한다. 따라서 아래 예제의 결과는 모두 true 이다.
let tempInt = Int("123")
tempInt == 123
tempInt == Optional(123)
tempInt! == 123
tempInt! == Optional(123)
// 1
var optValue01 = Optional(123)
// 2
var optValue02 : Int? = 123
옵셔널 값의 할당도 살펴 보면 위 코드의 1, 2번은 동일하다고 볼 수 있다. 2번처럼 직접 값을 대입하더라도 타입이 옵셔널 타입이므로, 실제 같은 Optional(123)이 저장된다.
옵셔널의 묵시적 해제
옵셔널 타입을 해제하는 방법 중 묵시적 해제라는 개념이 존재한다. 비록 옵셔널 타입이지만 값을 사용할 때에는 자동으로 옵셔널이 해제되기 때문에 굳이 "!"연산자를 사용하여 해제할 필요가 없는 편리한 구문이다. 묵시적 해제 구문은 "?"연산자 대신 "!"연산자만 붙여주면 된다. 아래의 코드는 명시적 옵셔널과 묵시적 옵셔널의 예시이다.
// 명시적 옵셔널
var str: String? = "Swift"
print(str)
// 결과값
// Optional("Swift")
// 묵시적 옵셔널
var str:String! = "Swift"
print(str)
// 결과값
// Optional("Swift")

이러한 내용이 나오는데 나중에 더 자세하게 적기로...
묵시적 옵셔널에서는 컴파일러에 의해 옵셔널 객체가 자동으로 해제되었다. 하지만 그렇다고 해서 nil을 넣는다고 문제가 생기진 않는다. 즉 str이 옵셔널 타입으로 정의되어 있음을 의미한다. 묵시적 옵셔널을 사용하면 옵셔널 타입 변수의 연산도 간단하게 처리할 수 있다.
var value01: Int? = 10
value01 + 5 // 오류
var value02: Int! = 10
value02 + 5 // 15
이처럼 묵시적 해제를 선언한 옵셔널은 일반 타입처럼 사용할 수 있어 굉장히 편리하다. 그런데 주의해야 할 점이 있다. 변수의 값이 nil이 될 가능성이 있다면 묵시적 옵셔널 해제를 사용하지 않아야 한다는 점이다.
변수가 nil이 될 가능성이 있을 때 사용하는 것이 옵셔널 타입인데 nil이 될 가능성이 있으면 사용하지 말라는 말은 좀 이상하다. 하지만 실제로 묵시적 옵셔널이 유용하게 사용되는 경우는 클래스 또는 구조체 내에서 쓰인다. 주로 멤버 변수를 정의할 때 선언과 초기화를 분리시키는 경우에 그렇다. 따라서 nil이 될 가능성이 있는 변수에는 묵시적 옵셔널 해제를 사용해서는 안된다.