Kotlin

[Kotlin] Kotlin의 Null 안전성 완전 정리: ?, !!, ?: 그리고 Java Optional과의 차이

태오님 2025. 9. 3.

코틀린의 ?, !!, ?:는 왜 필요할까?

자바와 비교해 보는 Null 안정성 설계


1. 코틀린의 ?, !!, ?: 연산자는 무엇이고 왜 쓰는가?

코틀린은 NullPointerException(NPE)을 방지하기 위해 설계 단계에서부터 null 처리를 강제한다.
이를 위해 대표적으로 다음 세 가지 연산자를 제공한다.

  • ? : null이 될 수 있는 타입을 선언할 때 사용한다. 예: String?
  • !! : null이 아님을 단언할 때 사용한다. null이면 런타임 예외가 발생한다.
  • ?: : null일 경우 대체 값을 제공할 때 사용한다. 엘비스 연산자라고도 불린다.

2. 실제 예시로 이해하기

var name: String? = null
println(name?.length)      // null 안전 접근 → 출력: null

val length = name?.length ?: 0  // null이면 0 반환

val error = name!!.length  // null 단언 → NullPointerException 발생

3. 자바에서는 어떻게 처리할까?

자바는 모든 참조형 변수가 null이 될 수 있다. 그래서 다음과 같은 코드가 런타임 오류를 발생시킨다.

String name = null;
int len = name.length(); // NullPointerException 발생

자바 8 이후 Optional이 등장했지만, 여전히 코드가 길고 사용이 제한적이라는 평가도 있다.


4. 자바 스타일을 코틀린에서는 어떻게 표현할까?

예시로 비교해보자.

자바:

Optional.ofNullable(name).orElse("Guest");

코틀린:

name ?: "Guest"

또 다른 예시:

자바:

if (user != null && user.getName() != null) {
    return user.getName();
} else {
    return "Guest";
}

코틀린:

return user?.name ?: "Guest"

코틀린은 null-safe 연산자를 통해 더 짧고 읽기 쉬운 코드를 작성할 수 있다.


5. 스프링에서의 적용 사례

요청 파라미터 처리

자바:

@GetMapping("/greet")
public String greet(@RequestParam(required = false) String name) {
    return name != null ? name : "Guest";
}

코틀린:

@GetMapping("/greet")
fun greet(@RequestParam name: String?): String {
    return name ?: "Guest"
}

엔티티에서 null 허용 필드

@Entity
class Member(
    @Column(nullable = false)
    val name: String,

    val nickname: String? // null 허용 필드
)

Kotlin에서는 타입 수준에서 null 가능성을 명시하므로 의도를 더 명확하게 표현할 수 있다.


6. 서비스 계층에서 Optional 처리 방식

자바에서는 Optional을 그대로 반환하거나, map/ifPresent 등으로 처리한다.

public Optional<User> findById(Long id) {
    return userRepository.findById(id);
}
public ResponseEntity<UserDto> getUser(Long id) {
    return userService.findById(id)
        .map(user -> ResponseEntity.ok(new UserDto(user)))
        .orElse(ResponseEntity.notFound().build());
}

코틀린에서는 nullable 타입을 사용하고 let, ?: 등의 연산자로 처리한다.

fun findById(id: Long): User? {
    return userRepository.findById(id).orElse(null)
}
fun getUser(id: Long): ResponseEntity<UserDto> {
    val user = userService.findById(id)
    return user?.let { ResponseEntity.ok(UserDto(it)) }
        ?: ResponseEntity.notFound().build()
}

7. 예외를 안전하게 처리하는 방식

코틀린에서는 예외 발생 가능성이 있는 코드를 다음처럼 처리할 수 있다.

fun findUserName(id: Long): String {
    return runCatching {
        userRepository.findById(id).orElseThrow().name
    }.getOrElse { "UNKNOWN" }
}

예외를 try-catch로 감싸는 대신 runCatching 블록을 통해 처리하면 코드가 깔끔해지고, 테스트도 쉬워진다.


8. 요약

  • 코틀린은 null 안정성을 언어 차원에서 제공하며, ?, !!, ?: 등의 연산자로 이를 처리한다.
  • 자바의 Optional은 코틀린에서는 nullable 타입과 엘비스 연산자로 자연스럽게 대체된다.
  • 컨트롤러나 서비스 계층에서도 ?.let, ?:, runCatching 등을 통해 안정적인 코드를 작성할 수 있다.

참고자료

'Kotlin' 카테고리의 다른 글

[Kotlin] lateinit vs lazy 정리  (0) 2025.05.12
[Kotlin] for문 안에서 인덱스 조정 에러, 해결법  (0) 2023.03.22

댓글