Kotlin in action 2/e: 널이 될 수 있는 값(1): nullability, safe call(?.) elvis operator(?:), safe cast(as?)

2026. 2. 16. 03:03·Kotlin

NullPointerException

NullPointerException, NPE는 null인 객체를 사용하려고 할 때 발생하는 런타임 예외이다. 

 

Nullability, 널 가능성, 런타임 예외를 컴파일 시점에 검사하도록 변경

이러한 NPE 문제를 해결하기 위해 컴파일 시점에 널 가능성을 검사하는 것으로 해결하려고 한다고 책에서 설명하고 있다.

이를 위해 널이 될 수 있는지 여부를 타입 시스템에 추가했다고 할 수 있겠다. 

 

널이 될 수 있는 타입을 명시적으로 지원

자바와 코틀린의 nullable에 대한 차이는 단순한 지원과 명시적인 지원이라는 점이다. 조금 다르게 설명하면 자바에서는 다음과 같은 코드를 컴파일 할 수 있다. 

int strLen(String s) {
    return s.length();
}

 

하지만 이 함수는 s가 null이면 NPE가 발생할 것이다. 하지만 코틀린은 애초에 이에 대한 가능성을 좁히고 시작한다. 

fun strLen(s: String) = s.length

 

s는 String 타입이므로, null을 넘겨주는 코드가 있다면 컴파일 시 오류가 발생한다. 때문에 NPE가 발생할 가능성 자체가 사라진다. 만약 s에게 null이 넘겨질 가능성을 추가해주고 싶다면 물음표(?)를 붙여 명시해줄 수 있다. 

fun strLenSafe(s:String) = ...

 

사실 위 코드를 보고 처음 문제가 발생한 자바 코드처럼 결국 null이 넘겨지면 NPE가 발생할 수 있지 않겠는가 생각할 수 있다. 

 

nullable에 대해 연산 종류 제한

하지만 코틀린에서는 nullable에 대해 연산 종류를 제한한다. 다음과 같이 오류가 발생하는 것을 확인할 수 있다. 

 

비슷한 맥락으로 널이 될 수 있는 값을 널이 될 수 없는 타입의 변수에 대입할 수 없고, 파라미터에도 전달할 수 없다. 그렇다면 이를 어떻게 해결할 수 있을까?

 

if 검사를 통해 null 아님 알리기

fun strLenSafe(s:String?): Int =
    if (s != null) s.length else 0

 

다음과 같이 null 검사를 하는 코드를 도입하기만 해도, 컴파일러는 if문 내의 블럭의 s가 null이 아님을 인식하고 널이 아닌 타입의 값처럼 사용할 수 있도록 해준다. 

물론 이 방법은 직관적이긴 하지만, 간결하지는 않다.

 

safe call 연산자 ?.

코틀린에서는 이를 간결히 사용하기 위해 세이프 콜 연산자를 제공한다. 이는 말 그대로 메서드를 안전하게 호출해주는데, 호출하려는 값이 null이라면 메서드를 호출하지 않고 null을 반환하며, null이 아니라면 메서드를 호출한다.

fun printAllCaps(str: String?) {
    val allCaps: String? = str?.uppercase()
    println(allCaps)
}

fun main() {
    printAllCaps("abc")
    printAllCaps(null)
}

 

예를 들어 위 코드에서 str?.uppercase()의 동작은 다음과 같을 것이다. 

 

str이 null이라면 뒤의 uppercase 메서드는 호출되지 않고 null을 반환하여 allCaps는 null이 된다. 

 

str이 null이 아니라면 String이므로, uppercase()가 문제없이 수행된다. 

이는 메서드뿐만 아니라 프로퍼티를 조작할 때도 동일하게 세이프 콜 연산자를 사용할 수 있다.

 

세이프 콜을 연쇄적으로 사용하기

세이프 콜을 다음과 같이 사용해 볼수도 있을 것이다.

class Address(val streetAddress: String, val zipCode: Int,
              val city: String, val country: String)

class Company(val name: String, val address: Address?)

class Person(val name: String, val company: Company?)

fun Person.countryName(): String {
   val country = this.company?.address?.country
   return if (country != null) country else "Unknown"
}

fun main() {
    val person = Person("Dmitry", null)
    println(person.countryName())
}

 

위 코드는 어떤 사람에 대한 정보, 회사, 회사 주소 정보를 각기 다른 클래스로 표현한 상태에서 Person의  회사 주소에서 country 프로퍼티를 가져오는 코드이다. 

 

이 부분에서 세이프 콜이 연쇄적으로 사용되는 것을 확인할 수 있다. 

val country = this.company?.address?.country
return if (country != null) country else "Unknown"

 

