오토 박싱과 오토 언박싱 (Auto Boxing and Unboxing)

한눈에 보기

오토 박싱은 기본형 값을 래퍼 클래스 객체로, 오토 언박싱은 그 반대로 자동 변환하는 기능이다. Integer 캐시, == 비교 함정, NullPointerException 위험과 성능 비용을 JVM 메모리 관점에서 정리한다.

최종 업데이트 2026.09.17

오토 박싱(Auto Boxing)은 자바 컴파일러가 int 같은 기본형(primitive type) 값을 Integer 같은 래퍼 클래스(wrapper class) 객체로 자동 변환해 주는 기능이고, 오토 언박싱(Auto Unboxing)은 그 반대 방향의 자동 변환이다. 두 기능은 Java 5에서 도입되어 제네릭 컬렉션에 기본형 값을 자연스럽게 넣고 꺼낼 수 있게 해 주었다. 다만 변환 과정에서 객체 생성 비용과 NullPointerException 위험이 숨어 있으므로 동작 원리를 정확히 이해하고 사용해야 한다.

기본형과 래퍼 클래스

자바의 타입 체계는 값 자체를 담는 기본형과 객체를 가리키는 참조형(reference type)으로 나뉜다. 기본형에는 byte, short, int, long, float, double, char, boolean 여덟 가지가 있고, 각각에 대응하는 래퍼 클래스 Byte, Short, Integer, Long, Float, Double, Character, Boolean이 java.lang 패키지에 존재한다.

래퍼 클래스가 필요한 가장 큰 이유는 제네릭(generics)이다. 자바의 제네릭 타입 매개변수에는 참조형만 올 수 있으므로 List<int>는 불가능하고 List<Integer>로 선언해야 한다. 이때 매번 개발자가 직접 Integer.valueOf(3)과 같이 감싸고 intValue()로 풀어야 한다면 코드가 번거로워지기 때문에 컴파일러가 이 변환을 대신 넣어 준다.

컴파일러가 하는 일

오토 박싱과 언박싱은 문법적 편의일 뿐이며, 컴파일된 바이트코드(bytecode)를 보면 실제로는 메서드 호출로 바뀌어 있다. 박싱은 Integer.valueOf(int) 같은 정적 팩터리 호출로, 언박싱은 intValue() 같은 xxxValue() 호출로 치환된다.

List<Integer> list = new ArrayList<>();
list.add(10);            // 컴파일 후: list.add(Integer.valueOf(10))
int first = list.get(0); // 컴파일 후: list.get(0).intValue()

Integer boxed = 42;      // 오토 박싱
int primitive = boxed;   // 오토 언박싱

Integer.valueOf는 -128부터 127까지의 값을 미리 만들어 둔 캐시에서 돌려준다. 그래서 이 범위 안의 두 Integer를 == 로 비교하면 true가 나오지만, 범위를 벗어나면 서로 다른 객체가 생성되어 false가 나온다. 래퍼 객체의 값 비교에는 반드시 equals()를 써야 하는 이유다.

Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b);      // true (캐시된 동일 객체)
System.out.println(c == d);      // false (서로 다른 객체)
System.out.println(c.equals(d)); // true

JVM 메모리 관점의 차이

기본형 지역 변수의 값은 스레드 스택의 프레임 안, 정확히는 지역 변수 배열에 그대로 저장된다. 반면 래퍼 객체는 힙(heap)에 할당되고 스택에는 그 객체를 가리키는 참조만 남는다. 따라서 박싱이 일어날 때마다 힙 할당이 발생할 수 있고, 그만큼 가비지 컬렉션(Garbage Collection) 대상도 늘어난다.

반복문 안에서 무심코 래퍼 타입을 누적 변수로 쓰면 이 비용이 크게 불어난다. 아래 코드는 매 반복마다 언박싱과 박싱이 한 번씩 일어나 Long 객체가 대량으로 생성된다.

Long sum = 0L; // long으로 선언해야 한다
for (long i = 0; i < 1_000_000L; i++) {
    sum += i;  // sum = Long.valueOf(sum.longValue() + i)
}

NullPointerException 함정

언박싱은 참조에서 값을 꺼내는 메서드 호출이므로, 래퍼 변수가 null이면 즉시 NullPointerException이 발생한다. 데이터베이스 조회 결과나 Map의 get() 반환값처럼 null이 섞일 수 있는 값을 기본형 변수에 바로 대입하는 코드가 대표적인 사고 지점이다. 또한 삼항 연산자에서 한쪽이 기본형이고 다른 쪽이 래퍼면 전체가 기본형으로 통일되면서 예상치 못한 언박싱이 일어나기도 한다.

Map<String, Integer> scores = new HashMap<>();
int score = scores.get("kim"); // null 언박싱, NullPointerException

Integer boxed = null;
boolean flag = true;
int result = flag ? 1 : boxed; // boxed가 선택되지 않아도 타입은 int로 통일된다

기본형과 래퍼 타입 비교

구분기본형 (int)래퍼 클래스 (Integer)
저장 위치스택 프레임에 값 저장힙에 객체, 스택에 참조
null 허용불가능가능
기본값0null
비교 방법== 로 값 비교equals()로 값 비교
제네릭 사용불가능가능
연산 성능빠름박싱과 언박싱 비용 발생

정리하면, 성능이 중요한 연산이나 반복문의 누적 변수에는 기본형을 쓰고, 컬렉션 원소나 null이 의미를 갖는 값에는 래퍼를 쓰되 언박싱 지점에서 null 검사를 잊지 않는 것이 원칙이다. Java 21 기준으로도 이 동작은 동일하며, 향후 발할라(Valhalla) 프로젝트의 값 객체가 도입되면 박싱 비용이 줄어들 것으로 기대되고 있다.

면접에서 이렇게 물어본다

  • 오토 박싱과 오토 언박싱이 무엇이고, 컴파일러가 실제로 어떤 코드로 치환하는지 설명해 보라.
  • Integer 두 개를 == 로 비교했을 때 127까지는 true인데 128부터 false가 되는 이유는 무엇인가.
  • 래퍼 타입을 반복문의 누적 변수로 쓰면 성능이 나빠지는 이유를 JVM 메모리 구조와 연결해 설명해 보라.
  • 언박싱 과정에서 NullPointerException이 발생할 수 있는 상황을 예로 들어 보라.
  • 제네릭 컬렉션에 기본형을 직접 담을 수 없는 이유는 무엇인가.
Java 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기