예제로 배우는 자바 String Pool 이해하기
개요#
처음 String Pool을 볼 때 제일 헷갈렸던 건, 분명 같은 문자열처럼 보이는데 어떤 건 ==가 true가 되고 어떤 건 false가 된다는 점이었습니다.
new String()으로 만든 경우, 리터럴로 만든 경우, intern()을 붙인 경우가 섞이면 더 헷갈립니다. 그래서 예제로 하나씩 비교해보면서 String Pool과 intern()이 실제로 어떻게 동작하는지 정리해보게 됐습니다.
들어가기 전에#
// Case 1
String str1 = "Hello, World!";
String str2 = "Hello, World!";
String str3 = new String("Hello, World!");
String str4 = new String("Hello, World!");
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str3 == str4);
// Case 2
str3 = str3.intern();
str4 = str4.intern();
System.out.println(str1 == str3);
System.out.println(str3 == str4);
// Case 3
String str5 = "Hello, ";
String str6 = "World!";
String str7 = "Hello, " + "World!";
String str8 = str5 + str6;
System.out.println(str7 == str8);
System.out.println(str7.equals(str8));어떤 결과가 나올지 한번 예상해보겠습니다.
1. 문자열 리터럴과 String Pool#
자바에서 문자열은 보통 두 가지 방식으로 생성됩니다.
문자열 리터럴
리터럴 형태로 문자열을 생성하면, JVM은 String Pool이라는 메모리 영역에 해당 문자열을 저장합니다. 동일한 리터럴은 한 번만 저장되므로 str1 == str2는 true.
new 연산자를 통한 문자열 생성
new 연산자를 통해 Heap 영역에 별도의 객체가 생성됩니다. str1 == str3은 false, str3 == str4도 false.
2. intern() 메서드로 String Pool 활용하기#
자바의 intern() 메서드는 해당 문자열이 이미 String Pool에 존재하면 그 참조를 반환하고, 존재하지 않으면 String Pool에 추가한 후 그 참조를 반환합니다.
intern() 호출 후 str1 == str3은 true, str3 == str4도 true.
3. 컴파일 시점 vs 런타임 시점 문자열 결합#
컴파일 시점 결합
String str7 = "Hello, " + "World!";컴파일러가 상수 리터럴끼리의 결합을 최적화하여 "Hello, World!"로 미리 계산 후 String Pool에 저장합니다.
런타임 시점 결합
String str5 = "Hello, ";
String str6 = "World!";
String str8 = str5 + str6;변수들을 사용한 결합은 런타임에 수행되어 새로운 문자열 객체가 생성됩니다. str7 == str8은 false, str7.equals(str8)은 true.
4. 전체 예제 코드#
public class UsingConstantPool {
public static void main(String[] args) {
// Case 1
String str1 = "Hello, World!";
String str2 = "Hello, World!";
String str3 = new String("Hello, World!");
String str4 = new String("Hello, World!");
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str3 == str4); // false
// Case 2
str3 = str3.intern();
str4 = str4.intern();
System.out.println(str1 == str3); // true
System.out.println(str3 == str4); // true
// Case 3
String str5 = "Hello, ";
String str6 = "World!";
String str7 = "Hello, " + "World!";
String str8 = str5 + str6;
System.out.println(str7 == str8); // false
System.out.println(str7.equals(str8)); // true
}
}5. 정리#
결국 여기서 핵심은 문자열 값이 같아 보여도, JVM이 그 문자열을 어디에 어떻게 올렸는지에 따라 참조 비교 결과는 달라질 수 있다는 점이었습니다. ==는 참조를 보고, equals()는 내용을 봅니다.
또 intern()은 Heap에 있는 문자열을 String Pool 쪽 기준으로 다시 연결해주는 역할을 하고, 컴파일 시점에 붙는 문자열과 런타임에 붙는 문자열은 비교 결과가 다르게 나올 수 있습니다. String Pool을 외우기보다, "이 문자열이 지금 Pool에 있는가, 새 객체인가"를 떠올릴 수 있으면 훨씬 덜 헷갈렸습니다. (JLS 3.10.5 - String Literals)