문자열 인터닝 (String Interning)
한눈에 보기
문자열 인터닝은 같은 내용의 문자열을 문자열 풀에 하나만 두고 공유하는 JVM 메커니즘이다. 리터럴과 new의 차이, intern() 동작, 상수 접기와 == 비교 함정을 코드로 확인한다.
최종 업데이트 2026.09.17
문자열 인터닝(String Interning)은 같은 내용의 문자열을 JVM이 문자열 풀(String Pool)에 하나만 저장해 두고 여러 참조가 그 하나를 공유하게 하는 메커니즘이다. 자바에서 문자열 리터럴은 자동으로 인터닝되기 때문에 같은 리터럴로 만든 두 변수는 동일한 객체를 가리키지만, new 연산자로 만든 String은 풀 밖의 별도 객체가 된다. String이 불변(immutable)이기에 안전하게 공유할 수 있다는 점이 이 최적화의 전제다.
문자열 풀의 위치와 구조
문자열 풀은 핫스팟 JVM 내부에서 StringTable이라는 해시 테이블로 구현되며, 풀에 등록된 String 객체들은 힙(heap)에 존재한다. Java 6까지는 풀이 힙과 분리된 영구 세대(PermGen)에 있어 크기 제약과 메모리 부족 문제가 있었으나, Java 7부터 일반 힙으로 옮겨져 풀에 등록된 문자열도 가비지 컬렉션 대상이 될 수 있게 되었다. 클래스 파일의 상수 풀(constant pool)에 기록된 문자열 리터럴은 클래스가 로딩되고 해당 리터럴이 처음 해석될 때 StringTable에 등록된다.
리터럴과 new의 차이
리터럴로 문자열을 쓰면 JVM은 먼저 풀에서 같은 내용을 찾고, 있으면 그 참조를 돌려주고 없으면 새로 등록한다. 반면 new String("...")은 풀의 등록 여부와 무관하게 힙에 새 객체를 만든다. 그래서 == 비교 결과가 달라진다.
String a = "devfit";
String b = "devfit";
String c = new String("devfit");
System.out.println(a == b); // true, 풀의 동일 객체 공유
System.out.println(a == c); // false, c는 풀 밖의 새 객체
System.out.println(a.equals(c)); // true, 내용은 같다
String d = c.intern(); // 풀에 있는 대표 객체를 반환
System.out.println(a == d); // true
intern() 메서드를 호출하면 풀에 같은 내용의 문자열이 있는지 확인해서 있으면 그 대표 객체를 반환하고, 없으면 현재 문자열을 풀에 등록한 뒤 반환한다. 이를 이용하면 런타임에 만들어진 문자열도 풀의 단일 객체로 정규화할 수 있다.
컴파일 타임 상수 접기
컴파일러는 상수 표현식으로만 이루어진 문자열 연결을 컴파일 시점에 미리 계산해 하나의 리터럴로 만든다. 이를 상수 접기(constant folding)라 한다. 반면 변수나 메서드 호출이 끼어 있으면 실행 시점에 새 객체가 만들어지므로 풀의 객체와 동일하지 않다.
String x = "dev" + "fit"; // 컴파일 시 "devfit" 리터럴로 접힌다
System.out.println(x == "devfit"); // true
String prefix = "dev";
String y = prefix + "fit"; // 실행 시점 연결, 새 객체
System.out.println(y == "devfit"); // false
final String constPrefix = "dev";
String z = constPrefix + "fit"; // final 상수라 접힌다
System.out.println(z == "devfit"); // true
final로 선언된 컴파일 타임 상수는 접기 대상에 포함된다는 점이 자주 나오는 함정 문제다. 어떤 경우든 문자열 내용 비교는 equals()로 해야 하며, == 결과는 인터닝 여부에 따라 달라지는 구현 세부 사항으로 이해해야 한다.
인터닝의 효과와 비용
| 구분 | 장점 | 주의할 점 |
|---|---|---|
| 메모리 | 중복 문자열 제거로 사용량 감소 | 풀에 과도하게 등록하면 StringTable 부담 증가 |
| 비교 성능 | 인터닝이 보장되면 참조 비교 가능 | 보장 범위를 착각하면 버그 유발 |
| intern() 호출 | 런타임 문자열 정규화 가능 | 해시 테이블 조회 비용 발생, 남용 금지 |
대량의 런타임 문자열에 intern()을 무분별하게 호출하면 StringTable 조회 비용과 테이블 크기 증가로 오히려 성능이 나빠질 수 있다. 중복이 매우 많은 데이터라면 G1 GC의 문자열 중복 제거 옵션(-XX:+UseStringDeduplication)처럼 GC 차원에서 내부 문자 배열을 공유시키는 대안도 있다. 이 기능은 String 객체는 그대로 두고 내부 배열만 합치므로 인터닝과는 다른 메커니즘이다.
참고로 Java 9부터 String은 내부적으로 char 배열 대신 byte 배열과 인코딩 플래그를 쓰는 컴팩트 문자열(Compact Strings)로 바뀌어, 라틴 문자만 담긴 문자열의 메모리 사용량이 절반으로 줄었다. 인터닝과 직접 관련은 없지만 문자열 메모리 최적화라는 같은 맥락에서 함께 알아 두면 좋다.
면접에서 이렇게 물어본다
- String을 리터럴로 만들 때와 new로 만들 때 메모리에서 어떤 차이가 있는지 설명해 보라.
- 문자열 풀은 JVM 메모리의 어느 영역에 있으며, Java 7에서 어떤 변화가 있었는가.
- intern() 메서드는 무엇을 하며 어떤 경우에 쓸 수 있는가, 남용하면 왜 문제가 되는가.
- "a" + "b" 와 변수를 이용한 문자열 연결의 == 비교 결과가 다른 이유를 설명해 보라.
- String이 불변이 아니었다면 문자열 풀이 왜 성립할 수 없는지 설명해 보라.
Java에서 같이 읽기
- 오토 박싱과 오토 언박싱 (Auto Boxing and Unboxing)오토 박싱은 기본형 값을 래퍼 클래스 객체로, 오토 언박싱은 그 반대로 자동 변환하는 기능이다. Integer 캐시, == 비교 함정, NullPointerException 위험과 성능 비용을 JVM 메모리 관점에서 정리한다.
- Call by Value와 Call by Reference자바는 기본형이든 참조형이든 언제나 값에 의한 호출만 사용하며, 참조형은 객체가 아니라 참조값이 복사되어 전달된다. swap 실패 예제와 스택 프레임 관점으로 그 근거를 설명한다.
- 캐스팅 (Casting), 업캐스팅과 다운캐스팅캐스팅은 값이나 참조를 다른 타입으로 취급하는 변환이다. 기본형 확대와 축소 변환, 업캐스팅과 다형성, 다운캐스팅과 instanceof 패턴 매칭, ClassCastException 예방법을 다룬다.
- 자바 컴파일 과정 (Java Compile Process)javac가 소스를 바이트코드로 바꾸고 JVM이 로딩, 링킹, 초기화 후 인터프리터와 JIT로 실행하는 전체 흐름을 정리한다. 클래스 파일 구조와 부모 위임 모델, 계층형 컴파일까지 다룬다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기