문자의 집합인 문자열(string)은 어지간한 프로그래밍 언어들이 기본으로 제공해 주는 기본 중의 기본 자료형이지만, 그저 기초라고만 치부하기에는 처리하는 데 내부적으로 손이 많이 가기도 하는 자료형이다.

문자열은 그 특성상 배열 같은 복합(compound) 자료형의 성격이 다분하며, 별도의 가변적인 동적 메모리 관리가 필요하다. 또한 문자열을 어떤 형태로 메모리에 저장할지, 복사와 대입은 어떤 형태로 할지(값 내지 참조?) 같은 전략도 구현체에 따라서 의외로 다양하게 존재할 수 있다.

그래서 C 언어는 컴퓨터 자원이 열악하고 가난하던 어셈블리 시절의 최적화 덕후의 정신을 이어받아, 언어 차원에서 따로 문자열 타입을 제공하지 않았다. 그 대신 충분히 크게 잡은 문자의 배열과 이를 가리키는 포인터를 문자열로 간주했다. 그리고 코드값이 0인 문자가 문자열의 끝을 나타내게 했다.

그 이름도 유명한 null-terminated string이 여기서 유래되었다. 오늘날까지 쓰이는 역사적으로 뿌리가 깊은 운영체제들은 응당 어셈블리나 C 기반이기 때문에, 내부 API에서 다 이런 형태의 문자열을 사용한다.
그리고 파일 시스템도 이런 문자열을 사용한다. 오죽했으면 이를 위해 MAX_PATH (=260)같은 표준 문자열 길이 제약까지 있을 정도이니 말 다 했다. 그렇기 때문에 null-terminated string은 앞으로 결코 없어지지 않을 것이며 무시할 수도 없을 것이다.

딱히 문자열만을 위한 별도의 표식을 사용하지 않고 그저 0 문자를 문자열의 끝으로 간주하게 하는 방식은 매우 간단하고 성능면에서 효율적이다. 지극히 C스러운 발상이다. 그러나 이는 buffer overflow 보안 취약점의 근본 원인을 제공하기도 했다.

또한 이런 문자열은 태생적으로 문자열 자기 내부엔 0문자가 또 들어갈 수 없다는 제약도 있다. 하지만 어차피 사람이 사용하는 표시용 문자열에는 코드 번호가 공백(0x20)보다 작은 제어 문자들이 사실상 쓰이지 않기 때문에 이는 그리 심각한 제약은 아니다. 문자열은 어차피 문자의 배열과는 같지 않은 개념이기 때문이다.

문자열을 기본 자료형으로 제공하는 언어들은 대개 문자열을 포인터 형태로 표현하고, 그 포인터가 가리키는 메모리에는 처음에는 문자열의 길이가 들어있고 다음부터 실제 문자의 배열이 이어지는 형태로 구현했다. 그러니 문자열의 길이를 구하는 요청은 O(1) 상수 시간 만에 곧바로 수행된다. (C의 strlen 함수는 그렇지 않다)

그리고 문자열의 길이는 대개 machine word의 크기와 일치하는 범위이다. 다만, 과거에 파스칼은 이례적으로 문자열의 크기를 16비트도 아닌 겨우 8비트 크기로 저장해서 256자 이상의 문자열을 지정할 수 없다는 이상한 한계가 있었다. 더 긴 문자열을 저장하려면 다른 특수한 별도의 자료형을 써야 했다.

과거에 비주얼 베이직은 16비트 시절의 버전 3까지는 “포인터 → (문자열의 길이, 포인터) → 실제 문자열”로 사실상 실제 문자열에 접근하려면 포인터를 이중으로 참고하는 형태로 문자열을 구현했다. 어쩌면 VB의 전신인 도스용 QuickBasic도 문자열의 내부 구조가 그랬는지 모르겠다.

그러다가 마이크로소프트는 훗날 OLE와 COM이라는 기술 스펙을 제정하면서 문자열을 나타내는 표준 규격까지 제정했는데, COM 기반인 VB 4부터는 문자열의 포맷도 그 방식대로 바꿨다.

일단 기본 문자 단위가 8비트이던 것이 16비트로 확장되었다. 마이크로소프트는 자기네 개발 환경에서 ANSI, wide string, 유니코드 같은 개념을 한데 싸잡아 뒤죽박죽으로 재정의한 것 때문에 문자 코드 개념을 좀 아는 사람들한테서 많이 까이고 있긴 하다. 뭐, 재해석하자면 유니코드 UTF16에 더 가깝게 바뀐 셈이다.

OLE 문자열은 일단 겉보기로는 null-terminated wide string을 가리키는 포인터와 완전히 호환된다. 하지만 그 메모리는 OLE의 표준 메모리 할당 함수로만 할당되고 해제된다. (아마 CoTaskMemAlloc) 그리고 포인터가 가리키는 메모리의 앞에는 문자열의 길이가 32비트 정수 형태로 또 들어있기 때문에 문자열 자체가 또 0문자를 포함하고 있을 수 있다.

