SimpleIsBest.NET

유경상의 닷넷 블로그

문자열 이야기 V : 인턴 풀(Intern Pool)이란?

by 블로그쥔장 | 작성일자: 2005-07-12 오전 11:19:00
이 글은 오래된 전에 작성된 글입니다. 따라서 최신 버전의 기술에 알맞지 않거나 오류를 유발할 수 있습니다. 저자는 이 글에 대한 질문을 받지 않을 것입니다. 하지만 이 글이 리뉴얼 되면 이 글에 대한 질문을 하거나 토론을 할 수도 있습니다.
이번 문자열 이야기 포스트는 아티클 기반의 모 개발자 커뮤티니 사이트에서 황당(?)한 글을 읽어서 몇 글자 적어보려고 합니다. 해당글은 높은 평점을 받았음에도 불구하고 잘못된 정보를 기록하고 있었습니다. StringBuilder를 쓰는 것이 대부분 좋다 라든가, 닷넷이 모든 문자열에 대한 정보를 해쉬 테이블 형태로 기록해두고 동일 문자열이 사용되면 기록해 두었던 문자열을 사용한다든가 등등...

StringBuilder에 대한 이야기는 지난 문자열 이야기 씨리즈 1, 2, 3에서 이미 다루었으므로 이번엔 소위 리터럴 문자열이란 것과 인턴 풀(intern pool)에 대해 이야기 해보도록 하겠습니다.

About Intern Pool

인턴 풀? 무슨 개 같은 소리 인가? 청년 실업이 이렇게 심각한 지금... 인턴 사원하기도 힘든 이 세상에 문자열에도 프로그래밍에도 인턴 나부랭이와, 정식 나부랭이가 있단 말인가?

흥분까지 할 필요는 없다. 인턴 풀이란 것을 이해하기 위해서는 먼저 리터럴 문자열(literal string)이란 것을 이해해야 한다. 리터럴 문자열을 풀어서 말하자면 소스 코드내에 상수로서 존재하는 문자열을 말한다. 예를 들어 str = "abcd" 이런 코드가 있다면 "abcd" 는 하드 코드된 문자열 상수이며 이거시 바로 인턴 문자열이 되긋다. 이러한 리터럴 문자열들을 CLR이 모아놓은 테이블을 인턴 풀이라고 하는 것이다. 인턴 풀에 있는 문자열 객체(System.String 클래스)는 이미 만들어진 문자열 객체이며, 인턴 풀에 존재하는 문자열 객체는 관리되는 힙(managed heap; managed/unmanaged... 이거 번역하기 정말 뷁스런 단어다. -_-)에 존재하지 않으며 GC의 대상도 아님에 유의할 필요가 있다.

Why ?

왜 리터럴 문자열을 모아둘 필요가 있을까? 일종의 최적화라고 생각하면 되겠다. 예를 들어 "hello world" 란 문자열이 코드내에 명시되면 이 "hello world"란 문자열 객체는 컴파일 타임에 System.String 객체를 미리 만들어 놓을 수 있다. 또한 이 문자열 객체는 어셈블리 (EXE 나 DLL) 파일 내에 기록을 해두어야 하고 어셈블리가 메모리에 로드될 때 어셈블리 파일의 이 객체가 메모리로 로드 된다는 이야기 이다.

여기서 최적화 뽀인뜨를 찾을 수 있다. 첫째로, 하드 코드된 "hello world" 란 문자열이 코드내에서 2회 이상 사용된다면 문자열의 immutable 이란 점을 감안할 때 2개 이상의 동일한 "hello world" 문자열 값을 갖는 System.String 객체를 만들 이유가 전혀 없는 것이다. 따라서 컴파일러는 1개의 "hello world" 문자열 객체만을 만들어 두고 "hello world"가 사용될 때마다 이미 만들어 놓은 System.String 객체의 참조를 사용하면 되는 것이다.

두번째 최적화 뽀인또는 "hello world" 란 값을 갖는 문자열 리터럴은 프로그램의 수행 순서상 언제 다시 사용될지 컴파일러는 예측하지 못한다. 생각해 보라. 컴파일러가 스티븐 스필버그의 AI 란 영화처럼 AI를 갖지 않는 이상 프로그램의 수행 순서를 다 추적할 수는 없지 않는가? 따라서 런타임은 "hello world" 란 리터럴 문자열을 GC 할 수 없게 되는 것이며, 이것이 리터럴 문자열을 관리되는 힙에 둘 수 없는 이유인 것이다.

Literal String Test

말로만 하면 잘 와 닫지 않으니 테스트를 좀 해보자. 앞서 문자열 비교에 관련된 포스트에서 문자열 비교에서 참조 비교를 하면 같은 문자열 값을 갖더라도 다른 결과가 나올 수 있다고 했다. 그렇다면 다음 코드를 보자.

string s1 = "Hello World";
string s2 = String.Concat("Hello ", "World");
Console.WriteLine("Reference equal ? s1 == s2 :: {0}", object.ReferenceEquals(s1, s2));

