본문 바로가기
프로그래밍 팁/Swift

1-13. Swift - 옵셔널(Optionals) 문법 개념 정리

by Archivers 2020. 11. 11.

 

옵셔널(Optionals)

값이 존재하지 않을 수 있는 상황에서는 옵셔널을 사용합니다. 옵셔널은 두 가지 가능성을 내포합니다. 값이 존재하는 가능성과 존재하지 않는 가능성입니다. 값이 존재하는 경우에는 해당 값에 접근하기 위해 옵셔널을 해제할 수 있습니다.

NOTE

옵셔널이라는 개념은 C 언어나 Objective-C 언어에는 존재하지 않습니다. 그나마 Objective-C 언어에서 객체 대신에 nil을 반환하는 메소드가 Swift의 옵셔널과 유사하다고 볼 수 있습니다. 여기에서 nil은 타당한 객체(valid object)가 존재하지 않는 것을 의미합니다. 하지만 Objective-C 언어의 이 nil이라는 개념은 객체에만 해당하는 개념으로 구조체(structure)나 기본적인 C 타입(basic C type), 열거형 값(enumeration value)에는 해당하지 않습니다. Objective-C 언어에는 후술된 구조체, 기본적인 C 타입, 열거형 값의 경우 값이 존재하지 않는다는 것을 지칭하기 위해 NSNotFound와 같은 특별한 값을 반환하는 메소드가 존재하기도 합니다. 이 메소드는 호출자로 하여금 특별한 값이 존재한다는 것을 알고 있으며 따라서 이 값을 검사해야 할 필요가 있다는 것을 알고 있으리라 가정합니다. Objective-C 언어와는 다르게 Swift의 옵셔널을 사용하면, 특별한 상수를 사용하지 않고도 어떤 타입의 값이든 값이 존재하지 않는 가능성을 나타낼 수 있습니다.

 

Swift의 Int 타입은 이니셜라이저를 가지고 있는데, 이 이니셜라이저는 String 타입의 값을 Int 타입의 값으로 변환하도록 시도할 수 있습니다. 하지만 모든 String 타입의 값이 Int 타입으로 변환될 수는 없습니다. 123이라는 String 타입의 값이 있다고 가정해 봅시다. 여기에서 123은 숫자 123이 아니라 문자열 타입으로 작성된 123입니다. 숫자 123은 사칙연산을 할 수 있지만 문자열 123은 사칙연산을 할 수 없습니다. 수험번호나 사원번호 등의 경우가 이러한 문자열 타입으로 작성된 숫자에 해당할 것입니다. 만약 이 String 타입의 값인 123을 Int 타입의 값인 123으로 변환하려고 하면 변환이 성공적으로 가능합니다. 123은 숫자로도 쓰일 수 있기 때문입니다. 문자열 123을 표시할 때는 큰따옴표("") 안에 해당 값을 넣으면 되지만, 정수 123을 표현할 때는 큰따옴표를 제거하면 됩니다. 하지만 hello, world와 같은 String 타입의 값을 Int 타입의 값으로 변환하려고 하면 오류가 발생합니다. hello, world는 숫자로 변환될 수 없기 때문입니다.

let possibleNumber = "123"
let converedNumber = Int(possibleNumber)

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

possibleNumber 상수에 String 타입의 123을 저장하고자 합니다.

그리고 이 possibleNumber 상수에 담긴 값을 Int() 안에 넣는 방식으로 Int 타입으로 변환한 다음 converedNumber 상수에 저장하고자 합니다.

 

