Swift Optional (feat. dart Null-Safety)

JJUDEV·2023년 6월 21일
2

Swift

목록 보기
6/16

"Swift의 꽃!"이 뭔가요? 라고 물으면 단연 Optional일 거에요. (두가지를 꼽자면 Optional과 Closure라고 말할 수 있습니다.)
그만큼 Swift에서 중요한 개념이고, Objective-C나 다른 언어를 사용하다 Swift를 접하면 가장 난해한 개념이라고 합니다.
저는 겉핥기 말고 제대로 공부하고 사용하는 게 Swift/Dart여서 사실 특별함을 못 느끼긴 했지만요.

1. Optional이란?

Swift 코드에서 변수의 타입 뒤에 물음표(?)를 붙여 선언한 것을 보신 적이 있다면 이것이 Optional Type입니다.

let nilUserName: String?

Optional은 물음표(?)를 붙여 선언하는데, 변수명에서 유추되듯이 nil 값을 가질 수 있는 Type입니다. 위 코드에서 nilUserName은 nil 또는 String 값을 가지게 되죠. nil이란 값이 없음을 나타내는 의미로 사용되며 보통 다른 언어에서는 null로 표현됩니다.

2. 그럼 언제, 왜 Optional을 사용할까?

  • 언제?
    변수나 프로퍼티, 객체가 값이 없을 수 있는 경우(nil)에 사용합니다. nil이란 값이 없음을 나타내는 상태입니다.(타 언어의 null)
  • 왜?
    iOS에선 nil인 변수나 객체에 접근을 하게 되면 예외(Exception)가 발생하고, 이로 인해 앱이 충돌(Crash)하게 될 수 있습니다. 앱이 충돌하는 것을 방지하기 위해서는 nil 접근을 방어하기 위한 조치를 취해야 합니다. 이를 위해서 옵셔널(Optional) 타입을 사용하는 것입니다. Optional 타입에 접근하기 위해서는 Unwrapping의 과정이 필요하기 때문이죠.

3. Upwrapping Optional Types

옵셔널 타입을 언래핑하는 방법에는 여러가지가 있습니다.

1) 강제적 방법 (Forced Unwrapping)

옵셔널 타입 변수에 접근할 때 "!"를 붙여 optional을 해제하는 방법입니다.
하지만 값이 nil일 때 강제 해제하면 Fatal Error가 발생하게 됩니다. 옵셔널 타입의 값에 "!"를 붙이면 컴파일러는 해당 값이 nil이 아닐 것이라고 가정하고 컴파일을 진행하기 때문에 컴파일 에러는 발생하지 않지만 런타임 에러가 발생할 수 있습니다(nil일 경우에). 따라서 개발자가 인지하지 못한 에러가 발생하여 애플리케이션이 중단되고 프로그램 실행이 멈출 수 있기때문에 강제로 Unwrapping하는 방법은 지양해야합니다.

var optionalVar: String?
let unwrappedVar = optionalVar!

위 코드를 실행했을 때, 정상적으로 컴파일되지만 nil인 값에 접근했기 때문에 런타임 중 아래와 같이 Fatal error가 발생합니다.

2) 옵셔널 바인딩 (Optional Binding)

옵셔널 바인딩은 1번의 강제적 방법은 위험하기 때문에 권장하는 방법 중 하나입니다. 옵셔널 바인딩은 옵셔널 값이 nil인지 확인하고, 값이 존재하는 경우에만 변수에 할당하는 기능입니다.
if let 또는 guard let 구문을 사용하여 옵셔널 값을 Unwrapping하고, 변수에 할당합니다. if let과 guard let을 사용한 옵셔널 바인딩 예제를 하나씩 들어보겠습니다.

  • if let 사용
func bindingSample() {
	let num: Int? = 5
	if let result = num {
    	print(result)
    } else { return }
}
  • guard let 사용
func bindingSample2() {
	let num: Int? = 5
    guard let result = num else { return }
    print(result)
}

둘의 차이점을 아시겠나요!?
둘 모두 옵셔널 값을 언래핑하여 변수에 할당하지만 변수를 사용하는 위치가 다르죠?
if let을 사용하면 코드 블록 내에서 optional을 추출한 값을 사용 가능하며 guard let을 사용하면 함수 전체에서 값을 사용 가능합니다.

3) 옵셔널 체이닝 (Optional Chaining)

옵셔널 체이닝은 옵셔널 값에 대해 메서드 호출, 프로퍼티 접근을 안전하게 수행하는 방법입니다. 옵셔널 체이닝은 "?." 연산자를 사용하여 옵셔널 값의 프로퍼티에 접근하고, 중간에 있는 어느 부분이라도 nil이면 nil을 반환합니다. 아래 예시로 설명해보겠습니다.

class Address {
	var street: String?
    var city: String?
}

class Person {
	var name: String?
    var address: Address?
}

let person: Person? = Person()
person?.address?.city = "Seoul" // 옵셔널 체이닝

// 옵셔널 체이닝 + 옵셔널 바인딩
// 옵셔널 체이닝 통해 person의 address 프로퍼티에 접근, 다시 address의 city 프로퍼티에 접근
// 옵셔널 바인딩 사용해 person?.address?.city 값 추출하여 nil이 아닐 경우 personcCity 상수에 바인딩
if let personCity = person?.address?.city {
	print("Person's city: \(personCity)")
} else {
	print("City is nil.")
}