그리고 문자열의 진짜 끝부분에는 0문자가 1개가 아니라 2개 들어있다. 윈도우 운영체제는 여러 개의 문자열을 tokenize할 때 double null-termination이라는 희대의 괴상한 개념을 종종 사용하기 때문에, 이 관행과도 호환성을 맞추기 위해서이다.

2중 0문자는 레지스트리의 multi-string 포맷에서도 쓰이고, 또 파일 열기/저장 공용 대화상자가 사용하는 확장자 필터에서도 쓰인다. MFC는 프로그래머의 편의를 위해 '|'(bar)도 받아 주지만, 운영체제에다 전달을 할 때는 그걸 다시 0문자로 바꾼다. ^^;;;

요컨대 이런 OLE 표준 문자열을 가리키는 포인터가 바로 그 이름도 유명한 BSTR이다. 모든 BSTR은 (L)PCWSTR과 호환된다. 그러나 PCWSTR은 스택이든 힙이든 아무 메모리나 가리킬 수 있기 때문에 그게 곧 BSTR이라고 간주할 수는 없다. 관계를 알겠는가? BSTR은 SysAllocString 함수를 통해 생성되고 SysFreeString 함수를 통해 해제된다.

'내 문서', '프로그램 파일' 등 운영체제가 특수한 용도로 예정하여 사용하는 디렉터리를 구하는 함수로 SHGetSpecialFolderPath가 있다. 이 함수는 MAX_PATH만치 확보된 메모리 공간을 가리키는 문자 포인터를 입력으로 받았으며, 특수 폴더들을 CSIDL이라고 불리는 일종의 정수값으로 식별했다.

그러나 윈도우 비스타에서 추가된 SHGetKnownFolderPath는 폴더들을 128비트짜리 GUID로 식별하며, 문자열도 아예 포인터의 포인터 형태로 받는다. 21세기에 도입된 API답게, 이 함수가 그냥 메모리를 따로 할당하여 가변 길이의 문자열을 되돌려 준다는 뜻이다. 260자 제한이 없어진 것은 좋지만, 이 함수가 돌려 준 메모리는 사용자가 따로 CoTaskMemFree로 해제를 해 줘야 한다. SysFreeString이 아님. 메모리만 COM 표준 함수로 할당했을 뿐이지, BSTR이 돌아오는 게 아닌 것도 주목할 만한 점이다.

예전에 FormatMessage 함수도 FORMAT_MESSAGE_ALLOCATE_BUFFER 플래그를 주면 자체적으로 메모리가 할당된 문자열의 포인터를 되돌리게 할 수 있는데, 이놈은 윈도우 NT 3.x 시절부터 있었던 함수이다 보니, 받은 포인터를 LocalFree로 해제하게 되어 있다.

이렇게 운영체제 API 차원에서 메모리를 할당하여 만들어 주는 문자열 말고, 프로그래밍 언어가 제공하는 문자열은 메모리 관리에 대한 센스가 추가되어 있다. 대표적인 예로 MFC 라이브러리의 CString이 있다.

CString 자체는 BSTR과 마찬가지로 언뜻 보기에 PCWSTR 포인터 하나만 멤버로 달랑 갖고 있다. 그래서 심지어 printf 같은 문자열 format 함수에다가 "%s", str처럼 개체를 명시적인 형변환 없이 바로 넘겨 줘도 괜찮다(권장되는 프로그래밍 스타일은 못 되지만).

그런데 그 포인터의 앞에 있는 것이 단순히 문자열 길이 말고도 더 있다. 바로 레퍼런스 카운트와 메모리 할당 크기. 그래서 문자열이 단순 대입이나 복사 생성만 될 경우, 그 개체는 동일한 메모리를 가리키면서 레퍼런스 카운트만 올렸다가, 값이 변경되어야 할 때만 실제 값 복사가 일어난다. 이것을 일명 copy-on-modify 테크닉이라고 하는데, MFC 4.0부터 도입되어 오늘날에 이르고 있다. 이는 상당히 똑똑한 정책이기 때문에 이것만 있어도 별도로 r-value 참조자 대입 최적화가 없어도 될 정도이다.

메모리 할당 크기는 문자열에 대해 덧셈 같은 연산을 수행할 때 메모리 재할당이 필요한지를 판단하기 위해 쓰이는 정보이다. MFC는 표준 C 라이브러리에 의존적이기 때문에 이때는 응당 malloc/free가 쓰인다. 재할당 단위는 보통 예전에 비해 배수 단위로 기하급수적으로 더 커진다.