이처럼 Int() 안에 상수 값이나 변수 값을 넣는 방식으로 적으면, Int 타입의 이니셜라이저는 해당 값을 명시된 타입으로 변환하도록 시도하게 됩니다. 하지만 이러한 방식을 통해 Int 타입으로 변환을 하면 결과물은 일반적인 Int 타입이 아니라 옵셔널 Int 타입이 됩니다. Int 타입의 이니셜라이저 입장에서는 Int() 안에 무엇이 들어올지 모르기 때문입니다. 위 예시에서는 String 타입의 123이 들어와서 Int 타입으로 변환하는데 문제가 없지만, 만약 String 타입의 hello, world가 들어온다면 이는 Int 타입으로 변환하는 것이 불가능합니다. 따라서 Int 타입의 이니셜라이저는 Int() 안에 들어온 것을 Int 타입으로 변환할 때 결과물을 옵셔널 Int 타입으로 변환하는 것입니다. 설령 String 타입의 123이 들어오더라도 결과물은 옵셔널 Int 타입의 123이 되는 것입니다. 옵셔널 Int 타입의 123과 Int 타입의 123은 다릅니다.

 

옵셔널 Int 타입은 줄여서 Int?로 표시합니다. 물음표(?)는 해당 타입의 값이 옵셔널이라는 것을 뜻합니다. 즉, 해당 타입의 값에는 Int 값이 존재할 수도 있으며, 존재하지 않을 수도 있는 두 가지 가능성을 가지고 있습니다. Int? 타입의 경우에는 무조건 Int 타입의 값이 존재하거나 아무 값도 존재하지 않는 것입니다. 나중에 살펴보겠지만 String? 타입은 옵셔널 문자열 타입으로서 이 값에는 String 타입의 값이 존재하거나 아무 값도 존재하지 않을 수 있는 두 가지 가능성이 있습니다.

 

각설하고 위 예시의 결과로 converedNumber 상수에는 옵셔널 Int 타입(Int?)의 값인 123이 저장됩니다.

 

 

nil

옵셔널 타입의 변수를 값이 존재하지 않는 상태로 만들고 싶다면 해당 변수에 nil 값을 저장하면 됩니다.

var serverResponseCode: Int? = 404
serverResponseCode = nil

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

serverResponseCode라는 이름의 변수를 선언하고자 합니다. 이 변수의 타입은 옵셔널 Int 타입입니다. 그리고 이 값에는 404라는 정수 값을 저장하고자 합니다.

그 다음 이 serverResponseCode 변수에 nil 값을 다시 저장하고자 합니다. 원래 serverResponseCode 변수에는 404가 저장되어 있었으나 nil 값을 다시 저장함으로써, serverResponseCode 변수는 현재 아무 값이 존재하지 않는 상태가 되었습니다.

NOTE

옵셔널이 아닌 상수나 변수에는 nil 값을 저장할 수 없습니다. 만약 사용하고자 하는 상수나 변수가 특정한 조건에서는 값이 존재하지 않을 수 있다면, 해당 상수나 변수는 옵셔널 타입으로 선언해야 합니다.

 

만약 옵셔널 변수를 선언하는 과정에서 코드를 작성하는 사용자가 기본값을 제공하지 않는다면, 해당 변수에는 자동으로 nil 값이 저장됩니다.

var surveyAnswer: String?

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

surveyAnswer라는 이름의 변수는 옵셔널 String 타입을 가지도록 명시하고 선언하였습니다. 하지만 기본값은 입력하지 않았습니다. 따라서 이 변수에는 자동으로 nil 값이 저장될 것입니다.

NOTE

Swift의 nil은 Objective-C 언어의 nil과는 성격이 다릅니다. Objective-C 언어에서 사용하는 nil은 존재하지 않는 객체(nonexistent object)에 대한 포인터(point)이지만, Swift 언어에서 사용하는 nil은 포인터가 아니라 특정 타입의 값이 존재하지 않음을 나타내는 특별한 표현입니다. Swift 언어의 nil은 객체 타입뿐만 아니라 어느 타입에나 사용할 수 있다는 점에서 Objective-C 언어와 다릅니다.

 

 

If문과 강제 해제(If statements and forced unwrapping)

어떤 옵셔널에 값이 존재하는지 여부를 확인하기 위해 if문을 사용할 수 있습니다. if문의 조건문에서 해당 옵셔널이 nil과 일치하는지 여부를 일치(==) 또는 불일치(!=)와 같은 비교 연산자를 사용하여 비교해 보면 됩니다.