위 코드의 결과는 예상한 바대로 False 를 반환한다. s1과 s2가 서로 다른 string 객체에 대한 참조를 갖고 있기 때문이다. 그렇다면 위 코드에 다음 코드를 추가하여 수행하면 결과가 어떠할까?

string s3 = "Hello World";
Console.WriteLine("Reference equal ? s1 == s3 :: {0}", object.ReferenceEquals(s1, s3));

s1과 s3의 참조 비교의 결과는 True 이다. 이런 결과가 나오는 이유에 대해, 앞서 언급한 모 아티클에서는 이렇게 설명하고 있다.

.NET  Runtime는 내부적으로 모든 문자열의 참조를 해쉬테이블의 형태로 관리하는데 할당하고자하는 문자열이 이테이블에 이미 등록이 되있는지 확인하고 등록되있으면 기존의 참조를 반환한다.

'모든 문자열의 참조를 관리한다'는 이 설명은 틀린 것이 되겠다. 실제로는 런타임이 리터럴 문자열을 만나는 경우에만, 인턴 풀에서 해당 문자열 객체를 찾아서 해당 참조를 사용한다. 이런 이유로 s1과 s3의 참조가 동일한 문자열을 참조하고 있는 것이다. 그렇다면 다음과 같은 경우는 어떨까?

string s4 = "Hello" + " World";
Console.WriteLine("Reference equal ? s1 == s4 :: {0}", object.ReferenceEquals(s1, s4));

결과부터 말하면 이 경우에도 결과값은 True가 된다. 분명 s4는 "Hello" 문자열과 " World" 문자열의 연결 연산에 의해 이루어진 새로운 문자열이 아니던가? 아니다. -_- 컴파일러가 리터럴 문자열이 연결되는 경우에 연결된 리터럴로 만든다는 점은 StringBuilder에 관련된 포스트에 이미 언급했던바 이다. 위 코드는 string s4 = "Hello World" 처럼 컴파일 되어 버리는 것이다. 이 경우, 이 문자열은 인턴 풀에 이미 존재하는 문자열이며 따라서 s1과 참조 비교에서 같다는 결과가 나오는 것이다.

다음 테스트는 리터럴 문자열의 GC 여부를 테스트하는 코드이다.

string s1 = "Hello World";
string s2 = String.Concat("Hello ", "World");
WeakReference w1 = new WeakReference(s1);
WeakReference w2 = new WeakReference(s2);

s1 = null;
s2 = null;
GC.Collect();
Console.WriteLine("s1 is alive ? {0}", w1.IsAlive);
Console.WriteLine("s2 is alive ? {0}", w2.IsAlive);

WeakReference 클래스는 객체에 대한 약한 참조를 가지고 있다. GC가 힙상의 객체들을 가비지 컬렉션할 때 일반적인 참조(s1, s2 변수에 의한 참조와 같은 참조)를 받는 객체는 GC의 대상으로 삼지 않는다. 하지만 WeakReference 객체에 의해 참조되는 객체는 GC의 대상으로 삼아 버린다. 따라서 위 코드에서 s1, s2에 의해 참조되는 두 문자열을 약한 참조가 되도록 w1, w2에 할당하고 s1, s2의 참조를 제거한 후 GC를 수행한다. 그리고 w1, w2 가 여전히 실제 문자열 객체를 참조하고 있는가를 확인하는 코드이다.

이 코드의 결과는 w1의 참조는 여전히 남아 있으며 w2 참조는 남아 있지 않다. 즉, 리터럴 문자열에 대한 참조는 여전히 살아 있으며, 리터럴이 아닌 '만들어진' 문자열은 GC 되었음을 알 수 있다.

Methods for Intern Pool

String 클래스는 리터럴 문자열을 보관하고 있는 인턴 풀에 대한 메쏘드를 가지고 있다. IsInterned 메쏘드와 Intern 메쏘드가 그것인데, IsInterned 메쏘드는 해당 문자열이 인턴 풀에 존재하는지를 검사하여 존재한다면 해당 문자열의 참조를 반환하고 그렇지 않으면 null을 반환한다. 한편 Intern 메쏘드는 주어진 문자열이 인턴 풀에 존재하는지 검사하여 존재한다면 해당 문자열 참조를 반환하고 그렇지 않은 경우, 새로이 인턴 풀에 문자열을 추가한다. 즉, 런타임에도 인턴 풀에 문자열을 집어 넣을 수도 있는 것이다.

string s5 = String.Concat("Hello ", "World 2");
Console.WriteLine("s5 IsInterned ? {0}", String.IsInterned(s5) != null);  // 결과 : false
String.Intern(s5);
Console.WriteLine("s5 IsInterned ? {0}", String.IsInterned(s5) != null);  // 결과 : true

