옵셔널(Optional)은 스위프트에서 언어차원에서의 프로그램 안전성을 높이기 위해 사용하는 개념으로 'nil을 사용할 수 있는 타입과 사용할 수 없는 타입을구분하고, 사용할 수 있는 타입을 옵셔널 타입(Optional Type)이라고 부른다'고 할 수 있다.
여기서 말하는 nil이란, 값이 없음을 의미하는 특수한 값이다. 정수형의 0이나, 문자열의 ""(공백)과 다른, 순수하게 아무 값도 없다는 것을 의미한다. 이러한 특수성 때문에 nil은 종종 실제 값으로는 처리할 수 없는, 무언가 문제가 발생했을 때를 의미하기 위해 사용된다.
보통 문제가 발생했을 때는 오류를 발생시키는데 이는 그닥 좋은 흐름은 아니다. 왜냐하면 사소한 것 하나 때문에 실행을 중단시키는 것 뿐만 아니라 안정성에도 좋지 않기 때문이다. 이런 때를 위하여 바로 nil값이 존재한다.
대표적인 예로 딕셔너리가 있는데 딕셔너리는 키가 없는 상황에 대비해 자체적으로 옵셔널 값 처리가 된다.
let capital = ["KR": "Seoul", "CN": "Beijing", "JP": "Tokyo"]
capital["ko"] // nil
print(capital["KR"]) // Optional("Seoul")
주의할 점이 있는데, 스위프트에선 nil의 사용에 제약을 두었다. 바로 일반 자료형인 경우 nil 값을 가질 수 없는 것이다. 문자열이나 정수 등은 일반자료형이기 때문에 nil 값을 저장할 수 없다. 만약 억지로 대입하려고 하면 다음과 같이 오류가 발생한다.
이와 달리 옵셔널 타입으로 선언된 자료형은 nil값을 저장할 수 있다. 만약 nil값을 저장해야하거나 함수의 반환값에 nil 값이 포함될 가능성이 있는 경우, 즉 오류가 발생할 가능성이 있다면 반드시 반환 타입을 옵셔널 타입으로 설정해야 한다.
옵셔널 타입은 가질 수 있는 값이 nil값 혹은 nil이 아닌 값 두 가지만 존재하게 된다. nil이 아닌 값의 경우 옵셔널 객체로 감싼 형태를 가지게 된다.
Int("123") // Optional(123)
Int("안녕") // nil (문자열은 정수형이 될 수 없으므로)
이렇게 옵셔널 타입으로 반환된 값을 보면 옵셔널 타입으로 둘러 싸여있는 것을 확인할 수 있다. 이를 옵셔널 래핑(Optional Wrapping)이라고 한다. 이렇게 받은 값은 옵셔널 언래핑(Optional Unwrapping)과정을 통해 옵셔널 타입을 해제하고 실제 값을 추출하여 사용해야 한다.
그럼 이제부터 옵셔널에 대해 자세히 살펴보자.
옵셔널 타입 선언과 정의
일반 자료형을 옵셔널 타입으로 만드는 방법은 간단하다. 자료형 뒤에 그냥 물음표만 붙이면 된다. 'String?'은 Optional String 타입을 의미하고, 'Int?'는 Optional Int 타입을 의미한다. 다음 예시를 보면 이해하기 쉽다.
// 옵셔널 Int 타입
var optionalInt: Int?
optionalInt = 3
// 옵셔널 String 타입
var optionalString: String?
optionalString = 3
// 옵셔널 Array 타입
var optionalArray: [String]?
optionalArray = ["Korea", "China", "USA", "France"]
// 옵셔널 Dictionary 타입
var optionalDictionary: [String: Int]?
optionalDictionary = ["국어" : 96, "수학" : 70, "영어" : 88]
옵셔널 타입이라고 변수를 할당할 때 따로 처리가 필요한 것은 아니다. 일반 변수처럼 값을 할당하면 옵셔널 객체 내부에 값이 알아서 할당되기 때문이다. 값을 대입할 때에는 옵셔널타입이 아닌 일반 변수처럼 생각하고 다루면 된다.
옵셔널 값 처리
옵셔널 타입의 결과값은 자체적으로 아무것도 할 수 없다. 옵셔널 타입과 일반 타입도 서로 연산할 수 없으며 옵셔널 타입끼리의 연산이나 결합도 지원하지 않는다.
Int("123") + Int("123") // Optional(123) + Optional(123) >> ( X )
Int("123") + 30 // Optional(123) + 30 >> ( X )
이 때 해야하는 것이 바로 옵셔널 객체를 해제하는 것이다. 옵셔널 객체를 해제하면 일반 타입의 값이 되는데, 이 값이 바로 우리가 직접 사용할 수 있는 값이다.
옵셔널 해제의 경우 먼저 명시적 해제와 묵시적 해제로 나뉘고 명시적 해제는 강제 해제와 비강제 해제, 묵시적 해제는 컴파일러에 의한 자동해제와 !연산자를 사용한 자동해제로 나뉘게 된다.
- 명시적 해제
- 강제 해제
- 비강제 해제
- 묵시적 해제
- 컴파일러에 의한 자동해제
- !연산자를 사용한 자동해제
✅ 옵셔널 강제 해제
옵셔널을 강제해제하는 방법은 옵셔널 타입의 값 뒤에 "!"를 붙여주면 된다. 이렇게 처리하면 옵셔널 객체가 해제되고, 내부에 저장된 값을 꺼내 사용할 수 있게된다. 이 때 사용된 기호 "!"를 옵셔널에 대한 '강제 해제 연산자(Forced-Unwrapping Operaotr)'라고 한다.
var optionalInt: Int? = 3
print("옵셔널 자체값 : \(optionalInt)")
print("옵셔널 해제값 : \(optionalInt!)")
/* 실행결과
옵셔널 자체값 : Optional(3)
옵셔널 해제값: 3
이런식으로 해제가 이루어지게 된다. 이렇게 옵셔널 해제가 이루어진 값은 연산이 가능해진다.
Int("100")! + Int("120")!
// 220
Int("100") + 77
// 177
하지만 주의해야할 점이 있다. 바로 nil 값을 가지고 있는 옵셔널 변수에는 "!" 연산자를 붙이면 오류가 발생한다. 그렇기 때문에 우리는 옵셔널 변수나 상수를 안전하게 사용하려면 조건이 따르게되는데 바로 nil 값 여부를 먼저 확인해주는 것이다.
var str = "111"
var intFromStr = Int(str)
if intFromStr != nil {
print("값이 변환되었습니다. 변환된 값은 \(intFromStr!)입니다")
} else {
print("값 변환에 실패하였습니다")
}
/* 실행결과
값이 변환되었습니다. 변환된 값은 111입니다
이런식으로 if 구문을 사용해 nil값인 경우를 배제해줘야 "!"연산자를 더욱 안전하게 사용할 수 있게된다.
✅ 옵셔널 바인딩
앞서 nil 체크를 조건문을 사용해 안전하게 처리할 수 있었다. 이 예제는 동일한 기능을 하는 비강제적인 해제 구문으로 바꾸어 작성이 가능하다. 이는 if 구문 내에서 조건 대신 일반 변수나 상수에 할당하는 방식으로, 옵셔널 바인딩(Optional Binding)이라고 한다.
옵셔널 바인딩은 조건문 내에서 일반 상수에 옵셔널 값을 대입하는 방식으로 이루어진다. 반드시 조건문에서 사용해야만 하며, 상수에 옵셔널 값을 대입한 결과는 true / false로 리턴된다. 다음 예시를 살펴보자.
var str = "Swift"
if let intFromStr = Int(str) {
print("값이 변환되었습니다. 변환된 값은 \(intFromStr)입니다")
} else {
print("값 변환에 실패하였습니다")
}
강제 해제 연산자를 사용하지 않아도 옵셔널 값이 일반 변수나 상수에 할당되면서 자연스럽게 옵셔널 타입이 해제 되지만, 값이 nil이더라도 값의 할당이 실패하여 결과값이 false로 반환될 뿐이여서 오류는 발생하지 않는다. 단지 else 블록이 실행될 뿐이다.
func intStr(str: String) {
guard let intFromStr = Int(str) else {
print("값 변환이 실패하였습니다")
return
}
print("값이 변환되었습니다. 변환된 값은 \(intFromStr)입니다")
}
위 구문은 guard 구문을 이용해 옵셔널 바인딩을 구현한 예제이다. guard 구문은 특성상 함수나 메소드에만 사용할 수 있기 때문에 intStr() 함수를 정의하고 그 안에 guard 구문을 작성하였다.
보통 if 구문을 사용한 옵셔널 바인딩은 단순히 옵셔널 값의 처리 결과에 따라 서로다른 피드백을 주고 싶을 때 사용한다. 하지만 guard 구문은 조건에 맞지 않으면 무조건 함수의 실행을 종료시키는 특성이 있기 떄문에, 실행 흐름상 옵셔널 값이 해제되지 않으면 더이 상 진행이 불가능할 정도로 큰 일이 생길 때에만 사용하는 것이 좋다.
✅ 컴파일러에 의한 옵셔널 자동해제
강제 해제 연산자를 사용하거나 옵셔널 바인딩을 통해 옵셔널 타입을 해제하지 않아도 컴파일러에서 자동으로 옵셔널을 해제해 주는 경우가 있다. 바로 비교연산을 하는 경우이다. 다음 두 예시를 살펴보자.
// 예시 1. 강제해제 연산자 사용하는 경우
let optInt = Int("123")
if((optInt!) == 123) {
print("optInt == 123")
} else {
print("optInt != 123")
}
/* 실행결과
optInt == 123
*/
// 예시 2. 강제해제 연산자 사용하지 않는 경우
if (optInt == 123) {
print("optInt == 123")
} else {
print("optInt != 123")
}
/* 실행결과
optInt == 123
이처럼 옵셔널 객체의 값을 비교연산자를 사용해 비교하는 경우 명시적으로 옵셔널 객체를 강제 해제하지 않아도 한쪽이 옵셔널, 다른 한쪽이 일반 타입이라면 자동으로 옵셔널 타입을 해제하여 비교 연산을 수행한다.
✅ 옵셔널의 묵시적 해제
옵셔널 타입을 해제하는 방법 중에는 묵시적 해제(Implicitly Unwrapped Optional)이라는 개념이 존재한다. 이것은 비록 옵셔널 타입이긴 하지만 값을 사용할 때에는 자동으로 옵셔널이 해제되기 때문에 굳이 "!" 연산자를 사용하여 해제할 필요가 없는 편리한 구문이다.
묵시적 해제는 옵셔널 변수를 사용하는 모든 경우에 적용할 수 있으며, 옵셔널 변수의 타입을 선언할 때 묵시적 해제를 미리 선언해 주어야 한다는 차이점이 있다. 옵셔널 묵시적 해제 구문은 옵셔널 타입 선언시 해줬던 "?"를 "!"로 바꿔주기만 하면 된다.
// 명시적 옵셔널 선언
var str1: String? = "Swift Optional"
print(str)
/* 실행결과
Optional("Swift Optional")
*/
// 묵시적 옵셔널 선언
var str2: String! = "Swift Optional"
print(str)
/* 실행결과
Optional("Swift Optional")
예시에서 보듯 결과는 서로 다를 바가 없다. 하지만 var str: String!으로 선언해준 묵시적 옵셔널 선언 부분은 컴파일러에 의해 내부적으로 옵셔널 객체가 자동으로 해제된 상태이다. 확인하기 위해 문자열끼리의 연산을 해보자.
명시적 옵셔널의 경우 옵셔널 해제를 안 해줬기 때문에 이와 같이 오류가 발생한 것을 볼 수 있다.
하지만 묵시적 옵셔널의 경우 다음과 같이 연산이 가능하다.
이처럼 묵시적 해제를 선언한 옵셔널은 일반 타입처럼 사용할 수 있기 때문에 굉장히 편리하게 사용할 수 있다. 하지만 무조건 모든 경우에 묵시적 옵셔널을 사용할 수 있는 것은 아니다. 주의할 점이 하나 있는데 바로 '변수의 값이 nil이 될 가능성이 있다면 묵시적 옵셔널 해제를 사용하지 않아야 한다'는 것이다.
변수가 nil이 될 가능성이 있을 때 사용하는 것이 옵셔널 타입인데 변수가 nil이 될 가능성이 있다면 사용하지 말라는게 조금 아이러니한 부분이다. 그렇다면 도대체 언제 사용해야 되는걸까?
묵시적 옵셔널 해제를 사용하는 경우는 바로 '형식상 옵셔널로 정의해야하나, 실제 사용할 때엔 절대 nil 값이 대입될 가능성이 없는 변수일 때'이다. 다음 예제를 보자.
var value: Int! = Int("123")
이 구문은 어쩔 수 없이 Int(문자열)이 반환하는 타입이 옵셔널 타입이기 때문에 value 변수를 옵셔널 타입으로 선언해야 한다. 하지만 Int("123")은 정수로 변환될 것이 누가봐도 확실하다. 이런 확실한 값에 바로 묵시적 옵셔널을 사용해야 하는 것이다.
실제로 묵시적 옵셔널이 유용하게 사용되는 경우는 클래스 또는 구조체 내에서이다. 주로 멤버 변수를 정의할 때 선언과 초기화를 분리시켜야 하는 경우에 해당한다. 이는 추후 포스팅에서 다루도록 하겠다.
출처 : 꼼꼼한 재은씨의 Swift 문법편
'Swift > 문법' 카테고리의 다른 글
스위프트(Swift) - 함수(Function) Ⅱ. 내부 매개변수, 외부 매개변수 (0) | 2022.05.13 |
---|---|
스위프트(Swift) - 함수(Function) Ⅰ. 함수의 기본 개념 (0) | 2022.05.12 |
스위프트(Swift) - 집단자료형(Collection Types) Ⅳ. 딕셔너리(Dictionary) (0) | 2022.05.10 |
스위프트(Swift) - 집단 자료형(Collection Types) Ⅲ. 튜플(Tuple) (0) | 2022.05.09 |
스위프트(Swift) - 집단자료형(Collection Types) Ⅱ. 집합(Set) (0) | 2022.05.06 |