CString이 그냥 포인터와 크기가 같은 반면, 표준 C++ 라이브러리에 존재하는 string 클래스는 비주얼 C++ 2010 x86 기준 개체 하나의 크기가 28바이트나 된다. 길이가 16 이하인 짧은 문자열은 그냥 자체 배열에다 담고, 그보다 긴 문자열을 담을 때만 메모리를 할당하는 테크닉을 쓰기 때문이다. 그리고 대입이나 복사를 할 때마다 CString 같은 reference counting을 하지 않고, 일일이 메모리 재할당과 값 복사를 한다.

글을 맺겠다.
C/C++이 까이는 여러 이유 중 하나는 라이브러리가 지저분하고 동일 기능의 중복 구현이 너무 많아서 혼란스럽다는 점이다. 문자열도 그 범주에 정확하게 속하는 요소일 것이다. 메모리 할당과 해제 자체부터가 구현체 중복이 한둘이 아니니... 어지간히 덩치와 규모가 있는 프레임워크 라이브러리는 그냥 자신만의 문자열 클래스 구현체를 갖고 있는 게 이상한 일이 아니다. 하지만 그건 C/C++이 쓰기 편리한 고급 언어와 시스템 최적화 오덕질이라는 두 토끼를 모두 잡으려다 어쩔 수 없이 그리 된 것도 강하다.

문자열에 대한 이야기 중에서 일부는 내가 예전 블로그 포스트에서도 한 것도 있지만, 이번 글에 처음으로 언급한 내용도 많을 것이다. 프로그래밍 언어 중에는 문자열을 다루기가 기가 막히게 편리한 것이 있는데, 그런 것도 내부적으로는 다 결국은 컴퓨터가 무진장 고생해서 결과물을 만들어 내는 것이다.
컴퓨터가 받아들이고 뱉어내는 문자열들이 내부적으로 어떤 구현체에 의해 어떤 처리를 거치는지를 생각해 보는 것도 프로그래머로서는 의미 있는 일일 것이다.

Posted by 사무엘

2012/10/13 08:26 2012/10/13 08:26
, , , ,
Response
No Trackback , 8 Comments
RSS :
http://moogi.new21.org/tc/rss/response/743

Trackback URL : http://moogi.new21.org/tc/trackback/743

