어느 날 동료 개발자가 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
'Java' 카테고리의 다른 글
Java 의존성 역전하기 - Dependency Inversion (사례를 통한 학습) (4) | 2021.10.03 |
---|---|
Java Thread와 Concurrency(동시성) 이해하기 (0) | 2021.07.22 |
Statement VS PreparedStatement 차이점 (0) | 2021.07.08 |