이를 세이프 콜을 사용하지 않고 구현하려면 각 중간 객체마다 null 여부를 검사하는 코드를 추가해야 하므로 코드가 상당히 번잡해질 것이다. 

 

엘비스 연산자(?:)로 null에 대한 기본값 제공하기

엘비스 연산자는 연산자 왼쪽에 있는 값이 null이 아닐 때 연산자 오른쪽 값을 자동으로 대입해준다. 즉 null일 때의 기본값을 지정해주는 연산자이다. 

 

엘비스 연산자라고 불리는 이유는 해당 연산자를 오른쪽으로 90도 돌리면 엘비스 프레슬리라는 사람이 보인다고 해서 붙여졌다고 한다. 처음에는 이름 지을 아이디어가 정말 없었나 싶었는데, 자료를 보니 정말 똑같아서 놀랐던 기억이 있다. 

fun strLenSafe(s: String?): Int = s?.length ?: 0

 

세이프 캐스트 연산자 as?

이 책의 이전 장에서 살펴봤듯이 as는 대상 값을 as로 지정한 타입으로 바꿔준다! 그렇다면 ?가 붙은 세이프 캐스트 연산자는 당연히 다음과 같이 동작할 것을 예상해 볼 수 있다. 

 

값을 지정한 타입으로 변환할 수 있으면 그대로 변경을 진행한다.

 

값을 지정한 타입으로 변환할 수 없으면 null을 반환한다. 

 

세이프 캐스트 연산자가 null을 반환하는 것을 이용하여 이전에 세이프 콜과 엘비스 연산자를 함께 사용했던 것처럼 동일하게 엘비스 연산자를 사용하여 캐스트 되지 않았을 때 기본값을 지정해 줄 수 있을 것이다. 

class Person(val firstName: String, val lastName: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person ?: return false

        return otherPerson.firstName == firstName &&
                otherPerson.lastName == lastName
    }

    override fun hashCode(): Int =
        firstName.hashCode() * 37 + lastName.hashCode()
}

 

'Kotlin' 카테고리의 다른 글

Kotlin in action 2/e:고차함수: 람다를 파라미터와 반환값으로 사용(1) 고차함수, 스마트 스테핑, 함수 타입의 파라미터, 함수를 반환하는 함수  (1) 2026.02.18
Kotlin in action 2/e: 널이 될 수 있는 값(2): not null assertion(!!), let(), lateinit()  (0) 2026.02.16
Kotlin in action 2/e: 람다를 사용한 프로그래밍(2) 함수형 인터페이스, 수신 객체 지정 람다  (0) 2026.02.15
Kotlin in action 2/e: 람다를 사용한 프로그래밍(1) 코틀린의 람다  (0) 2026.02.14
Kotlin in action 2/e: 클래스, 객체, 인터페이스(2) 기본 구현 메서드, by, object  (0) 2026.02.13
'Kotlin' 카테고리의 다른 글
  • Kotlin in action 2/e:고차함수: 람다를 파라미터와 반환값으로 사용(1) 고차함수, 스마트 스테핑, 함수 타입의 파라미터, 함수를 반환하는 함수
  • Kotlin in action 2/e: 널이 될 수 있는 값(2): not null assertion(!!), let(), lateinit()
  • Kotlin in action 2/e: 람다를 사용한 프로그래밍(2) 함수형 인터페이스, 수신 객체 지정 람다
  • Kotlin in action 2/e: 람다를 사용한 프로그래밍(1) 코틀린의 람다
Kirbyyy
Kirbyyy
개인적인 일상과 회고를 기록하는 블로그입니다.
  • Kirbyyy
    커브볼의 생존일지
    Kirbyyy
  • 전체
    오늘
    어제
    • 분류 전체보기 (53)
      • 우아한테크코스 (8)
      • 프로덕트 빌드 (0)
      • Problem Solving (20)
      • C++ (0)
      • Kotlin (19)
      • Java (3)
      • CS (2)
        • AI (2)
      • 취미생활 (0)
        • 서평 (0)
        • 프라모델 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    ProblemSolving
    백준 1356
    그리디 알고리즘
    다이나믹 프로그래밍
    너비 우선 탐색
    백준 33272
    백준 RGB 거리
    Problem Solving
    백준 파도반 수열
    분할 정복
    백준 16174
    우테코 8기
    BFS
    백준
    백준 11123
    백준 연속 합
    백준 16173
    백준 알고리즘
    C++
    백준 31575
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Kirbyyy
Kotlin in action 2/e: 널이 될 수 있는 값(1): nullability, safe call(?.) elvis operator(?:), safe cast(as?)
상단으로

티스토리툴바