login register Sysop! about ME  
qrcode
    최초 작성일 :    2005년 07월 12일
  최종 수정일 :    2006년 10월 25일
  작성자 :    Loner (유경상)
  편집자 :    Loner(유 경상)
  읽음수 :    20,320

강좌 목록으로 돌아가기

필자의 잡담~

이번 문자열 이야기 포스트는 아티클 기반의 모 개발자 커뮤티니 사이트에서 황당(?)한 글을 읽어서 몇 글자 적어보려고 합니다. 해당글은 높은 평점을 받았음에도 불구하고 잘못된 정보를 기록하고 있었습니다. StringBuilder를 쓰는 것이 대부분 좋다 라든가, 닷넷이 모든 문자열에 대한 정보를 해쉬 테이블 형태로 기록해두고 동일 문자열이 사용되면 기록해 두었던 문자열을 사용한다든가 등등...

StringBuilder에 대한 이야기는 지난 문자열 이야기 씨리즈 1, 2, 3에서 이미 다루었으므로 이번엔 소위 리터럴 문자열이란 것과 인턴 풀(intern pool)에 대해 이야기 해보도록 하겠습니다.
현재 강좌의 원본 글의 링크는 http://www.simpleisbest.net/archive/2005/07/12/183.aspx 입니다.

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). 인턴 풀... 이거 몰라도 프로그램 짜는데는 큰 지장이 없다... 걍... 잘못된 내용을 기술문서를 보고 분기 탱천하여 키보드를 잡았을 뿐이다.

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


authored by


 
 
.NET과 Java 동영상 기반의 교육사이트

로딩 중입니다...

서버 프레임워크 지원 : NeoDEEX
based on ASP.NET 3.5
Creative Commons License
{5}
{2} 읽음   :{3} ({4})