코틀린을 사용하는 가장 큰 이유 중 하나는 바로 Null Safety입니다. 자바 개발자들을 수십 년간 괴롭혀온 NullPointerException
(NPE)이라는 "10억 불짜리 실수"를 해결하기 위해, 코틀린은 과연 어떤 방식을 택했을까요?
코틀린의 철학: "세계관을 둘로 쪼개다"
코틀린의 타입 시스템은 근본적으로 두 개의 세상으로 나뉘어 있습니다.
- Null을 담을 수 있는 세상 (Nullable)
- Null을 절대 담을 수 없는 세상 (Non-Nullable)
이 두 세상은 상속 계층 구조(Hierarchy)의 최상위부터 다릅니다.
- Any: Null이 아닌 모든 타입(String, Int, User 등)의 최상위 부모입니다. 여기엔 죽어도 null이 들어갈 수 없습니다.
- Any?: Null을 포함한 모든 타입의 진짜 최상위 부모입니다.
관계상으로 String은 String?의 하위 타입(Subtype)입니다. 하위 타입은 상위 타입에 대입할 수 있지만, 반대는 성립하지 않죠. 이 간단한 원리가 컴파일 단계에서 Null 안정성을 보장합니다.
[주의사항] 자바와의 아슬아슬한 동거: 플랫폼 타입 (Platform Types)
자바 코드에서 가져온 객체는 코틀린에서 플랫폼 타입(String!, User!)이라 불리는 특별한 타입으로 취급됩니다.
- 문제점: 코틀린 컴파일러는 자바 코드를 보고 이게 Nullable인지 Non-null인지 알 수 없습니다.
- 동작 방식: "개발자가 알아서 판단해라"라며 모든 권한을 위임합니다.
- 위험성: Non-null로 가정하고 썼는데 실제 자바 코드에서 null이 넘어오면, 런타임에 NPE가 터집니다.
💡 왜 이렇게 만들었을까? 만약 자바의 모든 타입을 강제로 Nullable(ArrayList<String?>?)로 취급했다면, 자바 라이브러리를 쓸 때마다 불필요한 null 체크를 수없이 해야 했을 것입니다. 코틀린은 실용성을 위해 "엄격함" 대신 "개발자의 책임"을 선택했습니다.
[주의사항] 제네릭에서 Null 막기: Upper Bound
제네릭 <T>를 쓰면 기본적으로 T는 Any?로 추론되어 null이 들어올 수 있습니다. 확정적으로 Non-null만 받고 싶다면 상한(Upper Bound)을 지정해야 합니다.
Kotlin
// T는 Any?로 추론됨 (null 가능)
fun <T> printHashCode(t: T) { ... }
// T는 Any(Non-null)로 추론됨 (null 불가능)
fun <T: Any> printHashCode(t: T) {
println(t.hashCode())
}
코틀린에서 Nullable 타입을 효과적으로 다루기 위한 여러 기능을 제공합니다. 해당 기능들은 이글에서 다루지 않으니, 아래 docs를 참고해주세요.
- Check for null with the if conditional
- Safe call operator ?.
- Elvis operator ?:
- Not-null assertion operator !!
- Nullable receiver
- let function
- Safe casts as?
- Collections of a nullable type
테스트코드 국룰: lateinit
NullSafety Docs에 포함되어 있지는 않지만 관련이 있는 lateinit입니다. 테스트 코드나 DI(의존성 주입) 프레임워크를 사용할 때, 변수 선언 시점에 초기화하기 어려운 경우가 많습니다. 이때 lateinit var를 사용합니다.
public class OrderServiceTest {
lateinit var orderService: OrderService
@SetUp fun setup() {
orderService = OrderService()
}
@Test fun processesOrderSuccessfully() {
// Calls orderService directly without checking for null
// or initialization
orderService.processOrder()
}
}
- 핵심: Nullable(?)로 선언하면 매번 !!나 ?.를 써야 하는 번거로움을 없애줍니다.
- 주의: 초기화하지 않고 접근하면 UninitializedPropertyAccessException이 발생하여, 어디서 초기화가 누락되었는지 명확히 알 수 있습니다.
Java Optional<T> vs Kotlin Nullable Type(T?)
두 방식 모두 "값이 없을 수 있음"을 표현하지만, 내부 동작은 완전히 다릅니다.
- Java Optional (박스 모델): 값을 Optional이라는 객체(Box)로 감쌉니다.
- 비용: 래퍼 객체를 힙 메모리에 생성해야 하므로 오버헤드가 발생합니다.
- Kotlin Nullable (꼬리표 모델): 별도의 객체를 생성하지 않습니다. 컴파일 타임에 메타데이터(꼬리표)로만 존재합니다.
성능 비교 (Bytecode 레벨) 코틀린의 Nullable 코드는 컴파일되면 자바의 if (x != null)과 완전히 동일한 바이트코드로 변환됩니다.
💡 Zero Overhead 코틀린의 Nullable 타입은 런타임에 추가적인 객체 생성 비용이 "0"입니다.
Java
// Kotlin: str?.length ?: 0
// Java 변환 결과 (대략적)
if (str != null) {
return str.length();
} else {
return 0;
}
반면 Optional은 Optional.ofNullable(str)을 호출하며 객체를 생성하고, 메서드 호출을 한 단계 더 거쳐야 합니다. 코틀린은 안전함(Safety)과 성능(Performance) 두 마리 토끼를 모두 잡았습니다.
마치며
코틀린의 Nullable 시스템은 단순히 NullPointerException을 막는 도구가 아닙니다. 이는 "값이 없을 수 있다"는 사실을 타입 시스템에 명시함으로써, 코드의 의도를 명확히 하고 컴파일러의 도움을 받아 견고한 애플리케이션을 만들게 해주는 강력한 철학입니다.
'개발' 카테고리의 다른 글
| [Kotlin] 컬렉션(Collection) vs 시퀀스(Sequence): 함수형 API와 동작 원리 심층 분석 (0) | 2025.12.08 |
|---|---|
| 람다에 대해서 알아보자 - 1편 (탄생배경, 변수캡쳐, 함수타입) (0) | 2025.11.26 |
| 싱글톤 패턴 (0) | 2025.03.30 |
| Index는 왜 B-Tree를 사용하나 (0) | 2025.03.16 |
| Ajax에 대해서 알아보자 (0) | 2025.03.02 |