if convertedNumber != nil {
	print("convertedNumber 상수는 정수 값을 지니고 있습니다.")
}

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

위에서 convertedNumber 상수에 옵셔널 123을 저장한 바 있습니다. 따라서 convertedNumber 상수에는 값이 존재합니다. 따라서 이 상수는 nil과 일치하지 않습니다. 따라서 해당 조건문은 참이므로 {} 안의 print 함수를 호출하게 됩니다. 이 print 함수는 'convertedNumber 상수는 정수 값을 지니고 있습니다.'를 출력할 것입니다.

 

만약 특정한 옵셔널에 값이 확실히 존재한다는 것을 알고 있다면, 해당 옵셔널의 이름 뒤에 느낌표(!)를 붙임으로써 해당 옵셔널에 담긴 값에 접근할 수 있습니다. 이처럼 옵셔널의 이름 뒤에 느낌표를 붙이는 방식을 해당 옵셔널의 값을 강제로 해제(forced unwrapping)한다고 일컫습니다.

if convertedNumber != nil {
	print("convertedNumber 상수에는 \(convertedNumber!)이라는 정수 값이 존재합니다.")
}

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

if문의 조건문을 통해 convertedNumber 상수가 nil과 일치하는지 여부를 비교합니다. convertedNumber 상수에는 옵셔널 123이 담겨있으므로 nil과 일치하지 않습니다. 따라서 if문의 조건문은 참이 됩니다. 따라서 {} 안에 적힌 print 함수를 호출합니다. 이 print 함수는 'convertedNumber 상수에는 \(convertedNumber!)라는 정수 값이 존재합니다.'라는 문구를 출력할 것입니다. convertedNumber 상수는 Int? 타입이지만, convertedNumber 상수의 이름 뒤에 !를 붙이면 옵셔널이 강제로 해제됩니다. ?가 !가 되면서 옵셔널이 강제로 해제되는 것입니다. 따라서 \(convertedNumber!)은 정수 값 123으로 대체됩니다. 이 print 함수는 'convertedNumber 상수에는 123이라는 정수 값이 존재합니다.'를 출력할 것입니다.

NOTE

값이 존재하지 않는 옵셔널에 !로 강제 해제하려고 하면 런타임 오류가 발생합니다. 따라서 옵셔널 강제로 해제하기 전에 해당 옵셔널이 값을 가지고 있는지 확인해야 합니다.

 

 

옵셔널 바인딩(Optional binding)

어떤 옵셔널에 값이 존재하는지 여부를 확인하는 방법으로 옵셔널 바인딩이 있습니다. 그리고 만약 해당 옵셔널에 값이 존재한다면 해당 값을 일시적으로 상수 또는 변수로써 사용 가능하도록 만들 수 있습니다. 옵셔널 바인딩은 if나 while문을 통해 이루어질 수 있습니다. if나 while문을 통해 해당 옵셔널에 값이 존재하는지 여부를 확인하고, 만약 값이 존재한다면 그 값을 상수나 변수로 추출하는 것입니다.

if let constantName = someOption {
	statements
}

 

if문을 통해 옵셔널 바인딩을 하면 위와 같은 형태를 가집니다.

아래 코드는 강제 해제 대신 옵셔널 바인딩을 진행하는 예시입니다.

let possibleNumber = "123"
if let actualNumber = Int(possibleNumber) {
	print("문자열 \"\(possibleNumber)\"은 \(actualNumber) 정수 값을 가집니다.")
} else {
	print("문자열 \"\(possibleNumber)\"은 정수로 변환될 수 없습니다.")
}

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

possibleNumber 상수에는 문자열 123이 저장되어 있습니다.

