관리 메뉴

The Nirsa Way

[Effective Java 3/E - 객체 생성과 파괴] Item 6-4. 불필요한 객체 생성을 피하라 - 오토 박싱(auto-boxing)의 함정 본문

Development/JAVA

[Effective Java 3/E - 객체 생성과 파괴] Item 6-4. 불필요한 객체 생성을 피하라 - 오토 박싱(auto-boxing)의 함정

KoreaNirsa 2025. 6. 23. 21:56
반응형

 

오토박싱(auto-boxing)과 언박싱(unboxing)

자바는 기본형(int, long, double, ...)과 래퍼 클래스(Integer, Long, Double, ...)를 구분합니다. 기본형과 래퍼 클래스는 오토박싱과 언박싱을 통해 자동으로 이루어지지만 때로는 성능 저하로 이루어질 수 있습니다.

우선 아래의 코드는 int 타입(10)을 Integer로 자동으로 변환시켜주는 오토 박싱의 예시입니다. 자바에서 숫자는 기본적으로 int 이므로 숫자 10은 기본형, 데이터 타입은 Integer인 래퍼클래스 이기에 자동으로 변환 시켜줍니다.

Integer num = 10;

 언박싱은 반대로 래퍼 클래스를 기본형으로 자동 변환해주는 것을 뜻하는데, 언박싱에 대한 내용은 포스팅 하단에 남기겠습니다.

 


 

오토 박싱의 함정

오토 박싱은 개발자를 편하게 해주지만, 오타처럼 보이는 어떠한 실수 하나로 성능 저하를 발생시킬 수 있습니다. 아래의 코드에서 문제는 sum 변수를 래퍼 클래스(Long)으로 선언했다는 것 입니다.

public static void main(String[] args) {
    Long sum = 0L;

    // int의 최대값 만큼 반복(약 21억)
    // 코드에서 변수 i는 기본형, sum은 래퍼클래스
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }

    System.out.println(sum);
}

 

sum += 1은 내부적으로 Long.valueOf(long) 메서드가 호출되어 매번 새로운 객체를 반환합니다. 즉, 위의 코드는 반복문이 한번 돌때마다 래퍼 클래스가 매번 생성되며 숫자가 더해지는 것입니다. (정확한 오피셜의 레퍼런스는 찾지 못했습니다. 혹시 의견이 다르시거나 레퍼런스를 알고 계신분이 있다면 댓글 부탁드립니다.)

위의 코드에서 확인이 가능하듯 -128 ~ 127은 캐시 전략에 따라 설계되어 새로운 객체를 반환하지 않으므로 성능 저하가 발생하지 않습니다.

실제로 간단한 코드를 작성하여 javap를 사용하여 디컴파일 할 경우 아래와 같은 결과를 확인할 수 있었습니다. 실제로 += 연산자 등을 사용하면 내부적으로 언박싱 → 연산 → valueOf(long)이 호출되며 새로운 Long 인스턴스를 만들어 값을 저장하는 것을 확인할 수 있었습니다.

public static void main(String[] args) {
    // 오토박싱 대상
    Long sum = 0L;

    // 기본형이므로 오토박싱 대상 아님
    long i = 1L; 

    // 기본형 -> 래퍼 클래스 이므로 오토박싱 대상
    sum += i; 
}
// 오토박싱 대상
0: lconst_0			// 스택에 long 타입의 리터럴 값 0L을 푸시
1: invokestatic  #16	  	// Long.valueOf(long) → 오토박싱
4: astore_1			// sum에 저장

// 기본형이므로 오토박싱 대상 아님
5: lconst_1			// 스택에 long 타입의 리터럴 값 1L을 푸시
6: lstore_2			// i에 저장

// 오토박싱 대상
7: aload_1			// sum (Long 객체) 꺼냄
8: invokevirtual #22		// Long.longValue() → 언박싱
11: lload_2			// i 꺼냄
12: ladd			// long 덧셈 수행
13: invokestatic  #16		// Long.valueOf(long) → 다시 오토박싱
16: astore_1			// 결과를 sum에 저장
17: return

javap -c Test.class

아래와 같이 오타(Long)를 수정하여 오토박싱이 발생하지 않도록 코드를 수정하여 성능을 개선할 수 있습니다.

public static void main(String[] args) {
    // 기본형으로 변경하여 오토박싱 제거
    long sum = 0L;

    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }

    System.out.println(sum);
}

 


 

언박싱은 어떠한가?

 언박싱은 반대로 래퍼 클래스를 기본형으로 자동 변환해주는 것을 뜻하는데, 이또한 성능 저하를 일으킬 수 있습니다. 래퍼 클래스(객체)를 기본형으로 변환하다 보니 NPE(NullPointerException) 발생 위험이 있으며 단순 연산보다는 언박싱이 변환하는 과정이 있기에 당연히 더 느리므로 어느정도의 성능 저하가 발생할 수 있습니다.

예시로 들자면 아래와 같은 코드가 언박싱으로 인한 성능 저하가 조금 발생할 수 있습니다.

public static void main(String[] args) {
    List<Integer> list = ...;
    int sum = 0;
    for (Integer i : list) {
        sum += i; // 언박싱
    }
}

래퍼클래스의 변수 i를 기본형 변수 sum에 값을 더하고 있습니다. 이러할 경우 i에 null이 들어가 있으면 NPE가 발생할 수 있습니다. 또한 += 언박싱 연산에 필요한 메서드를 내부적으로 호출하므로 오버헤드가 발생합니다.

더불어 List안의 Integer 객체들은 힙에 불연속적으로 분산 배치되므로 CPU 캐시 적중률이 낮아 성능에 부정적인 영향을 줄 수 있습니다.

하지만 근본적으로 List는 기본형을 제네릭으로 받을 수 없으므로 메모리 구조 상 캐시 적중률을 개선하기 어렵기에 스치듯 넘어가셔도 될 것 같습니다. 또한, 일반적인 상황의 경우 언박싱 자체가 큰 성능 저하를 유발하지는 않으며 이러한 경우도 있다는것을 알고 넘어가면 될 것 같습니다.

※ CPU 캐시 적중률(Cache Hit Rate)란?
간단히는 CPU가 요청한 데이터를 캐시 메모리에서 찾아낸 비율을 나타냅니다.
메모리상에서 데이터가 인접하게 배치되어 있으면 같은 캐시 라인에 함께 올라가기 때문에 cache hit 확률이 높아지고, 데이터가 분산 배치되어 흩어질수록 RAM을 자주 탐색해야 하므로 cache miss가 증가하여 성능 저하로 이어질 수 있습니다.

 

반응형