옵셔널(Optional)을 파헤쳐보자 1편에 이어서 옵셔널의 값에 접근하는 방법은 이번편에서 다뤄 보도록 하겠다.


[목차]
Optional의 값에 접근하는 방법
1. 강제 해제 - Forced Unwrapping(강제 언래핑)
2. 비강제 해제 - Optional Binding(옵셔널 바인딩)
3. 컴파일러에 의한 자동 해제
4. ! 연산자를 이용한 자동 해제 - Implicitly Unwrapped Optional(암묵적으로 벗겨진 옵셔널)

Optional의 값에 접근하는 방법

옵셔널 타입의 변수를 출력하면 Optional( )이 항상 따라 다닌다. Optional이 감싸고 있는 값에 접근하는 방법은 없을까?

🙋 2번 목차를 공부하고 정리하기까지 정말 헷갈려서 찾아보았던 점을 공유한다.

  • ‘옵셔널 해제’’옵셔널 바인딩‘은 같은 말이 아니다!
  • '옵셔널 해제'는 Optional()를 벗겨내고 '값을 추출'하는 것을 의미한다.
    '옵셔널 바인딩'은 옵셔널 타입 변수의 '값에 접근'하기 위한 4가지 방법 중 하나인 명시적 방법(비강제 해제)이다.
  • 많은 블로그에서 '옵셔널 해제'라는 표현을 많이 쓰는데
    Swift 공식 문서(https://bbiguduk.gitbook.io/swift/language-guide-1/optional-chaining#optional-chaining-as-an-alternative-to-forced-unwrapping)를 확인해보면 한국어 번역시 '옵셔널 해제'라는 표현보다는 "옵셔널의 값에 접근한다", "옵셔널의 값의 멤버에 접근한다"라는 표현을 사용한다.
    따라서 Swift 개념을 제대로 모르는 상태에서 여러 블로그를 보면서 공부하다 보니, 2번 목차의 제목을 'Optional 해제', 'Optional 값 추출', 'Optional 바인딩' 등 어떤거로 지어야할지 매우 고민했다. 하지만 아래 2.1~2.4 목차를 다 아우를 수 있는 제목은 'Optional의 값에 접근하기'가 제일 적절한 표현인 것 같다.

옵셔널의 값에 접근하는 방법에는 앞서 말했듯이 4가지가 있다. 크게 '명시적 해제''묵시적(암묵적) 해제' 두가지로 볼 수 있으며 어떻게 해제하는지는 아래 2.1~2.4 목차에서 자세하게 알아보겠다.

1. 명시적 해제

  • 강제 해제 - Forced Unwrapping(강제 언래핑)으로도 부른다.
    ➡️ 옵셔널을 강제로 해제한다. !를 붙였을때만 Optional()을 벗겨버리고 값을 추출한다. !가 붙여있는 부분만 값을 추출하지, ! 강제 언래핑을 한번 했다고 옵셔널이 해제된 상태를 계속 유지하는건 아니다. 값 추출이 필요할때마다 계속 !를 붙여야 한다!
  • 비강제 해제 - Optional Binding(옵셔널 바인딩)으로 불린다.
    ➡️ 옵셔널을 !를 사용하지 않고 한번 바인딩 구문을 거치면 비강제적으로 해제해서 옵셔널이 해제된 상태를 유지시켜 준다!

2. 묵시적(암묵적) 해제

  • 컴파일러에 의한 자동 해제
    ➡️ Swift 코드가 컴파일 되면서 옵셔널이 암묵적으로 비교구문에서만 컴파일러가 잠깐 옵셔널을 해제하고 값에 접근한다. 비교 구문이 끝나면 다시 Optional()로 래핑되어 있다!
  • ! 연산자를 이용한 자동 해제 - Implicitly Unwrapped Optional(암묵적으로 벗겨진 옵셔널)이라고도 부른다.
    ➡️ 옵셔널이 Optional 타입이 아닌 일반 변수와 연산시에만 해제된 상태에서 연산되고, 해당 구문이 끝나면 다시 Optional()로 래핑되어 있다!

🚨 이 4가지 방법들 중에 언래핑이 명시적으로나 암묵적으로나 옵셔널 해제가 시도되었을 때, 오직 '비강제 해제 - Optional Binding(옵셔널 바인딩)' 방법만 옵셔널이 해제되어서 옵셔널이 벗겨져 있는 상태를 계속 유지한다.

🙋 묵시적해제 방법을 공부하면서 헷갈려 찾아보았던 점 하나 더 공유한다.
옵셔널 해제를 검색해보면 명시적 해제(강제 언래핑, 옵셔널 바인딩)가 아닌 방법에 대해 '묵시적 해제', '암시적 해제', '암묵적 해제'의 표현을 혼용해서 설명한다. Swift원서에서 "implicit하게 옵셔널을 해제한다"라는 말이 한국어로 번역되면서 이러한 표현들로 번역된거 같다.
블로그마다 용어가 통일되지 않게 설명되고 있다보니 Swift 초보자인 나에게는 '암시적', '묵시적', '암묵적'이라는 단어의 뜻이 헷갈려 헷갈려 ‘국립국어원’의 우리말 풀이도 찾아보았다.

🙋🏼 결론부터 말하자면 '명시적'의 반대말은 '암시적'이고, 암시적의 유의어로 '암묵적', '묵시적'이 있기 때문에 '명시적', '암시적', '암묵적' 세가지 모두 비슷한 맥락의 말이다.

묵시적(암묵적) 해제

  • 컴파일러에 의한 자동 해제
  • !연산자를 이용한 자동 해제

많은 블로그에서 위의 두가지 옵셔널 해제 방법에 대해 '묵시적 해제'라고 표현을 하고 있지만, "암묵적으로 옵셔널을 자동 해제해서 사용한다."라고 해석해서 '암묵적 해제'가 더 표현이 와닿는거 같기도 하다. (우리가 평소에 '암묵적 동의'라는 말을 많이 쓰듯이..)

아무튼 Swift 초보자 입장에서는 옵셔널을 해제하는 방법에 대해서 블로그마다 용어가 통일되지 않아, 헷갈렸기 때문에 이 포스팅에서 한번 집고 넘어가본다.

1. 강제 해제 - Forced Unwrapping(강제 언래핑)

강제 해제는 'Force Unwrapping(강제 언래핑)'으로도 불린다.

강제 언래핑(Force Unwrapping)은 옵셔널을 해제하고 싶은 변수 뒤에 '!'를 붙여 'Optional()' 껍데기를 강제로 제거하는 방식이다.

옵셔널 변수인 hobby1에 !를 붙여 출력하니 'Optional()' 없이 값에 해당되는 문자열 "travel"만 출력되는 것을 볼 수 있다.

🙋 참고로 hobby1 변수는 print문에서 !을 만나면서 옵셔널이 잠깐 언래핑(해제)된것이지, 다시 ! 없이 출력하면 Optional()이 감싼채로 그대로 출력된다.

옵셔널 해제 방법 2.1~2.4 중에 어찌보면 제일 간단한 방법이라고 할 수 있다.

🚨 하지만 주의할 점이 있다.

강제 언래핑하고 싶은변수의 값이 만약 재수없게 nil인 경우가 발생하면, crash(프로그램이 이유 없이 종료되는 현상)가 발생한다.

실행이 중지되었다는 내용의 error 메시지가 발생하고, 플레이그라운드 창 위에는 "The playground encountered a crash and could not finish executing" 플레이그라운드가 크래쉬가 발생하여 실행을 마칠 수 없다는 내용의 경고 메시지가 뜨는 것을 볼 수 있다.

1.1 nil coalescing(닐 병합) 연산자

강제 언래핑 방법으로 인한 crash 발생을 피하기 위해서 nil 값 여부를 체크할 수 있는 nil coalescing(nil 병합) 연산자인 '??'를 사용할 수 있다.

사용법은 변수 ?? 값으로 작성한다. nil 타입 변수가 nil일 경우 nil 병합 연산자인 '??' 뒤의 값을 갖게 된다.

hobby2의 값이 초기화 되지 않아 nil 값을 갖게 되므로 닐 병합 연산자 뒤의 값으로 출력함을 알 수 있다.

🙋🏼 여기서 드는 의문점이 있다. 그럼 hobby2는 ?? 연산자 뒤의 "hobby2는 nil 값임"으로 초기화 됬을까?

그래서 Hobby2를 nil 병합 연산자를 사용 후 다시 출력해보았다.

정답은 hobby2는 여전히 nil값이라는 것이다.
여기서 기억해야할 점은 nil병합 연산자를 사용한다고 옵셔널 타입 변수가 ?? 뒤의 값으로 초기화되지는 않는다.

강제 언래핑을 통해 옵셔널 타입 변수의 값에 접근하고자 할 때 nil coalescing(닐 병합) 연산자를 사용하여 nil값 체크를 함으로써 crash를 피할 수 있다. 하지만 옵셔널의 값에 접근할 때 명시적 해제 방법 중 '2.1 강제 해제' 방법보다는 '2.2. 비강제 해제' 방법을 사용하는 것이 바람직하다. 비강제 해제 방법이 어떤건지는 아래 2.2 목차에서 자세히 살펴보자!

2. 비강제 해제 - Optional Binding(옵셔널 바인딩)

'2.1. 강제 해제' 방법보다 안전하게 옵셔널 값에 접근하는 방법이다. 대표적으로 아래 2가지 if let과 guard let 선언을 통해 접근한다. (두 구문 모두 let 말고 var로도 선언 가능하다.)

  • if let
  • guard let

두 구문을 어떻게 사용하는지는 각각 코드 예제를 통해 확인해보자.

2.1. if let

옵셔널의 값이 존재하는지를 검사한 뒤, 존재하면 그 값을 다른 변수에 대입시
킨다. 값이 없다면 else 문을 아무처리 하지 않고 비워둬도 되고(보통은 에외처리를 함) print문을 찍어 예외처리를 해도 된다.

optionalVar 변수가 문자열로 초기화되어 있기 때문에, ifLetTest 변수에 대입시 에러가 발생하지 않는다.

따라서 옵셔널 변수로 선언되었던 optionalVar를 출력해보면 "Optional("just do the g")"로 출력된다.

하지만 옵셔널 바인딩으로 새로운 초기화된 변수 ifLetTest를 출력해보면 Optional이 해제된 상태로 "just do the g" 값만 출력된다.

왜 그럴까?

'option키 + 마우스 클릭'하여 옵셔널 바인딩된 ifLetTest의 자료형을 보면 optionalVar의 자료형처럼 Stting?이 아닌, 옵셔널이 해제된 일반 String 타입이다.

! 키워드로 강제 언래핑을 하지 않았는데도, if let 구문을 거쳐 바인딩 된 새로운 변수는 비강제적으로 옵셔널이 해제된것이다!

2.2. guard let

if let과 원리가 거의 비슷하다. 바인딩을 통하여 nil여부를 확인한다.
아래 코드를 같이 봐보자.

좀 더 자세히 설명하자면 guard의 조건(특정 상수 또는 변수에 옵셔널 변수를 대입했을 때, nil 값이 아니어서 에러가 발생하지 않음)을 만족하지 못하면 else 문의 코드블럭의 return을 실행하여 guard let이 감싸는 실행중인 함수를 종료 시킨다.

결국 guard let은 특정 함수의 메인 코드를 본격적으로 실행하기 전에 함수가 종료되어야 할 케이스들에 대한 '방어벽 역할'을 하는것이다.

whoAreU는 어떠한 값으로도 초기화되지 않았기 때문에 nil값을 가진다. 이로써 setName 함수에 nil 값인 whoAreU가 던져졌을 때 guard let문을 만나 예외처리 print문을 출력하고 setName 함수가 종료된다.

test 상수에 nil값인 인자로 name을 대입하면서 else문으로 분기되어 "name은 nil값 입니다."를 출력하는것이다.


nil이 아닌 문자열로 whoAreU 옵셔널 변수를 초기화시켜주면 어떻게 될까?

print(name)
print(guardLetTest)

👇 출력 결과 👇

setName 함수 내부에서 guar let 구문에 걸리지 않고 정상적으로 진행되어 위의 print구문 두개를 만단다. 옵셔널 변수인 name을 출력하면 옵셔널에 감싸져 있는 상태로 Optional("just do the g")가 출력되고, guard let 바인딩 구문을 통해 옵셔널이 해제된 guardLetTest 변수는 "just do the g" 값만 출력한다.

guardLetTest의 자료형을 'option + 마우스 클릭'하여 확인해보면 String?이 아닌 옵셔널이 해제된 String 일반 타입임을 확인할 수 있다.

2.3. if let과 guard let의 차이점

🙋 그렇다면 if let과 guard let의 차이점은 뭘까?

두 구문의 차이점을 한번 정리해보겠다.

if let과 guard let 둘다 기본적인 원리는 옵셔널이 아닌 변수에 옵셔널 변수의 값을 대입해보고 에러를 던지는지(참이 아닌 값)를 검사하는 것이다.

단, 아래 두가지가 다르다.

  • if let - 값의 유무를 확인 후, 있을 때 없을 때 각각의 코드를 실행 가능
  • guard let - 값의 유무를 확인 후, 값이 없으면 함수를 guard let을 통해 특정 상황에서 함수가 실행되지 못하도록 방어하는데 초점이 맞춰져 있다.

3. 컴파일러에 의한 자동 해제

옵셔널 타입 변수와 옵셔널이 아닌 변수 또는 상수를 ==와 !=로 비교하면 컴파일러가 자동적으로 옵셔널 타입 변수의 값을 해제 시켜 비교한다.

🙋 참고로 비교 연산자를 만나면 컴파일러가 자동으로 옵셔널을 해제해서 비교한다는 글이 많은데, 테스트해본 결과 Swift가 제공하는 비교 연산자 6가지(==, !=, <, >, <=, >=) 중 ==와 != 이 두가지만 옵셔널이 자동 해제되어 비교되고 나머지 비교 연산자들은 강제 언래핑(!)을 옵셔널 타입 변수 뒤에 붙여줘야 비교문에서 에러가 발생하지 않는다.


(예제1)

value의 타입은 정확히 말하면 Optional Int 타입의 상수이다.
따라서 원래라면 'Int 타입'인 10과 'Optional Int 타입'은 비교가 불가능하므로 아래와 같이 강제 언래핑(!)을 통해 Int 타입의 값들끼리 비교해주어야 한다. 하지만 컴파일러가 비교 연산자 ==를 만나면서 옵셔널을 자동 해제해주었다.

if value! == 10{
	...
}

물론 비교시에만 자동으로 암묵적으로 Optional을 해제해서 비교해주고, 비교가 끝난 후 출력해보면 Optional은 여전히 래핑되어있다!

옵셔널 변수가 비교구문 == 을 거치고 자료형을 확인해보면 Int?인 것을 확인할 수 있다.


예제를 하나 더 보겠다.

(예제2)

여기서 strToInt 변수의 타입은 Optional Int 타입이다.
왜일까?
Int()를 통해 형변환을 해주었는데 String에서 Int로 형변환시 실패해서 nil값인 경우도 고려하기 때문에 Int()는 nil을 혀용하는 Optional Int 자료형이 된다.

위 예제 코드의 결과를 해석해보겠다.

strToInt == 12345
strToInt == Optional(12345)
strToInt! == 12345
strToInt! == Optional(12345)

12345는 일반 Int 타입이다.
첫번째줄은 컴파일러에 의해 옵셔널 자동 해제가 되어 true이다.
두번째줄은 Optional(12345) == Optional(12345) 값도 자료형도 완전히 같은 데이터끼리 비교하므로 true이다.
세번째줄은 ! 강제 언래핑을 통해 12345 == 12345를 비교하게 되어 true이다.
네번째줄은 ! 강제 언래핑을 통해 왼쪽 변수의 값이 12345가 되면서 12345 == Optional(12345) 비교문이 되는데, 컴파일러에 의해 오른쪽 옵셔널이 언래핑되면서 true이다.

4. ! 연산자를 이용한 자동 해제 - Implicitly Unwrapped Optional(암묵적으로 벗겨진 옵셔널)

보통의 옵셔널 변수는 var 변수: 자료형? 로 선언하여 nil값을 허용한다. 하지만 옵셔널 다른 일반 변수나 상수와 연산이 필요할 때 매번 옵셔널 해제해줘야 하는 귀찮음이 있다. 따라서 이를 보완하는 아주 좋은 장치가 있다.

바로 var 변수: 자료형! 으로 선언하면 옵셔널 타입 변수처럼 nil값을 허용하되, 값을 사용할 때는 자동으로 옵셔널이 해제되어 사용 가능하다.

!로 선언된 변수는 암묵적으로 이미 언래핑된, 벗겨진, 해제된 옵셔널 변수라고 하여 Implicitly Unwrapped Optional이라고도 부른다.

(예제1)

'! 연산자를 이용한 자동 해제'를 보기 위해 ?로 선언된 옵셔널 변수의 에제를 하나 살펴보자!

아래 str 변수는 String 타입으로 선언된 문자열 1이다. 그리고 strToInt1 변수는 형변환을 통해 Optional Int 타입으로 선언되었다.

strToInt1 변수를 출력해보면 이전코드에서 값을 초기해주었기 때문에 Optional(1)이 나온다.

이 상태에서 1을 더해서 출력하면 "Value of optional type 'Int?' must be unwrapped to a value of type 'Int'"라는 에러 메시지가 발생한다. Optional Int 타입 변수는 Int로 언래핑되어야 + 1 연산을 할 수 있다는 뜻이다.

(예제2)

! 연산자를 이용해 자동 해제가 되었을 경우 같은 상황에서 어떻게 되는지 확인해보자!

이번에는 strToInt2 변수를 nil값을 허용하지만 ?이 아니라 암묵적으로 옵셔널이 벗겨지도록 !로 변수를 선언해주었다.

print(strToInt2+1)

그 결과 아래 구문에서 ?로 변수를 선언해주었던 (예제1)의 경우와 다르게 print(strToInt2+1) 구문에서 에러가 발생하지 않는다.

암묵적으로 옵셔널이 해제된 변수로 선언되어 print문에서 strToInt2!로 강제 언래핑을 안 해주어도 Int + Int의 연산으로 컴파일링되어 문제가 없으므로 에러가 발생하지 않은 것이다.

포스팅을 마지막으로 마치기 전에 퀴즈가 있다.

print(strToInt2)
print(strToInt2+1)
print(strToInt2!)

! 연산자로 strToInt2를 선언 후 이 3가지를 순서대로 출력하면 어떻게 될까?

...

이게 정답이다.

포스팅을 끝내기 전에 전달하고자 하는 바는 !연산자로 선언해주었지만 옵셔널이 아닌 일반 변수나 상수와 연산시에만 암묵적으로 그리고 일시적으로 옵셔널을 해제해주는 것이지 연산이 끝나면 옵셔널이 해제되어 있지 않는 것은 그대로다.

즉 옵셔널이 해제되어 있지 않은 값이 첫번째 print문에서 출력되는 것이다.


이어서 옵셔널 체이닝(Optional Chaining)을 사용하는 법에대해서는 "옵셔널(Optional)을 파헤쳐보자 3편"에서 자세히 다루도록 하겠다!!

👇3편 click👇
https://velog.io/@justdotheg/Swift-%EA%B8%B0%EC%B4%88-%EC%98%B5%EC%85%94%EB%84%90Optional%EC%9D%84-%ED%8C%8C%ED%97%A4%EC%B3%90%EB%B3%B4%EC%9E%90-3%ED%8E%B8

0개의 댓글