Java나 C++ 같은 언어를 다루다가 Swift로 넘어왔을 때 가장 혼란스럽고도 흥미로운 개념이 바로 nil과 Optional이었다. 얼핏 보면 다른 언어의 null과 비슷해 보이지만, Swift의 nil은 근본적으로 다른 철학을 가지고 있다.
다른 언어에서의 null
일반적으로 C 계열 언어에서 null은 '존재하지 않는 객체를 가리키는 포인터'를 의미한다. 메모리 상의 주소값이 0(또는 무효한 값)인 상태다. 따라서 특정 객체에 접근하려 할 때 그 참조 변수가 null이라면, 런타임 시점에 Null Pointer Exception(NPE)이나 세그멘테이션 폴트(Segmentation Fault)가 발생하며 프로그램이 비정상적으로 종료된다.
이러한 오류는 컴파일 시점에 발견하기 어렵기 때문에, 개발자는 방어 코드를 습관적으로 작성해야 하는 부담을 안게 된다.
Swift에서의 nil
반면 Swift의 nil은 포인터가 아니다. Swift 공식 문서에 따르면 nil은 '값의 부재(absence of a value)'를 나타내는 특수한 값이다. 참조 타입(Reference Type)뿐만 아니라 정수, 구조체 같은 값 타입(Value Type)에도 nil 개념을 적용할 수 있다.
하지만 Swift의 일반적인 자료형은 기본적으로 nil을 허용하지 않는다.
var serverResponseCode: Int = 404
// serverResponseCode = nil // 컴파일 에러 발생Swift 컴파일러는 Int 타입 변수에 nil이 들어가는 것을 원천적으로 차단한다. 값이 없을 수도 있는 상황을 표현하기 위해서는 반드시 옵셔널(Optional)이라는 특별한 타입 시스템을 사용해야 한다.
옵셔널 (Optional)
옵셔널은 말 그대로 '값이 있을 수도 있고, 없을 수도(nil) 있음'을 포장(Wrapping)하고 있는 타입이다. 타입 선언 뒤에 물음표(?)를 붙여 사용한다.
var serverResponseCode: Int? = 404
serverResponseCode = nil // 가능내부적으로 옵셔널은 제네릭 열거형(enum)으로 구현되어 있다. 값이 있는 경우(case some(Wrapped))와 없는 경우(case none) 두 가지 상태를 가진다. 즉, Swift에서 nil을 할당한다는 것은 포인터를 비우는 것이 아니라, 변수의 상태를 Optional.none으로 변경하는 안전한 과정이다.
옵셔널 바인딩 (Optional Binding)
옵셔널로 감싸진 값은 직접 연산에 사용할 수 없다. 상자 안에 들어있는 값을 안전하게 꺼내는 과정, 즉 언래핑(Unwrapping)이 필요하다. 가장 보편적이고 안전한 방법은 if let을 사용하는 옵셔널 바인딩이다.
var optionalName: String? = "John Appleseed"
// optionalName이 nil이 아닌지 확인하고, 값이 있다면 name 상수에 할당한다.
if let name = optionalName {
print("Hello, \(name)") // name은 이 블록 안에서 일반 String 타입으로 사용된다.
} else {
print("Name is nil")
}이 방식은 값이 nil인지 검사하는 것과 동시에, 안전한 임시 상수에 값을 추출해 준다. 만약 optionalName이 nil이라면 if 블록은 아예 실행되지 않으므로 런타임 오류를 걱정할 필요가 없다.
강제 언래핑 (Forced Unwrapping)
물론 느낌표(!)를 사용하여 강제로 값을 꺼낼 수도 있다.
let range: Int? = 10
print(range!) // 10 출력하지만 만약 값이 nil인 상태에서 강제 언래핑을 시도하면 앱은 즉시 충돌(Crash)하며 종료된다. 이는 Swift가 추구하는 안전성에 위배되므로, 확실한 경우가 아니라면 지양해야 한다.
결국 Swift의 옵셔널 시스템은 개발자가 '값이 없는 상황'을 코드 작성 시점에 명시적으로 고려하도록 강제한다. 이는 귀찮은 제약이 아니라, 런타임 오류를 컴파일 타임에 미리 잡아내어 견고한 앱을 만들게 돕는 핵심 기능이다.