Comments List

  1. 주의사신 2012/10/13 09:40 # M/D Reply Permalink

    1. 조엘 온 소프트웨어를 읽다 보면 저자가 해 본 프로젝트 중에서 CF***edString이라는 자료형이 있었다고 합니다. 얼마나 만드는 사람이 화가 났으면 이름을 그렇게 지었을까 싶은 문자열 클래스입니다.

    2. C++에 이제는 문자열 자료형을 포함하지 않으면 안 되는 이유가 없을 것 같은데, 어떠한 이유인지 전혀 관련된 논의를 하지 않는 것 같더군요.

    3. std::string의 경우에는 modify-on-copy를 하지 않는데, 이렇게 하는 이유가 Multithread 프로그래밍을 하게 되면, string 관련 연산을 할 때 Lock을 자주 걸어 주어야 해서 성능 저하가 엄청나기 때문에 그랬다고 합니다. (Code Craft라는 책에 그렇게 나와 있었습니다.)

    1. 사무엘 2012/10/13 10:10 # M/D Permalink

      1. 하하하!!! 흠좀무스러운 작명이군요.

      2. string은 C++ 라이브러리에 포함된 것으로도 감지덕지지, 개념적으로 customization의 여지가 너무 많아서 C++ 같은 언어의 기본 자료형으로 들어가는 건 이제 무리일 것 같습니다. 그리고 넣어 봤자 이제는 이미 중복 구현체들이 역할을 대신 수행하고 있고요.
      당장 wchar_t만 해도 int만큼이나 크기가 들쭉날쭉.. Windows에서는 UTF16이 짱입니다만, 유닉스 계열로 가면 아예 1글자당 4바이트인 UTF32도 많이 쓰이지요?

      3. std::string이 그렇게 설계된 것은 스레드 안전성 같은 걸 차지하고라도, 문자열을 뭔가 serious한 객체라기보다는 primitive한 약간 덩치 큰 기본 자료형으로 간주한 사상이 크게 작용해서인 것 같습니다.

      아무튼, 문자열이라는 물건은 프로그래머로서 생각할 주제가 굉장히 많은 주제임은 분명합니다. ^^

  2. kippler 2012/10/13 12:51 # M/D Reply Permalink

    * 이러니 저러니 해도 저는 개인적으로 CString 이 제일 좋더군요. 효율성도 좋고, %s 파라메터로 쓸 수 있는것도 좋고, 편리한 메쏘드도 많이 갖춰져 있고..

    * windows 2000 이전은 UCS2 였고, 2000 부터는 공식적으로 UTF-16이라고 하더군요. 사실 그 당시는 아직 유니코드가 UCS2 만 있고, UCS4 는 없던 시절이라....

    * 그리고 utf16 이 유닉스의 ucs4 보다는 확실히 편한듯 합니다. 어차피 ucs2 를 넘어가는 문자열 다룰일이 많지 않고, 메모리도 절약되니깐요.

    * 고급언어만 쓰던 개발자는 문자열 타입을 추상적으로 생각하는 경우가 많더군요. 그래서 replace 같은 함수가 얼마나 cpu를 많이 쓰는지에 대해서는 생각해 본 적이 없는 경우도 많고요. 그래서 신입사원 뽑을때 문자열 관련 함수를 직접 구현해 보게 시키는것(strlen, strcpy..) 이 c 의 로우레벨이나 포인터에 대한 이해도를 측정하기 좋더군요.

    1. 사무엘 2012/10/13 21:25 # M/D Permalink

      앗, 유명하신 그분께서...! (형님, 반갑습니다.. 이렇게 불러 드려도 되죠? ^^)
      저도 문자열 클래스라는 걸 제일 먼저 접한 게 CString이다 보니, 그 디자인이 가장 무난하고 마음에 드는 것 같습니다. 사실, thread safety라는 게 절대적으로 보장돼야 하는 상황은 그리 많지 않거든요. 멀티스레딩을 하는 상위 소프트웨어 계층에서 동기화를 알아서 해 줘야겠죠. 굳이 포인터 단위의 저수준 조작이 필요하면 GetBuffer 함수를 써서 내부 버퍼를 변형 가능한 형태로 잠시 까 볼 수도 있다는 점도 더욱 좋습니다.

      UCS와 UTF 사이의 관계는 말씀하신 대로이구요. 진짜로 UC2 범위를 넘어가는 문자를 일상생활에서 접할 일은 거의 없는 게 사실이죠. 아무리 메모리가 남아 돈다고 해도, 한 글자당 32비트는 솔직히 정서적으로 기억장소 낭비가 너무 심한 감이 있죠.

      또한, 마치 실수가 정수보다 다루기 힘들듯, 문자열은 단순 문자나 숫자에 비해 컴퓨터가 다루기 힘들어하는 타입이라는 것을 프로그래머라면 알 필요가 있습니다. ^^;;

  3. 김 기윤 2012/10/13 13:13 # M/D Reply Permalink

    옛날부터 단순히 char* 또는 char[] 형 만 계속해서 다루다 보니까
    std::string 이나 CString 같은 것을 쓰다가도 문자열 조작을 하려면
    c_str() 같은 것으로 char* 로 변환 한 뒤 변환하고 다시 원래대로 되돌리는 등의 연산을 하는
    삽질(..)을 하는 저입니다.

    std::string 이나 CString 쪽도 익숙해져야 할텐데, 그냥 버릇이 버릇이다보니 기회를 잡지를 못하고 있네요..;

    1. 사무엘 2012/10/13 21:25 # M/D Permalink

      포인터 덕질을 하다가 string 클래스로 갈아타자니, 내가 원하는 조작을 마음대로 못 하는 게 좀 마음에 걸리기도 하지요.
      저의 <날개셋> 한글 입력기도 자체적인 문자열 클래스 구현체가 있는데요, 주어진 구간의 문자열을 특정 문자열로 대체하는 Substitute 함수가 있고(대체 대상 문자열은 굳이 null-terminate가 아니어도 됨!), Format된 문자열을 기존 문자열의 앞이나 뒤에다 추가하는 FormatBefore, FormatAfter라는 함수를 제 식대로 만들어서 제공하고 있답니다.

  4. Lyn 2012/10/13 15:47 # M/D Reply Permalink

    copy-on-modify 보다는 copy-on-write 가 좀더 일반적으로 쓰이는 명칭일듯 합니다

    1. 사무엘 2012/10/13 21:25 # M/D Permalink

      제가 CString 클래스의 메커니즘을 처음으로 알게 된 곳이 바로 MSDN에서 전설적인 개발자였던 Paul DiLascia의 글인데요, 거기서 처음부터 'modify'라는 용어를 쓰더군요. 저는 그냥 그 영향을 받은 것입니다.

      http://www.microsoft.com/msj/archive/S1F0A.aspx
      First off, if you're not using CStrings, shame on you! Come on guys, the year 2000 is almost upon us!

      “세상에 아직도 CString도 모르다니, 부끄러운 줄 아셈!”... 1996년에 작성된 글입니다. ㅎㅎ

Leave a comment
« Previous : 1 : ... 860 : 861 : 862 : 863 : 864 : 865 : 866 : 867 : 868 : ... 1521 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2019/07   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      

Site Stats

Total hits:
1221102
Today:
287
Yesterday:
474