이 상수에 담긴 값을 Int 타입으로 변환하도록 시도하고, 만약 변환이 성공한다면, 즉 Int? 값이 존재한다면, 이 옵셔널 값을 actualNumber라는 새로운 상수에 저장하고자 합니다. possibleNumber 상수에는 문자열 123이 담겨 있으므로, 문자열 123을 정수 123으로 변환할 수 있습니다. 이 정수 123은 Int? 타입으로 존재하는 값입니다. 그리고 이 값을 actualNumber라는 상수에 저장할 수 있으므로, if문에 적힌 조건문은 성립, 즉 참이 됩니다. 이 경우에 옵셔널을 강제로 해제하여 actualNumber 상수에 값을 저장할 필요가 없습니다. 왜냐하면 옵셔널 바인딩 방식으로 옵셔널의 값에 접근하는 것이 성공했기 때문입니다. 결국 {} 안에 적힌 print 함수를 호출하게 됩니다. 만약 possibleNumber 상수에 문자열 123 대신 문자열 hello가 저장되어 있었다면, 변환이 실패하여 actualNumber라는 상수에 저장할 수 없었을 것입니다. 따라서 if문의 조건문은 성립하지 못하게 되고, 즉 참이 아니므로, else {} 안에 적힌 print 함수가 호출될 것입니다.

 

옵셔널 바인딩을 할 때는 상수와 변수 모두 사용할 수 있습니다. 즉, 위 예시에서 if let actualNumber 대신 if var actualNumber로 코드를 작성해도 문제가 없습니다. 이 경우 actualNumber는 상수 대신 변수가 될 뿐입니다.

 

하나의 if문 안에는 여러 개의 옵셔널 바인딩과 불리언 조건을 함께 포함시킬 수도 있습니다. 단 쉼표(,)로 구분만 해 주면 됩니다. 만약 해당 옵셔널 중 하나라도 값이 존재하지 않거나(nil), 불리언 조건이 거짓(false)이 된다면 중 하나라도 거짓이 된다면, if문 전체가 거짓(false)이 된다는 점을 주의해야 합니다.

if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
	print("\(firstNumber) < \(secondNumber) < 100")
}

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

하나의 if문 안에 두 개의 옵셔널 바인딩과 한 개의 불리언 조건문이 담겨 있습니다. 문자열 4를 정수 타입으로 변환을 시도하고 변환이 성공하면 이 값을 firstNumber라는 상수에 저장합니다. 문자열 4는 정수 타입으로 변환이 가능하므로 Int? 타입의 4가 firstNumber 상수에 저장됩니다. 그리고 문자열 42를 정수 타입으로 변환을 시도하고 변환이 성공하면 이 값을 secondNumber라는 상수에 저장합니다. 문자열 42는 정수 타입으로 변환이 가능하므로 Int? 타입의 42가 secondNumber 상수에 저장됩니다. 그리고 firstNumber < secondNumber를 비교한 결괏값과 secondNumber < 100을 비교한 결괏값을 비교합니다. firstNumber < secondNumber를 보면 4 < 42이므로 이 불리언 조건은 참(true)이 됩니다. secondNumber < 100을 보면 42 < 100이므로 이 불리언 조건도 참이 됩니다. 따라서 true && true를 비교하는 불리언 조건으로 정리가 됩니다. &&는 영어의 AND와 같습니다. &&의 좌우의 값이 모두 참일 경우 결과도 참이 됩니다. 따라서 이 불리언 조건은 결과적으로 참이 됩니다. 만약 && 좌우의 값이 모두 거짓(false)이라면 결과는 거짓이 됩니다. 만약 && 좌우의 값이 참과 거짓이라면 결과는 거짓이 됩니다. 따라서 이 if문에 적힌 두 개의 옵셔널 바인딩은 성공하였으며 한 개의 불리언 조건문도 결괏값이 참이 나왔으므로, 즉 모두 성립하므로 {} 안에 담긴 print 함수를 호출하게 됩니다. 이 print 함수는 '4 < 42 < 100'을 출력할 것입니다.

 

