
1. Primitive 와 Reference
JVM 환경
Primitive type (원시 타입)
- int, long, float, double, boolean, char, byte, short
- 스택 / 레지스터 위주, null 불가
- 객체가 아니라 값(value)으로만 다룸.
Reference type (참조 타입)
- Integer, Long, Float, String, List, Any 등 모든 객체 타입
- 힙(Heap)에 객체가 존재하고, 변수에는 그 객체를 가리키는 참조(주소)가 들어감
- null 가능
Kotlin 입장
Kotlin 언어 자체는 "primitive"라는 말은 쓰지 않고, nullable 여부로만 type을 나눈다.
- Int, Long, Float, Boolean
- Kotlin 레벨에선 그냥 값 처럼 보이는 것들
- nullable 아님 → Int, Long (기본)
- Int?, Long?, Float? ...
- nullable 타입 → null 허용
그리고 JVM으로 컴파일될 때 아래처럼 매핑된다.
- Int → 가능하면 int(primitive)로 컴파일
- Int? → java.lang.Integer (Reference 타입, 박싱된 객체)
- List<Int> → 제네릭 때문에 내부적으로는 List<Integer>처럼 박싱된 타입 사용
- IntArray → 진짜 int[] (Primitive 타입)
Kotlin 소스에서 보이는 Int / Int?는 같아 보이지만, JVM 바이트코드에서는 Int는 primitive, Int?는 reference로 변신한다.
2. Int vs. Int? 활용 차이
2-1. non-null 값 타입 예시 (Int)
val count: Int = 10 // 절대 null 아님
val length: Int = "hello".length
- null 체크 없이 바로 연산 가능
- 컴파일 시 Kotlin이 non-null을 보장하기 때문에 JVM에서는 primitive int로 컴파일됨.
- 빠르고 메모리도 적게 씀
2-2. nullable 값 타입 예시 (Int?)
val maybeCount: Int? = null
fun parseIntOrNull(s: String): Int? = s.toIntOrNull() // 실패하면 null
- "값이 있을 수도 있고 없을 수도 있다"는 도메인 의미를 표현함
- JVM에서는 Integer 객체(Reference type)로 다뤄짐
- 사용 시에는 항상 null 처리가 요구됨
val value: Int? = parseIntOrNull("123")
val safe: Int = value ?: 0 // elvis 연산자로 기본값
val plusOne: Int? = value?.plus(1) // safe call

