자바의 String Pool

# String pool 의 개념

String은 자바 개발자라면 굉장히 자주 쓰게 되는 자료형 일 것 입니다.
오늘 책을 보다가 String pool 이라는 단어를 접했는데 여태까지 자바가 내부 적으로 String을 이렇게 처리하고 있는지 몰랐다는게 부끄러워 져서 정리 하는 포스트 입니다.

이미 모두가 잘 알고 있듯이 자바에서 String 객체의 값은 변경할 수 없습니다.

1
2
String a = "hello, world";
a += "bye";

위와같이 a를 수정하는 코드를 작성하면 실제로는 기존 “hello, world”를 가지고 있던 a는 버리고 새롭게 “hello, world bye”를 가지고 있는 a를 만든다는 것 이지요. 이 내용은 이미 알고 있거나 비슷한 이야기를 어디선가 들어 보셨을 겁니다.

여기서 재밌는 사실이 하나 등장합니다.
String a = "hello, world" 라고 선언된 부분 중에 실제 내용 부분 (aka. literal)인 hello, world 는 컴파일 후에 String pool 에 들어 갑니다.

1
2
3
4
5
6
7
@Test
public void stringChanges(){
String a = "Hello, world";
String b = "Hello, world";

assertTrue( a == b );
}

String 객체 간의 == 연산은 값을 비교하는게 아니고 같은 메모리를 참조하고 있느냐를 비교하는것 입니다.

  • 처음에 String a = "Hello, world"; 라고 선언 했을 때 Hello, world 를 String pool에 넣고
  • 두번째로 String b = "Hello, world"로 또 만들려고 보니 Hello, world 라는 literal은 이미 String pool에 존재하기 때문에
  • 다시 Hello, world 만들어서 스트링풀에 넣지 않고, 기존에 만들어놓은 스트링풀에서 꺼내서 할당 해 줬다는 이야기 입니다.

이게 가능한것은 맨 처음에 말했듯이 스트링 객체의 값은 변경 할 수 없기 때문에 가능 합니다.
만약 맨처음 설명했듯이 값의 변경이 있을때 새로운 객체에 할당하지 않고 실제로 값을 바꿔버리게 되면 a만 바꿨을 뿐인데 b도 영향을 받을수 있겠죠?

String b = "Hello, world" 에서 새로 객체를 할당하지 않고 String pool 을 뒤져서 equal 연산 후 있으면 그대로 리턴해 주는 부분은 컴퓨터 공학에서 String interning 이라고 합니다. 지금 이 포스팅에서 이야기 하고 있는 String pool 은 JVM이 string interning을 구현한 것이라고 볼 수 있습니다.
더 자세히 알고 싶으신 분은 String interning 위키피디아 를 참조하세요.

# Java에서의 String pool

컴파일이 완료된 클래스 파일들과 스태틱 변수들 등등은 JAVA에서(~java7) Permanent 영역에 저장된다는 사실은 모두 알고 있는 사실인데 string pool에 대해서 알아보다 보니 몇몇 책이나 블로그 등에서 잘못된 정보를 제공하는 경우가 있었습니다. “String pool이 PermGen 영역에 저장된다” 라고 알려주는 책과 블로그등이 있는데 이는 ** 일부만 맞고 일부는 잘못된 정보입니다.**

일부만 맞다고 하는 이유는 대부분의 잘못된 정보를 알려주는 자료들은 ** 작성된지 오래된 자료 ** 이거나 ** 오래된 자료를 참고한 글 ** 이고 그때 당시에 java6 혹은 그 이하 버전이 범용적으로 사용되던 시절이라면 맞는 정보입니다.

하지만 요즘은 대부분 java7 이상을 사용하기 때문에 사실 현재 시점에서는 저 정보는 틀렸다고 볼 수 있습니다. ** java7 이상에서는 String pool을 PermGen 영역에 저장하지 않고 heap 영역에 저장 ** 합니다.
그 이유는 .. 너무 어려워서 제가 완벽히 이해를 못했기 때문에 파고들면 이번 포스트가 산으로 갈것 같아서 요약해서 설명해 드리면 다음과 같습니다.

