본문 바로가기

Java

기본 타입과 boxing된 기본 타입 이해하기

어느 날 동료 개발자가 Boxing과 Unboxing을 이야기하는 것을 들었다.

창피하게도 무슨 말을 하는 것인지 몰랐다. 이펙티브 자바를 아직 다 읽기 전이라 몰랐을 법하다.

이펙티브 자바를 다 읽고 나서야 Boxing과 Unboxing을 어떤 의미로 사용하는 것인지 알게 되었다.

변명처럼 들리겠지만, 사실 Boxing 된 기본 타입과 Unboxing 된 기본 타입이라고 이야기를 했었으면 무슨 말인지 이해했을 거 같다.

나는 기존에 Boxing 된 기본 타입을 Wrapper Type이라 부르고, Unboxing 된 기본 타입은 Primitive Type이라 불렀었다.

지금도 이렇게 부르는 게 편하고 익숙하다. 하지만 많은 사람이 이펙티브 자바를 자바의 기본서처럼 많이들 읽으니 앞으로는 나도 Boxing 된 기본 타입과 Unboxing 된 기본 타입이라고 불러야겠다. 정확히는 기본 타입과 Boxing 된 기본 타입이다.

 

Primitive Type은 기본 타입 그리고 Wrapper Primitive Type은 Boxing 된 기본 타입이다.

우선 기본 타입에 대해 알아보자. 자바에는 아래와 같이 총 8개의 기본 타입이 있다.

byte, short, int, long, float, double, boolean, char

Boxing 된 기본 타입은 아래와 같이 기본 타입과 동일하게 존재한다.

Byte, Short, Integer, Long, Float, Double, Boolean, Character

 

왜 기본 타입과 Boxing 된 기본 타입으로 나누어져 있을까?

JVM의 구조를 설명하려니 일이 너무 커질 거 같다. JVM의 메모리 구조는 따로 확인하는 것으로 하고 여기서는 간단히 설명하겠다.

기본적으로 JVM은 Stack과 Heap 메모리 영역을 OS로부터 할당받아 실행되는데 Stack은 Thread가 생길 때 마다 별도로 생성된다.

Stack은 Thread 간에 자원의 공유가 불가능하지만, Heap은 Thread 간 자원 공유가 가능하다.

Thread 간에 자원 공유가 가능하고 불가능한 것이 기본 타입과 Boxing 된 기본 타입의 존재를 설명하는데에 어떤 관련이 있을까?

Stack은 각각의 Thread마다 별도로 영역이 생성되어 Thread 내에서 생성되는 변수를 저장하고, Heap은 Stack에 있는 변수들이 참조하는 값을 저장하는 영역이다. 여기서 생각해볼 수 있는 것이 있다. 기본 타입이든 Boxing 된 기본 타입이든 간에 모든 값이 Heap에 저장된다면 구조상 어떤 점이 불리할까? 모든 Thread가 하나의 Heap 영역을 바라보기 때문에 한 Thread가 본인이 생성한 변수의 값을 다른 Thread에 공유하고 싶지 않아도 다른 Thread는 해당 값을 언제든 꺼내 볼 수 있는 구조일 것이다. 그래서 기본 타입은 Stack에 변수와 값이 함께 저장되고, Boxing 된 기본 타입은 Stack에는 변수만 저장되고 값은 Heap에 저장하여 Stack이 Heap에 있는 값을 참조하게 되어 있다. 아래 코드 및 그림과 같다고 보면 된다.

 

public class Tests {
    public static void main(String[] args) {
        int primitiveInt = 9;
        Integer boxingInteger1 = 9; // Integer@478
        Integer boxingInteger2 = 7; // Integer@479
        Integer boxingInteger3 = 7; // Integer@479
        Integer boxingInteger4 = new Integer(7); // Integer@480
    }
}

 

기본 타입과 Boxing 된 기본 타입의 주된 차이점

  • 기본 타입은 값만 가지고 있으나, Boxing 된 기본 타입은 값에 더 해 식별성이란 속성을 갖는다. 달리 말하면 Boxing 된 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다.
  • 기본 타입의 값은 언제나 유효하나, Boxing 된 기본 타입은 유효하지 않은 값, 즉 null을 가질 수 있다.
  • 기본 타입이 Boxing 된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.

 

Boxing된 기본 타입 주의할 사항

기본 타입과 Boxing 된 기본 타입을 혼용한 연산에서는 Boxing 된 기본 타입의 Boxing이 자동으로 풀린다. 그리고 null 참조를 Unboxing하면 NullPointException이 발생한다.

// 해당 코드는 오류나 경고 없이 컴파일되지만,
// sum을 박싱된 기본 타입으로 선언하여 박싱과 언박싱이 반복해서 일어나 성능이 느려진다.
public static void main(String[] args) {
	Long sum = 0L;
	for (long i = 0; i <= Integer.MAX_VALUE; i++) {
		sum += i;
	}
	System.out.print(sum);
}

 

그렇다면 Boxing 된 기본 타입은 언제 써야 하는가?

  • Collection의 원소, 키, 값으로 쓴다. Collection은 기본 타입을 담을 수 없으므로 어쩔 수 없이 Boxing 된 기본 타입을 써야만 한다. 더 일반화해 말하면, 매개변수화 타입이나 매개변수화 Method의 타입 매개변수로는 Boxing 된 기본 타입을 써야 한다. 자바 언어가 타입 매개변수로 기본 타입을 지원하지 않는다.
    • Collection의 매개변수화 타입 예시: List<Integer> list = new ArrayList<>();
    • 매개변수화 Method 타입 예시:
      • public static <T, S> T toObject(S source, Class<T> targetClass) {...}
      • Long a = toObject(Integer.MAX_VALUE, Long.class);
  • Reflection을 통해 Method를 호출할 때도 Boxing 된 기본 타입을 사용해야 한다.

 

핵심정리 - Effective Java 3/E 인용

기본 타입과 Boxing 된 기본 타입 중 하나를 선택해야 한다면 가능하면 기본 타입을 사용하라. 기본 타입은 간단하고 빠르다. Boxing 된 기본 타입을 써야 한다면 주의를 기울이자. Auto Boxing이 Boxing 된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지는 않는다. 두 Boxing 된 기본 타입을 == 연산자로 비교한다면 식별성 비교가 이뤄지는데, 이는 여러분이 원한 게 아닐 가능성이 크다. 같은 연산에서 기본 타입과 Boxing된 기본 타입을 혼용하면 Unboxing이 이뤄지며, Unboxing 과정에서 NullPointException을 던질 수 있다. 마지막으로 기본 타입을 Boxing 하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있다.

 

 

참고문헌

조슈아 블로크, Effective Java 3/E