person?.address?.city = "Seoul" 구문에서 "?." 연산자를 통해 person의 address, 그리고 address의 city 프로퍼티에 접근해 "Seoul"이라는 값을 할당합니다. 또한 if let/guard let 구문을 사용해 옵셔널 체이닝과 옵셔널 바인딩을 함께 사용할 수도 있습니다.

4) nil 병합 연산자 (Nil-Coalescing)

옵셔널 체이닝과 옵셔널 바인딩 외에 옵셔널 값에 기본 값을 제공하는 방법이 있습니다. nil-coalescing 연산자인 "??"를 사용하여 옵셔널 값이 nil인 경우 대체 값을 사용할 수 있도록 해주는 것입니다.

var optionalVar: String?
let defaultVar: String = "DefaultValue"

print(optionalVar) // nil
print(optionalVar ?? defaultVar) // DefaultValue

optionalVar = optionalVar ?? defaultVar
print(optionalVar) // Optional("DefaultValue")

5) nil 체크..

물론 Swift에서 제공하는 옵셔널 체이닝, 옵셔널 바인딩 외에 수동으로 nil 체크를 할 수도 있긴 합니다. 물론 권장하는 방법은 아니지만요. Swift는 안정성과 가독성을 높이기 위해 옵셔널 바인딩과 옵셔널 체이닝 기능을 제공하기 때문입니다.

var optionalVar: String?

if optionalVar != nil {
	// 수행할 로직
    print(optionalVar)
} else {
	print("optionalVar is nil")
}

//optionalVar is nil

4. dart의 Null Safety

dart에서도 Swift의 optional과 비슷한 개념이 있습니다. "nullable" 이름에서 알 수 있듯이 변수가 null일 수 있는지 여부를 지정하는 것입니다. dart에서도 Swift와 같이 변수의 타입 뒤에 물음표 "?"를 붙여 nullable로 표시합니다. dart 2.12 버전에서 지원하게 된 기능으로 이전 버전에서는 "?"를 사용해 nullable 변수를 선언할 수 없었으며 모든 변수가 기본적으로 nullable이었고 Null Pointer Exception을 방지하기 위해 개발자가 수동으로 null 체크를 해주어야 했기 때문에 실수할 가능성이 높았습니다. 이를 해결하기 위해 dart 2.12 버전에서 도입된 것이 null safety입니다. dart에서 nullable 변수를 다루는 것을 살펴봅시다.

1) non-null 단언 연산자 (non-null assertion)

non-null assertion operator인 "!"를 사용하여 해당 변수가 null이 아니라고 단언하여 컴파일러에게 전달하는 것입니다. Swift의 Forced Unwrapping과 유사한 개념입니다.

String? nullableStr = "non-null";
String nonNullStr = nullableStr!;

하지만 optional 강제 해제 방법과 마찬가지로 non-null 연산자를 사용한 변수가 null일 경우 런타임 시에 NullPointerException이 발생할 수 있으므로 해당 변수가 null이 아님을 확신할 수 있을 때 사용해야 합니다. 그렇지 않을 경우에는 옵셔널 체이닝 "?."과 null-aware 연산자인 "??"를 사용하여 nullable 변수를 처리해야 합니다.

2) Conditional Property Access Operator

"?." 연산자를 사용하여 객체의 프로퍼티나 메서드에 접근하는 방법입니다. Swift에서 Optional Chaining과 마찬가지로 중첩된 nullable 객체에서 값을 추출하거나 메서드를 호출할 때 유용합니다.

class Person {
	String? name;
    Address? address;
}

class Address {
	String? city;
    String? street;
}

void main() {
	Person? person = Person();
    
    // nullable 변수에 안전하게 접근
    String? cityName = person?.address?.city;  
    print(cityName); // null
}

3) Null 병합 연산자 (Null-Coalescing Operator)

"??" 연산자를 사용하여 null일 경우 디폴트 값을 제공해주는 것입니다.

String? nullableStr;

void nullExample() {
	String? name;
	String nonNullName = name ?? 'Jjudev'; // name이 null이기 때문에 'Jjudev' 할당  	print(nonNullName); // Jjudev

	String? nullableStr;
    nullableStr ??= 'DefaultValue' // nullableStr이 null이기 때문에 'DefaultValue' 할당
    print(nullableStr); // DefaultValue
}

4) null 체크..

nullable 변수가 null인지 체크 후 처리를 하는 방식입니다.

void nullExample() {
	String? name;
    var nonNullName;
    
    if (name != null) {
    	nonNullName = name;
    } 
    else {
    	nonNullName = 'defaultName';
    }
    print(nonNullName); // defaultName
}

이렇게, Swift의 optional과 dart의 null-safety(nullable)를 살펴보았습니다. 둘 모두 nil/null로 인한 런타임 시 개발자가 예상하지 못한 에러가 발생하는 것을 방지하기 위한 기능이며 유사점이 많습니다.
이만 포스팅 마치겠습니다 :)

참고자료

profile
4년차 앱개발자입니다.

0개의 댓글