위 코드를 아래와 같이 바꾸어 표현할 수도 있습니다. 두 if문의 결과는 같습니다.

if let firstNumber = Int("4") {
	if let secondNumber = Int("42") {
		if firstNumber < secondNumber && secondNumber < 100 {
			print("\(firstNumber) < \(secondNumber) < 100")
		}
	}
}

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

만약 Int("4")의 값을 성공적으로 firstNumber 상수에 저장할 수 있다면, 그리고 Int("42')의 값을 성공적으로 secondNumber 상수에 저장할 수 있다면, 그리고 firstNumber < secondNumber && secondNumber < 100이 참이라면, print 함수를 호출하고자 합니다.

NOTE

if문 안에서 옵셔널 바인딩을 통해 선언된 상수나 변수는 오직 if문 안에서만 사용할 수 있습니다. 반면 guard문을 통해 생성도니 상수나 변수는 오직 guard문 안에서만 사용할 수 있습니다. 후자의 경우 Early Exit이라고 일컫습니다.

 

 

옵셔널 바인딩(Optional binding)

암묵적으로 해제된 옵셔널(Implicitly unwrapped optionals)

옵셔널은 상수나 변수에 값이 존재하거나 존재하지 않는 가능성을 표현합니다. if문을 통해 옵셔널에 값이 존재하는지 여부를 확인할 수 있으며, 만약 값이 존재한다면 옵셔널 바인딩을 통해 해당 옵셔널을 해제하여 옵셔널에 담긴 값에 접근할 수 있습니다.

 

특정한 옵셔널에 값이 무조건 존재할 것이라는 점이 명확한 경우도 있습니다. 이러한 경우에는 옵셔널에 담긴 해당 값에 접근할 때마다 굳이 옵셔널에 값이 존재하는지 여부를 확인하고 해당 값을 해제하는 것은 매우 번거로운 작업이 됩니다. 왜냐하면 특정한 옵셔널에 값이 무조건 존재할 것이라는 점이 명확하다면, 굳이 이러한 과정을 매번 거칠 필요가 없기 때문입니다.

 

이러한 경우에 해당하는 옵셔널을 암묵적으로 해제된 옵셔널이라고 일컫습니다. 옵셔널의 이름 뒤에 물음표(?)를 붙이는 대신 느낌표(!)를 붙이면 해당 옵셔널을 암묵적으로 해제할 수 있게 됩니다.

 

옵셔널이 처음 정의될 때 해당 옵셔널에 무조건 값이 존재할 것이라는 점을 알고 있다면 암묵적으로 해제된 옵셔널이 유용하게 쓰일 수 있습니다. 이러한 방식은 클래스(class)를 초기화하는 과정에서 주로 쓰입니다.

암묵적으로 해제된 옵셔널은 형식은 일반적인 옵셔널이지만, 해당 옵셔널에 담긴 값에 접근할 때마다 옵셔널을 해제할 필요가 없다는 점에서 옵셔널이 아닌(non-optional) 값처럼 쓰일 수 있기도 합니다.

 

아래 코드는 옵셔널 string과 암묵적으로 해제된 옵셔널 string 간의 차이가 있음을 보여줍니다.

let possibleString: String? = "옵셔널 string입니다."
let forcedString: String = possibleString!

let assumedString: String! = "암묵적으로 해제된 옵셔널 string입니다."
let implicitString: String = assumedString

 

possibleString이라는 상수를 선언하고자 합니다. 이 상수의 타입은 옵셔널 String이며, 이 상수에 '옵셔널 string입니다.'라는 String 타입의 값을 저장하고자 합니다. 그리고 forcedString이라는 상수를 선언하고자 합니다. 이 상수의 타입은 String이며, 이 상수에는 possibleString 상수의 옵셔널을 해제하여 저장하고자 합니다. possibleString 상수는 옵셔널입니다. 즉 이 상수 안에 값이 존재하지 않을 가능성이 있기 때문에 String?으로 타입을 명시하여 선언한 것입니다. 그리고 이 상수에는 '옵셔널 string입니다.'라는 String 타입의 값이 존재하고 있습니다. 따라서 이 상수를 느낌표(!)을 사용하여 옵셔널을 해제하면 forcedString 상수에 저장할 수 있는 것입니다.

 

assumedString 상수를 선언하고자 합니다. 이 상수의 타입은 옵셔널이 암묵적으로 해제된 String이며 이 상수에는 '암묵적으로 해제된 옵셔널 string입니다.'라는 String 타입의 값을 저장하고자 합니다. 그리고 implicitString이라는 상수를 선언하고자 합니다. 이 상수의 타입은 String이며, 이 상수에는 assumedString 상수의 값을 저장하고자 합니다. assumedString 상수를 정의할 때 이 상수에는 항상 값이 존재할 것이라는 확신 하에 String! 타입으로 선언하였습니다. 따라서 이 상수를 implicitString 상수에 저장할 때는 굳이 옵셔널을 해제하는 과정이 필요가 없는 것입니다.

 

암묵적으로 해제된 옵셔널은 마치 옵셔널이 사용될 때마다 자동으로 옵셔널을 해제해 달라고 허락을 구하는 것과 같습니다. 상수의 이름 뒤에 느낌표를 붙여 옵셔널을 해제하는 대신, 옵셔널의 타입 뒤에 느낌표를 붙여 선언하면 됩니다.

NOTE

만약 암묵적으로 해제된 옵셔널에 값이 존재하지 않는데, 이 nil 값에 접근하려고 시도하면 런타임 오류를 발생시키게 됩니다. 이는 일반적인 옵셔널에 값이 존재하지 않는데 느낌표를 붙여서 이 nil 값에 접근하려고 할 때 오류를 발생시키는 것과 같은 상황입니다.

 

암묵적으로 해제된 옵셔널은 값의 존재 여부를 확인하기 위해 일반적인 옵셔널처럼 사용할 수도 있습니다.

let assumedString: String! = "암묵적으로 해제된 옵셔널 string입니다."
if assumedString != nil {
	print(assumedString!)
}

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

assumedString 상수를 선언하고자 합니다. 이 상수는 옵셔널이 암묵적으로 해제된 String 타입입니다. 그리고 이 상수에는 '암묵적으로 해제된 옵셔널 string입니다.'라는 String 값을 저장하고자 합니다.

만약 assumedString 상수와 nil이 불일치한다면, 즉 assumedString 상수에 값이 존재한다면, print 함수를 호출합니다. 이 print 함수는 assumedString을 느낌표(!)로 옵셔널을 해제한 다음 값을 출력합니다. 결과적으로 '암묵적으로 해제된 옵셔널 string입니다.'가 출력될 것입니다.

 

암묵적으로 해제된 옵셔널은 값의 존재 여부를 확인하고 옵셔널을 해제하기 위해 옵셔널 바인딩을 사용할 수도 있습니다.

if let definiteString = assumedString {
	print(definiteString)
}

 

위 코드는 다음과 같이 글로 표현할 수 있습니다.

 

assumedString 상수에 값이 존재한다면 definiteString 상수에 저장 및 선언하고자 합니다. assumedString 상수에는 값이 존재합니다. 따라서 definiteString 상수에 값이 저장됩니다. 즉 if문의 조건문이 성립하므로 print 함수가 호출됩니다. 결과적으로 '암묵적으로 해제된 옵셔널 string입니다.'가 출력될 것입니다.

NOTE

혹시 나중에라도 값이 존재하지 않을 가능성이 있다면 암묵적을 해제된 옵셔널을 사용해서는 안 됩니다. 이와 같은 경우에는 일반적은 옵셔널 타입을 사용해야 합니다.

 

 

 

 

 

이 글은 Apple이 제공하는 'The Swift Programming Language 5.2 버전' (https://swift.org)을 번역 및 참고하여 작성하였습니다.

댓글