Java 래퍼 클래스와 오토박싱 완전 정리
Java는 기본형(primitive type)과 참조형(reference type)이라는 두 가지 타입 체계를 가집니다. 이 둘 사이의 간극을 메우는 것이 래퍼 클래스(Wrapper Class)이며, 오토박싱(Auto-boxing)은 이 변환을 자동화한 Java 5의 핵심 기능입니다.
1. 기본형(primitive) vs 참조형(reference)
타입 체계 비교
Java 타입 시스템:
┌─────────────────────────────────────────────────┐
│ Java Types │
│ │
│ 기본형 (Primitive) 참조형 (Reference) │
│ ┌───────────────┐ ┌────────────────────┐ │
│ │ byte │ │ String │ │
│ │ short │ │ Integer │ │
│ │ int │ │ Double │ │
│ │ long │ │ List<T> │ │
│ │ float │ │ Object │ │
│ │ double │ │ 모든 클래스 인스턴스│ │
│ │ char │ └────────────────────┘ │
│ │ boolean │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────┘
메모리 저장 방식
기본형 — 스택(Stack)에 직접 값 저장:
┌──────────────────┐
│ Stack │
│ int i = 42; │ [42] ← 값 자체
│ double d = 3.14 │ [3.14]
└──────────────────┘
참조형 — 스택에 참조(주소), Heap에 객체:
┌──────────────────┐ ┌────────────────┐
│ Stack │ │ Heap │
│ Integer n ──────┼───►│ Integer{42} │
└──────────────────┘ └────────────────┘
크기와 기본값
| 기본형 | 크기 | 기본값 | 래퍼 클래스 |
|---|---|---|---|
byte |
1 byte | 0 | Byte |
short |
2 bytes | 0 | Short |
int |
4 bytes | 0 | Integer |
long |
8 bytes | 0L | Long |
float |
4 bytes | 0.0f | Float |
double |
8 bytes | 0.0d | Double |
char |
2 bytes | ‘\u0000’ | Character |
boolean |
1 bit (JVM 구현 의존) | false | Boolean |
2. 래퍼 클래스(Wrapper Class)
래퍼 클래스가 필요한 이유
// 1. 제네릭은 참조형만 허용
List<int> list = new ArrayList<>(); // 컴파일 에러!
List<Integer> list = new ArrayList<>(); // OK
// 2. null을 표현해야 할 때
int score = null; // 컴파일 에러!
Integer score = null; // OK — 값 없음 표현 가능
// 3. Object가 필요한 곳 (다형성)
Object obj = 42; // 오토박싱 → Integer
// 4. 유틸리티 메서드 활용
Integer.parseInt("42");
Integer.toBinaryString(255);
Integer.MAX_VALUE;
주요 래퍼 클래스 메서드
// Integer
Integer.parseInt("42") // String → int
Integer.valueOf("42") // String → Integer
Integer.toString(42) // int → String
Integer.toBinaryString(42) // "101010"
Integer.toHexString(255) // "ff"
Integer.toOctalString(8) // "10"
Integer.MAX_VALUE // 2147483647
Integer.MIN_VALUE // -2147483648
Integer.bitCount(42) // 1의 개수 (3)
Integer.reverse(42) // 비트 역전
Integer.compare(a, b) // 비교 (Java 7+)
Integer.sum(a, b) // a + b (메서드 참조용)
Integer.max(a, b) // 큰 값
// Double
Double.parseDouble("3.14")
Double.isNaN(Double.NaN) // true
Double.isInfinite(1.0 / 0.0) // true
Double.MAX_VALUE
Double.MIN_VALUE // 양수 최솟값 (0에 가장 가까운)
// Character
Character.isDigit('5') // true
Character.isLetter('A') // true
Character.isLetterOrDigit('_') // false
Character.isUpperCase('A') // true
Character.toLowerCase('A') // 'a'
Character.toUpperCase('a') // 'A'
// Boolean
Boolean.parseBoolean("true") // true
Boolean.parseBoolean("TRUE") // true (대소문자 무시)
Boolean.parseBoolean("yes") // false (true/false만 인식)
3. 오토박싱(Auto-boxing) / 언박싱(Unboxing)
동작 원리
// 오토박싱 — 기본형 → 래퍼 클래스 (컴파일러가 자동 변환)
int i = 42;
Integer boxed = i; // 컴파일러: Integer.valueOf(i)
// 언박싱 — 래퍼 클래스 → 기본형
Integer n = Integer.valueOf(42);
int unboxed = n; // 컴파일러: n.intValue()
컴파일 전: 컴파일 후:
Integer n = 42; ───► Integer n = Integer.valueOf(42);
int i = n; ───► int i = n.intValue();
오토박싱이 일어나는 상황
// 1. 대입
Integer a = 100; // 박싱
// 2. 컬렉션에 추가
List<Integer> list = new ArrayList<>();
list.add(1); // 박싱
// 3. 연산
Integer x = 10;
Integer y = 20;
int sum = x + y; // 둘 다 언박싱 후 더하기
// 4. 메서드 호출 (파라미터 타입 불일치)
void process(Integer n) { ... }
process(42); // 박싱
// 5. 조건문
Integer flag = getFlag();
if (flag == 1) { ... } // flag 언박싱 후 비교
NPE 함정
// 언박싱 시 null이면 NPE!
Integer value = null;
int i = value; // NullPointerException!
// 실수하기 쉬운 패턴
Map<String, Integer> map = new HashMap<>();
int count = map.get("key"); // key 없으면 null → 언박싱 → NPE!
// 안전한 코드
Integer count = map.get("key");
if (count != null) {
int c = count;
}
// 또는
int count = map.getOrDefault("key", 0);
4. Integer 캐시 (-128 ~ 127)와 == 비교 함정
Integer 캐시 동작
JVM은 -128부터 127 범위의 Integer 객체를 미리 생성해 캐싱합니다.
// Java 소스 (Integer.valueOf 구현)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
// 캐시 범위 내 (-128 ~ 127) — 같은 객체 반환
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true (캐시 객체)
System.out.println(a.equals(b)); // true
// 캐시 범위 초과 — 새 객체 생성
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false! (다른 객체)
System.out.println(c.equals(d)); // true
캐시 구조:
IntegerCache.cache:
[0] → Integer(-128)
[1] → Integer(-127)
...
[127] → Integer(-1)
[128] → Integer(0)
...
[255] → Integer(127)
127 → 캐시에서 반환 (항상 같은 객체)
128 → new Integer(128) (항상 새 객체)
다른 래퍼 클래스의 캐시
// Byte: 항상 캐시 (-128 ~ 127, 전체 범위)
// Short: -128 ~ 127 캐시
// Long: -128 ~ 127 캐시
// Character: 0 ~ 127 캐시
// Boolean: TRUE, FALSE 두 객체만 캐시
// Float, Double: 캐시 없음!
Boolean t1 = true;
Boolean t2 = true;
System.out.println(t1 == t2); // true (Boolean.TRUE 캐시)
== vs equals() 결론
// 래퍼 클래스는 절대 == 으로 비교하지 말 것!
Integer a = 1000;
Integer b = 1000;
a == b // 운이 좋으면 true, 아니면 false → 예측 불가
a.equals(b) // 항상 true → 항상 이것을 사용
// 기본형으로 비교
int x = a;
int y = b;
x == y // 항상 true — 안전
JVM 옵션으로 캐시 크기 조정 가능
# -XX:AutoBoxCacheMax=<size> 로 최대값 조정 가능
# 최소는 항상 -128
java -XX:AutoBoxCacheMax=1000 MyApp
5. 성능 주의사항
반복문에서의 박싱 비용
// 나쁜 예 — 매 반복마다 박싱/언박싱
Long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i; // sum 언박싱 → 더하기 → 박싱 (반복!)
}
// 약 100만 번의 Long 객체 생성 → GC 부담
// 좋은 예 — 기본형 사용
long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i; // 기본형 덧셈만
}
객체 생성 비용 측정
// JMH 벤치마크 결과 (대략적):
// 기본형 연산: ~1ns
// 박싱된 Integer: ~5-10ns (객체 생성 + GC 부담)
// 박싱 루프 1M: 기본형 루프 대비 약 6배 느림
컬렉션에서의 기본형 처리
// 표준 컬렉션 — 박싱 필수
List<Integer> list = new ArrayList<>();
list.add(1); // 박싱
// 기본형 특화 스트림 사용
int[] arr = {1, 2, 3, 4, 5};
int sum = IntStream.of(arr).sum(); // 박싱 없음
int max = IntStream.of(arr).max().getAsInt();
// IntStream, LongStream, DoubleStream
IntStream.range(0, 100).forEach(i -> ...); // 박싱 없음
IntStream.rangeClosed(1, 100).sum(); // 박싱 없음
외부 라이브러리: 기본형 컬렉션
// Eclipse Collections (기본형 특화)
IntList intList = IntLists.mutable.of(1, 2, 3);
// Trove / Koloboke (기본형 Map/Set)
TIntIntMap map = new TIntIntHashMap();
map.put(1, 100);
int val = map.get(1); // 박싱 없음
6. Optional과의 관계
Optional은 래퍼 클래스의 null 문제를 해결
// Integer null — 의미가 불명확
Integer score = null; // 점수가 없는 건지, 0인지, 오류인지?
// Optional<Integer> — 의도 명확
Optional<Integer> score = Optional.empty(); // 점수 없음
Optional<Integer> score = Optional.of(95); // 점수 있음
Optional<Integer> score = Optional.ofNullable(getScore()); // null 가능
기본형 Optional
// 박싱 비용 없는 기본형 Optional (권장)
OptionalInt optInt = OptionalInt.of(42);
OptionalLong optLong = OptionalLong.of(100L);
OptionalDouble optDouble = OptionalDouble.of(3.14);
optInt.getAsInt(); // 42
optInt.isPresent(); // true
optInt.orElse(0); // 42
// Optional<Integer> 보다 OptionalInt 선호 (성능)
OptionalInt result = IntStream.range(1, 100)
.filter(i -> i % 7 == 0)
.findFirst();
Optional 활용 패턴
// null 반환 대신 Optional 반환
public Optional<Integer> findScore(String userId) {
Integer score = db.getScore(userId);
return Optional.ofNullable(score);
}
// 사용 측
findScore("user123")
.map(score -> score * 2)
.filter(score -> score > 100)
.ifPresent(score -> System.out.println("High score: " + score));
// orElse / orElseGet / orElseThrow
int score = findScore("user123").orElse(0);
int score = findScore("user123").orElseGet(() -> calculateDefault());
int score = findScore("user123").orElseThrow(() -> new RuntimeException("Not found"));
7. 전체 요약
래퍼 클래스 핵심 정리:
┌──────────────────────────────────────────────────────────┐
│ 기본형 vs 참조형 │
│ - 기본형: 스택, 빠름, null 불가 │
│ - 참조형: 힙, 느림, null 가능, 제네릭 사용 가능 │
│ │
│ 오토박싱 │
│ - 컴파일러가 Integer.valueOf() / intValue() 삽입 │
│ - null 언박싱 → NPE 주의 │
│ │
│ Integer 캐시 함정 │
│ - -128~127 범위는 == 우연히 동작 │
│ - 항상 equals() 사용할 것 │
│ │
│ 성능 │
│ - 반복문 내 박싱 금지 → 기본형 사용 │
│ - 스트림: IntStream / LongStream / DoubleStream 우선 │
│ │
│ null 처리 │
│ - Optional / OptionalInt 활용 │
└──────────────────────────────────────────────────────────┘