좋다. 인턴 풀이 뭔지 대충 알았다. 하지만 당췌 어디다 이딴 메쏘드를 써 먹느냐는 것이 중요하겠다. 사실 말하면 써먹을 데가 많지 않다. 다만... 문자열 값이 아닌 오로지 유일한 문자열 객체의 참조 값을 이용하여 비교 연산을 수행하고자 한다면, 그리고 문자열의 참조값(문자열 값이 아닌)에 의해 비교를 하고자 할 때 사용할 가치가 쬐끔 있겠다.

아는게 힘이다 ?

많은 경우에 아는 것은 힘이 된다. 하지만 모르는게 약이 되는 경우도 대단히 많다. 특히... 잘 못된 지식인 경우 차라리 그것을 모르는 것이 더 나을 때가 많다. 이번 포스트의 내용이 모르는게 약인 경우 중 하나가 될 가능성이 높다고 본다(뭐하러 길게도 글을 썼을까나... T_T). 인턴 풀... 이거 몰라도 프로그램 짜는데는 큰 지장이 없다... 걍... 잘못된 내용을 기술문서를 보고 분기 탱천하여 키보드를 잡았을 뿐이다.

걍 그런게 있다 보다 하고 나중에 정말 필요하다면 이곳을 다시 디비보시길 바라며...



Comments (read-only)
#re: 문자열 이야기 V : 인턴 풀(Intern Pool)이란? / 김범준 / 2005-10-18 오전 11:46:00
유용한 포스트 잘읽었습니다. 근데 GC 테스트 코드에서처럼
동적으로 생성된 문자열은 null로 참조 제거를 해줘야만 가비지 컬렉팅이 되는건지요?
사실 문자열 조합에 따른 메모리 복사 등의 문제보다 가비지 컬렉팅 여부 때문에
StringBuilder의 사용을 고려했었거든요. 모 책에서 StringBuilder는 가비지 컬렉팅이
되고 동적 생성 문자열은 Intern 메써드 등을 이용해야 가비지 컬렉팅이 된다고 해서요..
#re: 문자열 이야기 V : 인턴 풀(Intern Pool)이란? / 블로그쥔장 / 2005-10-18 오후 12:48:00
StringBuilder를 쓰건 string을 쓰건 가비지 컬렉션은 됩니다.
다만 변수나 필드등에 의해 참조가 유효한 객체는 가비지 컬렉션이 안됩니다.
(유효한 참조가 있는 객체를 가비지 컬렉션 할 수 없음은 당연하지요?)
예제 코드에서 null 로 설정한 이유는 GC.Collect() 호출이 효과를 보도록 하기 위함입니다.
만약 null 로 설정해 놓지 않고 GC.Collect()를 호출한다면 두 문자열이 모두 유효한 참조(s1, s2)를
갖기 때문에 GC 되지 않기 때문이지요.

그리고... 어떤 서적에소 보신 건지 궁금하지만, 본문에서도 지적한 바대로
Intern 된 문자열은 가비지 컬렉션이 되지 않습니다. Intern 되지 않은 문자열들은 가비지 컬렉션이 됩니다.
문자열 상수는 항상 Intern 된 문자열임을 잘 기억하실 필요가 있습니다.
#re: 문자열 이야기 V : 인턴 풀(Intern Pool)이란? / 김범준 / 2005-10-18 오후 1:05:00
감사합니다. 글을 읽으면서 제가 좀 착각했던게 있었네요.
그리고.. Intern에 대한 예기는 이런겁니다.

string str = "테스트";
string str1 = "테스트1";
string str2 = str + "1";

이럴경우 str2는 동적으로 생성되면서 str1과 값은 같지만 참조가 다른데
str2 = string.Intern(str2);
이렇게 해주면 str1과 str2의 참조가 같아지면서 이전에 동적으로 생성된
str2의 참조는 가비지 컬렉팅 된다는 예기였습니다.
이제 머리속에서 명확하게 정리가 된듯 합니다..
#re: 문자열 이야기 V : 인턴 풀(Intern Pool)이란? / 블로그쥔장 / 2005-10-18 오후 1:20:00
넵!! 바로 그겁니다. 정확하게 이해하고 계시는 군요 !!!
#re: 문자열 이야기 V : 인턴 풀(Intern Pool)이란? / masoman / 2006-08-24 오후 7:26:00
글 재밌게 쓰셔서 웃다 갑니다^^
#re: 문자열 이야기 V : 인턴 풀(Intern Pool)이란? / 블로그쥔장 / 2006-08-25 오전 10:05:00
글을 재밌게 쓰려고 노력하는데... 잘 안되네요... 음냐.. -_-;
#re: 문자열 이야기 V : 인턴 풀(Intern Pool)이란? / 남처리... / 2007-06-15 오후 4:17:00
string str1 = string.Empty;
strsing str2 = "";
이것에 대한 차이를 알아보다가 데브보고 여기에 왔서 잘 보고 갑니다... 아직 저에겐 100% 이해가 안되는 이야기이지만...
그래도 잘 보고 갑니다...!
#re: 문자열 이야기 V : 인턴 풀(Intern Pool)이란? / 조승태 / 2008-03-12 오후 4:39:00
재밌는 글, 좋은 글 잘 보고 갑니다. ^^