옵셔널(Optionals)은 swift의 특징 중 하나인 안전성을 나타낸다고 할 수 있다.
단어 뜻 그대로 '선택적인' 즉, 값이 '있을 수도 있고, 없을 수도 있다.'라는 의미이다. 다시 말해서, '변수 또는 상수의 값이 nil일 수도 있다.'라는 의미이다. 여기서 nil은 C언어에서의 NULL이라고 이해하면 된다.
이 개념을 공부하기 시작하였을 때 필자는 '변수 또는 상수의 값에 nil이 들어갈 수도 있지, 왜 이렇게 만들었을까?'라는 의문을 가졌는데 이 개념을 알고나서 왜 필요한지를 알게 되었다. 만약 여러분도 이러한 의문을 가졌다면, 이 글을 읽으면서 해소할 수 있기를 바란다.
옵셔널과 옵셔널이 아닌 값은 철저히 다른 타입으로 인식하기 때문에 컴파일할 때 바로 에러를 내뱉는다.
var name: String = "silverCastle"
name = nil
결과
'nil' cannot be assigned to type 'String'
옵셔널 변수 또는 상수 등은 데이터 타입 뒤에 ?를 붙여 표현한다. 옵셔널 타입의 값을 print 함수를 통해 출력하면 아래와 같이 출력되는 것이 정상이다.
var name: String? = "silverCastle"
print(name)
name = nil
print(name)
결과
Optional("silverCastle")
nil
옵셔널의 놀라운 점은 열거형으로 구현되어 있다는 점인데 옵셔널의 정의는 다음과 같다.
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
public init(_ some: Wrapped)
}
보시다시피, 옵셔널이 값을 갖는 케이스와 값을 가지지 않는 케이스로 정의되어 있다. nil일 때는 none 케이스가 되고 값이 있는 경우는 some 케이스가 되는데 만약 값이 있는 경우라면 some의 연관 값인 Wrapped에 값이 할당된다. 즉, 값이 옵셔널이라는 열거형의 방패막에 보호되어 래핑되어 있는 모습이다.
옵셔널 자체가 열거형이므로 옵셔널 변수는 switch문을 통해 값이 있는지 없는지 확인할 수 있다.
func checkOptionalValue(value optionalValue: Any?) {
switch optionalValue {
case .none:
print("nil 값이 들어있습니다.")
case .some(let value):
print("\(value) 값이 들어있습니다.")
}
}
var name: String? = "silverCastle"
checkOptionalValue(value: name)
name = nil
checkOptionalValue(value: name)
결과
silverCastle 값이 들어있습니다.
nil 값이 들어있습니다.
값이 옵셔널이라는 열거형의 방패막에 보호되어 래핑되어 있는 모습인데, 옵셔널의 값을 옵셔널이 아닌 값으로 추출할 수도 있다.
옵셔널이 아닌 값으로 추출할 수 있는 가장 간단하지만 가장 위험한 방법이다. 옵셔널의 값을 강제 추출하려면 옵셔널 값의 뒤에 !를 붙여주면 값을 강제로 추출하여 반환한다.
만약, 강제 추출 시 옵셔널에 값이 없다면?
당연하듯이, 런타임 시 오류를 내뱉는다.
var name: String? = "silverCastle"
var myName: String = name!
name = nil
myName = name!
결과
__lldb_expr_29/MyPlayground.playground:36: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Playground execution failed:
.
.
.
이를 if문을 이용해서 조금 더 안전하게 처리해볼 수 있다.
var name: String? = "silverCastle"
var myName: String = name!
name = nil
//myName = name!
if name != nil {
print("내 이름은 \(name!)입니다.")
} else {
print("내 이름은 없습니다.")
}
결과
내 이름은 없습니다.
다른 프로그래밍 언어에서 NULL 값을 체크하는 것처럼 if문을 이용해서 안전하게 처리해볼 수 있지만 좀 더 세련된 방법을 사용해보자.
옵셔널에 값이 있다면 옵셔널에서 추출한 값을 일정 블록 안에서 사용할 수 있는 상수 또는 변수로 할당해서 옵셔널이 아닌 형태로 사용할 수 있다.
var name: String? = "silverCastle"
if var myName = name {
print("내 이름은 \(myName)입니다.")
} else {
print("내 이름은 없습니다.")
}
결과
내 이름은 silverCastle입니다.
이러한 옵셔널 바인딩을 통해 한 번에 여러 옵셔널의 값을 추출할 수도 있다. ,를 사용하면 되는데 바인딩하려는 옵셔널 중 하나라도 값이 없다면 해당 블록 내부의 명령문은 실행되지 않는다.
var myName: String? = "silverCastle"
var yourName: String? = nil
if var name = myName, var friend = yourName {
print("출력이 될까요?")
}
yourName = "goldCastle"
if var name = myName, var friend = yourName {
print("출력이 됩니다!")
}
결과
출력이 됩니다!
매우 위험한 생각이긴 하지만 로직상 nil 때문에 런타임 오류가 발생하지 않을 것 같다는 확신이 들 때 nil을 할당해줄 수 있는 옵셔널이 아닌 변수나 상수가 있으면 좋을 것이다. 이때 사용하는 것이 암시적 추출 옵셔널이고 데이터 타입 뒤에 !를 붙여 표현한다.
여전히 옵셔널이기 때문에 nil도 할당해줄 수도 있고 일반 값처럼 사용할 수 있다.
var name: String! = "silverCastle"
name = nil
if let myName = name {
print("내 이름은 \(myName)입니다.")
} else {
print("내 이름은 없습니다.")
}
결과
내 이름은 없습니다.
단, nil이 할당되어 있을 때 접근을 시도한다면 에러를 내뱉는다는 것을 주의하자.
var name: String! = "silverCastle"
name = nil
if let myName = name {
print("내 이름은 \(myName)입니다.")
} else {
print("내 이름은 없습니다.")
}
name.isEmpty
결과
error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).