들어가기 전: 글을 쓰게 된 계기

마지막으로 글을 쓴 시점으로부터 1주일 반이 지났다. 내가 TIL을 쓰는 방식에 변경이 필요하다는 핑계를 대며 그 방안에 대해 생각한다고 글 쓰는 걸 차일피일 미뤘다.

오늘 일정을 소화하는 중 동기 한 명이 코드 실행에 문제를 겪어 매니저님이 상황을 다른 동기들에게도 공유해주셨다. 대충 상황을 설명하면:

  1. CourseLecturedomain1:N 관계로 묶여 있다.
  2. Course에 강의를 추가할 때(POST /courses/{courseId}/lectures) 다음과 같은 코드를 통해 강의를 추가한다:
    override fun addLecture(...): LectureResponse {
        val course = courseRepository.findByIdOrNull(courseId) ?: throw ModelNotFoundException("Course", courseId)
        val lecture = Lecture(title = request.title, videoUrl = request.videoUrl, course = course)
        course.addLecture(lecture)
        courseRepository.save(course)
        return lecture.toResponse()
    }
    Lecture.toResponse는 다음과 같이 구현되어 있다:
    fun Lecture.toResponse(): LectureResponse = {
        return LectureResponse(id = id!!, title = title, videoUrl = videoUrl)
    }
  3. lecture.toResponse()가 호출될 때 lecture.idnull인 상황이 발생하여, 안에서 NullPointerException이 발생하게 된다.

다른 동기 그리고 매니저님하고 이야기를 하다가

”그럼 Lecture.idlateinit으로 씌우면 어떨까?”

하는 제안을 내놓았으나, Long 타입의 idlateinit 속성을 붙일 수 없었다.

'lateinit' modifier is not allowed on properties of primitive types

-라는 에러와 함께 말이다. 일과 시간이 마무리되어 더 이상 볼 수 없었지만, 대신 이 부분에 대하여 새로운 걸 찾게 되었다. 하나 쓸 거리가 생긴 셈이다.

lateinit을 기본형 변수에 붙일 수 없는 이유

lateinit

보통, non-nullable 타입으로 선언된 property들은 생성자에서 초기화되어야 합니다. 하지만, 이게 불편한 경우가 종종 있습니다. 예를 들어, property들이 의존성 주입을 통해 초기화될 수도 있고, 단위 테스트의 setup method 안에서 초기화될 수도 있습니다. 이런 상황에선 생성자 속에서 non-nullable하게 초기화할 수 없지만, 클래스 속 property를 참조할 때 null 체크를 피하고 싶을 겁니다. 이런 상황에 대응할 수 있게, property에 lateinit 제어자를 씌울 수 있습니다. - Kotlin Documentation: Concepts/Classes and Objects/Properties # Late-initialized properties and variables (한글로 의역을 시도한 것으로 해석이 올바르지 않을 수 있습니다.)

간단하게 보면 “나중에 값을 주긴 할 건데 nullable하게 선언하고 싶지 않을 경우” 프로퍼티에 씌울 수 있는 제어자다. 위에 나와있는 대로, 프로퍼티에 null이 들어갈 수 없기에 null 체크를 하지 않아도 되는 편리함이 있다.

단 제어자를 붙일 수 있는 조건이 있는데, lateinit 제어자가 붙는 프로퍼티는 non-nullable해야 하며(애초에 null 체크를 하기 싫어서 붙이는 거니 그럴 이유가 없다), 기본형(primitive type)이 아니어야 한다. 이 ‘기본형이 아니어야 하는 이유’에 대해서 찾아보니, 한 Stack Overflow 질문에 대한 답에 이런 내용이 있었다:

lateinit은 기본형 혹은 nullable한 타입에 붙일 수 없습니다, lateinit이 내부적으로 null을 “초기화되지 않은” 값으로 사용하거든요. 기본형 프로퍼티는 null이 될 수 없으니, lateinit이 작동하지 않습니다.

null이 왜 거기서 나와

kotlin compiler(kotlinc)으로

private lateinit var stringLaterInitialized: String

-과 이걸 참조하는 코드를 컴파일한 걸 javap로 디어셈블하면 해당 프로퍼티를 참조할 때 다음과 같은 부분이 삽입된다:

6: ifnonnull 16
9: ldc #22
11: invokestatic #28
14: aconst_null
15: athrow

lateinit 제어자가 붙은 프로퍼티가 초기화가 되었는지를 체크하는 부분이 바이트코드에 만들어진다. 만약 초기화가 안 됐다면(6번 인덱스: non-null하면 스킵, 아니라면…), UninitializedPropertyAccessException이 발생한다(11/14/15 인덱스: 런타임 도중 예외를 던진다). 이걸 다르게 말하면, lateinit 제어자가 붙은 프로퍼티에 null을 넣을 수 없지만, 선언 시 null로 초기화하며, 초기화가 되었는지 확인하는 걸 null인지 아닌지로 확인하는 것이다. 기본형엔 null 값이 없으므로 넣을 수도 없으니, lateinit 제어자를 기본형 프로퍼티에 붙일 수 없다고 한 것이다.

참고