3. Boxing과 Unboxing
3-1. Boxing (primitive → reference)
원시값을 객체로 wrapping해서 Reference 타입으로 바꾸는 것.
Kotlin에서 boxing이 발생하는 대표적인 상황들은 아래와 같다.
- Nullable로 바뀌는 순간
val a: Int = 10
val b: Int? = a // 여기서 boxing 발생: int → Integer
- Any / Any?로 업캐스팅할 때
fun printAny(x: Any) {
println(x)
}
val n: Int = 42
printAny(n) // Int → int → boxing → Integer → Any
- 일반 컬렉션 (List / Set / Map)에 넣을 때
val list: List<Int> = listOf(1, 2, 3)
// 제네릭 때문에 JVM에선 List<Integer> 형태 → 전부 boxing
- Java 메서드가 Integer를 요구하는 경우
// Java
void printInteger(Integer x) { ... }
val v: Int = 10
printInteger(v) // Kotlin → boxing 해서 Integer로 전달
3-2. Unboxing (reference → primitive)
객체(Integer)에서 실시 원시값(int)을 꺼내는 것.
Kotlin에서 unboxing이 일어나는 상황은 아래와 같다.
- nullable 값을 연산에 사용하는 순간
val a: Int? = 10
val b: Int = a!! + 5
// a!! : Int?를 Int로 강제 변환(실제로는 unboxing + NPE 체크)
- nullable을 non-null 타입 variable에 대입할 때
val a: Int? = 10
val b: Int = a!! // unboxing + NPE 가능성
- Java 함수가 primitive를 요구할 때
// Java
void process(int value) { ... }
val x: Int? = 10
process(x!!) // x를 int로 unboxing 후 전달
4. Boxing, Unboxing의 중요성
4-1. 성능 측면
Heap 할당 및 GC 비용
- primitive(int)는 스택/레지스터 영역으로 할당 비용이 거의 없다.
- boxed 타입(Integer)는 힙에 객체를 생성하기 때문에 할당 및 garbage collection 비용이 발생한다.
fun sumList(list: List<Int>): Int {
var sum = 0
for (x in list) {
sum += x // 매 반복마다 boxing/unboxing 가능
}
return sum
}
fun sumArray(arr: IntArray): Int {
var sum = 0
for (x in arr) {
sum += x // 순수 primitive 연산, 훨씬 저렴
}
return sum
}
- List<Int>는 내부적으로 Integer 객체들의 리스트라서 박싱된 값을 반복적으로 unboxing하면서 연산한다.
- IntArray는 진짜 int[]라서 primitive 연산만 수행하기 때문에 성능 우위가 존재한다.
핫 루프, 대량 연산에서는 IntArray / LongArray 처럼 primitive 배열을 사용하는 것이 훨씬 유리하다.
4-2. Null-safety / NPE 측면
Unboxing은 null일 때 NPE의 직접적인 원인이 된다.
val a: Int? = null
val b: Int = a!! // 여기서 NPE
이는 Java interop에서도 주의해야 한다.
// Java
Integer getValueOrNull() { return null; }
val v: Int = getValueOrNull() // 플랫폼 타입 Int! 로 인식, 런타임에 NPE 가능
Kotlin은 null-safe한 언어지만, unboxing 시점에 null이면 결국 NPE가 발생한다.
때문에 가능하면 non-null 타입(Int) 위주로 사용하고, 진짜 필요할 때만 Int?를 쓰고 ?: / ?. / ㅣlet 등으로 안전히 처리해야 한다.
4-3. API 설계 관점
박싱 / 언박싱을 이해하면 Kotlin API를 어떻게 설계할지가 달라진다.
- "이 값은 반드시 있어야 한다." → Int, Long 처럼 non-null 타입으로 설계
- "없을 수도 있다. (Optional, 실패 가능)" → Int?, Long?으로 설계
// bad: 애매하게 nullable로 설계
fun findUserAgeBad(id: String): Int? {
// null이면? 실패인지, 나이가 0인지, 정보가 없는 건지 애매
}
// better: 도메인 의미가 분명하게 드러나게 설계
fun findUserAge(id: String): Int? // 사용자 없으면 null
fun getUserAgeOrThrow(id: String): Int // 없으면 예외
fun getUserAgeOrDefault(id: String, default: Int = 0): Int
여기서 Int?를 쓰는 순간 JVM은 박싱 타입(Integer)이 되고, Int를 쓰면 가능하면 primitive int로 최적화된다는 걸 함께 의도한 선택이라 볼 수 있다.
5. 정리
5-1. Kotlin 코드 → JVM 내부 타입 대응
| Kotlin 타입 | null 허용 | JVM 타입 예시 | 특징 |
| Int | X | int | primitive, 빠름, NPE X |
| Int? | O | java.lang.Integer | Reference, boxing, NPE 위험 |
| List<Int> | 상황에 따라 | List<Integer> | 내부 요소 boxing |
| IntArray | X | int[] | primitive 배열 |
5-2. Boxing, Unboxing
- Boxing : Int → Int? / Any / List<Int> 등 힙 객체를 생성하는 것.
- Unboxing : Int? / Integer를 Int 연산에 쓸 때 → NPE 위험 + 연산 오버헤드 발생 가능함.
6. 심화 주제
- 성능 : Kotlin 내 List<Int>와 IntArray의 성능 차이를 boxing / unboxing 관점에서 벤치마크 코딩 예제
- 설계 : 도메인 설계 시, Kotlin 함수의 반환 타입을 Int, Int? 중 무엇으로 할지 결정 기준.
- Java interop 위주 : Interop 환경에서 Int / Int? 사이의 boxing / unboxing 발생 시기 및 NPE 실제 케이스 예제
참고 자료
- What’s the Difference Between Primitive and Non-Primitive Data Types In Java?
- Do You Know Autoboxing and Unboxing in Java?
-
Powered By. ChatGPT
'Backend' 카테고리의 다른 글
| [Backend] TCP 스트림 기반 파일 전송 기능 구현 - CHAT/FILE multiplexing, 단일 Writer, backpressure 구현 기록 (1) | 2025.12.19 |
|---|---|
| [Backend] HTTP 발전의 역사 (1) | 2025.12.08 |
| [Backend] ShadowJar 플러그인 적용 후 “기본 Manifest 속성이 없습니다” 오류 해결기 (0) | 2025.11.24 |
| [Backend] Kotlin tips (0) | 2025.11.24 |
| [Backend] Jackson 기반 직렬화·역직렬화의 원리와 적용 절차 (0) | 2025.11.17 |