java 6 이하에서는 PermGen 영역에 String Pool을 저장해 놓았었는데 PermGen영역은 ** Runtime에 확장될 수 없는 고정된 capacity ** 를 가지고 있기 때문에 intern되는 String 값이 너무 많아 지면 OutOfMemoryException을 맞게 될 확률이 높았습니다. 때문에 java7부터 heap에 저장 하게 변경 되었다고 합니다.

# Intern()

String class는 public method인 intern()을 가지고 있습니다.

intern()

Returns a canonical representation for the string object.

canonical representation대표값정도의 의미로 해석하시면 됩니다. 즉, intern()을 호출하면 string object의 대표값이 리턴 된다는 말 입니다.

intern()을 호출 하면 해당 literal이 pool에 있는지 확인하고 있으면 pool에 있는 literal을 return 합니다. pool에 없으면 해당 literal을 pool에 집어 넣습니다.

위에서 주저리 주저리 설명은 했는데 도대체 이게 왜 필요한 것 입니까?
intern() 이 가지고 있는 특성. 즉 값이 string pool에 있으면 그냥 가져오고, 없으면 string pool에 등록시키는 특성을 잘 활용하면

  • 반복적으로 스트링이 같은지 비교연산 할 때
  • 중복값이 많은 스트링을 처리할때

실제 값을 체크하는 equals() 연산보다 ** 1. 적은 메모리 2. 빠른 속도 ** 로 이를 처리 할 수 있습니다.

** ※ Java는 모든 String 을 default로 intern 합니다.**

그럼 실제로 intern() method를 언제 사용할 수 있을지 생각 해 봅시다.
맨처음 예로 들었던 코드를 다시 보겠습니다.

1
2
3
4
5
6
7
@Test
public void stringCompares(){
String a = "Hello, world";
String b = "Hello, world";

assertTrue( a == b );
}

b는 intern 되었기 때문에 a와 같은 메모리 주소값을 가지고 있습니다.
그럼 아래와 같은 경우는 어떨까요?

1
2
3
4
5
6
7
@Test
public void stringCompares2(){
String a = "Hello, world";
String b = new String("Hello, world");

assertTrue( a == b );
}

위 테스트는 ** 실패 ** 합니다. 왜냐하면 b를 기존의 String pool을 살펴보는 대신에 new 를 통해 새로운 객체를 명시적으로 생성하도록 했기 때문에 String pool이 아닌 객체를 참조하기 때문이죠. 위 테스트를 성공하는 테스트로 만들려면 assertTrue( a.equals(b) ) 로 바꾸면 됩니다. equals 는 값을 비교하는 연산이기 때문입니다.

이제 세번째 테스트를 보면 intern()을 언제 사용하면 좋을지 감이 좀 잡힙니다.

1
2
3
4
5
6
7
8
9
10
@Test
public void stringInternTest(){
String a = "Hello, world";
String b = new String("Hello, world");
String c = b.intern();

assertTrue(a.equals(b));
assertTrue(b.equals(c));
assertTrue(a == c);
}

String 객체 c를 만드는데 이 때 bintern() 합니다. intern()을 호출하면 string pool을 뒤져보고 값이 있으면 리턴, 없으면 등록 한다고 했었는데 위의 테스트 케이스는 성공하는 테스트 케이스 일까요?

네 성공 하는 케이스 입니다.

그런데 사실 real world 에서는 intern()을 직접 호출하는 경우를 만나기는 쉽지 않을것 같습니다.
실제 사용할 일이 생긴다면 아마도

  • 엄청나게 많은 양의 데이터를 파싱한다던지,
  • 스트링 값이 많은 데이터를 저장하고 비교한다던지 하는 일이 필요한 프로그램

에서 사용할 수 있을것 같고 실제 코드는 아래와 같이
스트링 필드를 가지고 있는 데이터셋이 있을때 그 객체의 스트링필드를 intern 하고 비교하는 식으로 사용할 수 있겠네요.

1
2
3
4
5
6
7
8
9
@Test
public void stringInternTest(){
SomeObject someObject = someDataset.get();

String a = "Hello, world";
String b = someObject.getSomeStringField().intern();

assertTrue(a == b);
}

최대한 옳은 정보를 드리기 위해 공부하면서 정리했는데 혹시라도 틀린 부분이 있으면 부담없이 commnet 남겨 주세요.

# 참조한 글

Share