이 글에서 얻는 것

  • Primitive와 Wrapper의 메모리 구조 차이를 알고, 오토박싱으로 인한 숨겨진 비용(NPE, 성능)을 피합니다.
  • String Pool이 왜 존재하는지 이해하고, ==equals()의 차이를 “주소 vs 값” 관점에서 설명할 수 있습니다.
  • “Java는 포인터가 없나요?“라는 질문에 Call by Value 원리로 정확하게 답할 수 있습니다.

1. int vs Integer: 단순함 뒤의 큰 차이

구분int (Primitive)Integer (Wrapper Object)
저장 위치Stack (값 자체)Heap (객체 헤더 + 값)
Null 가능?불가 (0 초기화)가능 (null 허용)
용도연산, 로컬 변수컬렉션(List), DTO, Null 표현 필요 시
비용매우 저렴객체 생성 + GC 비용 발생

실무 주의점: 오토박싱(Auto-boxing)과 NPE

편리함을 위해 컴파일러가 변환해주지만, 이게 독이 될 때가 있습니다.

public int calculate(Integer value) {
    return value + 10; // value가 null이면 NullPointerException 터짐!
}

alue가 객체이므로 value.intValue()를 호출하는데, value가 null이면 여기서 터집니다. “DB에서 숫자가 null일 수 있다면” 반드시 Wrapper class를 써야 하지만, 연산 전에는 null 체크가 필수입니다.

2. String Constant Pool: new String을 쓰지 마세요

String a = "hello"; 
String b = "hello"; 
String c = new String("hello");

System.out.println(a == b); // true (둘 다 Pool의 같은 주소)
System.out.println(a == c); // false (c는 힙에 새로 만든 객체)

왜 이렇게 만들었을까? (Flyweight Pattern)

포털 사이트 등에서 “HTML” 이라는 문자열은 수백만 번 쓰입니다. 이걸 매번 새 메모리에 할당하면 낭비겠죠? 그래서 String Pool이라는 특별한 공간(Heap 내부)에 하나만 두고 공유해서 씁니다. new String("...")은 이 메커니즘을 무시하고 강제로 힙에 만드므로, 메모리 낭비의 주범입니다. (절대 쓰지 마세요)

3. Java는 Call by Value일까 Reference일까?

면접 단골 질문이자, 주니어들이 가장 많이 헷갈리는 부분입니다. 정답: Java는 언제나 Call by Value 입니다.

“객체를 넘기면 변경되던데요?”

void modify(Person p) {
    p.setName("New Name"); // 변경됨
}

void swap(Person p1, Person p2) {
    Person temp = p1;
    p1 = p2;
    p2 = temp; // 밖에서는 안 바뀜!
}
  • 객체를 넘길 때, “객체의 주소값(Reference)“을 복사해서 전달하기 때문입니다.
  • 복사된 주소(리모컨)를 통해 setName을 하면 원본 집의 가구는 바꿀 수 있습니다.
  • 하지만 리모컨 자체(p1)를 다른 리모컨으로 바꿔도(swap), 원본 함수가 가진 리모컨은 그대로입니다.

실무 요약

  1. 루프 안에서 String + String 하지 마세요 (StringBuilder 쓰세요).
  2. Integer 비교는 반드시 equals()로 하세요 (-128~127 캐싱 범위 밖은 ==이 false입니다).
  3. “이 함수 안에서 객체를 new로 바꿔치기해도” 밖에는 영향이 없음을 기억하세요.