@JvmInline은 자바 어노테이션이 아니라 코틀린 어노테이션이라는 것입니다.
하지만 JVM 관점에서 어떻게 동작하는지가 핵심이므로, 요청하신 대로 JVM 내부 동작과 실무적인 관점에서 깊이 있게 복습하고자 작성하였습니다.


1. @JvmInline의 역할과 Value Class

@JvmInline은 코틀린의 value class와 함께 사용됩니다.
이 녀석의 주 목적은 "객체로서의 안전성과 타입 가독성을 챙기되, 성능은 원시 타입처럼 유지하자"는 것입니다.

  • 역할: 컴파일 타임에는 새로운 타입을 정의한 것처럼 취급하지만,
    JVM에서는 해당 객체를 생성하지 않고 내부의 실제 값으로 대체합니다.
  • 왜 쓰나요? 예를 들어 UserId(val value: Long)라는 객체를 만들면 원래는 힙메모리에 UserId 객체가 생성되어야 합니다.
    하지만 @JvmInline을 쓰면 JVM 레벨에서는 그냥 long으로 돌아갑니다.

2. Value Class vs Data Class

두 클래스는 용도가 완전히 다릅니다.

구분Data ClassValue Class (@JvmInline)
속성 개수여러 개 가능반드시 1개만 가능
메모리힙에 객체 생성대부분의 경우 원시 값으로 인라이닝
동등성모든 속성 비교 equals내부 값 비교
정체성(Identity)참조 비교 (===) 가능참조 비교 불가능 (Identity가 없음)
주요 용도복합적인 데이터 묶음 전달특정 타입의 의미 부여

3. 왜 Nullable일 때 박싱이 될까요?

결론부터 말씀드리면 "JVM의 원시 타입은 null을 가질 수 없기 때문"입니다.

JVM의 한계

JVM에서 int, long, double 같은 원시 타입은 메모리에 직접 값이 들어가며 null이라는 상태가 존재하지 않습니다.
null은 오직 객체 참조 타입만 가질 수 있는 상태입니다.

박싱이 일어나는 과정

  1. value class Password가 있다고 가정해 봅시다.
  2. 이것을 Password? Nullable로 선언하면, 컴파일러는 고민에 빠집니다.
  3. null을 표현해야 하는데, 인라이닝된 String 자체가 null일 수도 있지만, 'Password 객체 자체가 null'인 상태와 'Password 내부의 String이 null'인 상태를 구분해야 할 수도 있고, 혹은 원시 타입을 감싼 경우엔 더 심각해집니다.
  4. 결국 JVM은 null 상태를 표현하기 위해 실제 객체를 만들어서 힙에 올리고 그 참조값을 넘깁니다. 이것이 바로 박싱입니다.

쉽게 말해: " '없음'을 표현하고 싶어?
그럼 어쩔 수 없네. 진짜 객체 상자에 담아서줄게.
상자 자체가 없으면 null인 걸로 치자."
라고 동작하는 것입니다.


4. 실무에서 쓰다 망하는 케이스

효율적이랍시고 썼다가 오히려 뒤통수를 맞는 경우들입니다.

① 제네릭과 컬렉션 사용 시의 성능 배신

List<MyValueClass> 처럼 제네릭에 넣는 순간, 자바 제네릭의 특성상 내부 값들은 전부 박싱됩니다.
100만 개의 데이터를 메모리 아끼려고 value class로 만들었는데, 리스트에 담는 순간 100만 개의 객체가 생성되어 메모리 점유율이 폭발할 수 있습니다.

② 인터페이스 구현을 통한 추상화

value class가 인터페이스를 구현할 수 있습니다.
하지만 이 인터페이스 타입으로 변수를 선언하고 값을 할당하면, 역시 박싱이 발생합니다.
런타임에 다형성을 구현하려면 결국 객체 형태가 필요하기 때문입니다.

③ Java와의 상호운용성

자바 코드에서 코틀린의 value class를 호출하면 인라이닝이 되지 않고 객체로 취급되거나, 컴파일러가 생성한 특수한 이름 때문에 호출하기가 매우 까다롭습니다.
자바-코틀린 혼용 프로젝트라면 주의해야 합니다.

④ identity가 필요한 곳에 사용

value class는 "값" 그 자체입니다.
만약 객체의 생명주기나 메모리 주소가 중요한 로직
예: synchronized 블록의 락 객체로 사용 등에 사용하면 예측 불가능한 버그가 발생합니다. 실제로 코틀린은 value class에 대한 === 연산을 금지합니다.


요약하자면

도메인의 의미와 JVM의 효율 사이에서

그동안 객체를 정의할 때 습관적으로 data class만 사용해 왔는데, 오늘 @JvmInline value class를 배우며 '의미 있는 포장''성능'이라는 두 마리 토끼를 잡는 법을 깨달았습니다.

가장 인상 깊었던 점은 Email이나 Money 같은 단일 값들을 객체로 감싸면서도, 런타임에서는 원시 타입처럼 동작하게 하여 메모리 오버헤드를 줄일 수 있다는 점이었습니다.
하지만 JVM의 한계를 이해하는 것이 무엇보다 중요하다는 사실도 배웠습니다.
편리하다고 Nullable이나 제네릭을 남용하면, 결국 JVM은 값을 다시 하여
힙 메모리로 보낼 수밖에 없다는 지점에서 성능 최적화는 공짜가 아님을 실감했습니다.

이제는 단순히 데이터를 묶을 때는 data class를, 도메인에 명확한 타입을 부여하고 싶을 때는 value class를 선택할 수 있는 선구안이 생긴 것 같지만
코드의 가독성을 높이면서도 하부 구조의 효율을 해치지 않는 설계, 그것이 진짜 실력이라는 점을 다시 한번 생각하는 시간을 갖게 되었습니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글