← 문제로

4. string 불변성(immutability)과 인터닝(interning)

난이도 중
내 리뷰 · C#
해설 · C#

해설 — string 불변성(immutability)과 인터닝(interning)

난이도: 중

요약

C#의 string불변(immutable) 참조 타입이다. 같은 내용이라도 "같은 객체"인지는 인터닝에 달렸다: 컴파일 타임 리터럴/상수는 인턴 풀에 모여 하나의 인스턴스를 공유하지만, 런타임에 만들어진 문자열은 자동 인터닝되지 않는다. ReferenceEquals는 참조(같은 객체) 비교, ==는 string에서 값 비교로 오버로드된다. 불변이라 += 한 번마다 새 문자열을 할당하므로 반복 연결은 비싸고, StringBuilder가 가변 버퍼로 이를 해결한다.

출력 예측

(1) ReferenceEquals(a, b)              → True    // 같은 리터럴 → 인턴 풀에서 동일 인스턴스
(2) ReferenceEquals(a, c)              → True    // "guild_"+"master" 는 컴파일타임 상수 폴딩 → 같은 리터럴로 인턴
(3) ReferenceEquals(a, d)              → False   // d 는 런타임 연결 → 새 힙 문자열, 인터닝 안 됨
(4) a == d                             → True    // string == 는 값 비교(내용 동일)
(5) ReferenceEquals(a, string.Intern(d)) → True  // Intern(d) 는 풀의 정규 인스턴스(=a) 반환

핵심 개념

  • 불변성: 한 번 만든 string의 문자는 바뀌지 않는다. "수정"처럼 보이는 연산(+, Substring, Replace)은 모두 새 string을 만든다.
  • 인터닝(intern pool): CLR은 컴파일 타임 문자열 리터럴을 풀에 모아 동일 내용 리터럴이 하나의 인스턴스를 공유하게 한다. 메모리 절약 + 참조 비교 가능.
  • 컴파일타임 상수 폴딩: "guild_" + "master" 처럼 모든 피연산자가 상수면 컴파일러가 미리 "guild_master"로 접고 리터럴로 인턴한다. 반면 변수(prefix)가 끼면 런타임 연결이라 새 객체.
  • ReferenceEquals vs ==: ReferenceEquals는 두 참조가 같은 객체인지(인터닝 여부에 민감). ==는 string에서 값 비교로 오버로드 → 내용이 같으면 True.
  • string.Intern: 주어진 문자열과 값이 같은 풀의 정규 인스턴스를 반환(없으면 추가). 그래서 Intern(d)a와 같은 참조가 된다.

단계별 내부 동작

  1. (1) a, b는 같은 리터럴 "guild_master" → 컴파일 타임에 인턴 풀의 동일 인스턴스를 가리킴 → 참조 동일 → True.
  2. (2) c = "guild_" + "master"는 두 피연산자가 상수라 컴파일러가 "guild_master"로 폴딩 → a와 같은 인턴 인스턴스 → True.
  3. (3) d = prefix + "master"는 변수 prefix가 끼어 런타임에 새 string을 힙에 생성 → 인턴 풀과 무관한 별도 객체 → ReferenceEquals(a, d) False.
  4. (4) a == d: string의 ==는 값 비교라 내용("guild_master")이 같으므로 True.
  5. (5) string.Intern(d)는 풀에서 값이 같은 정규 인스턴스(=a)를 찾아 반환 → ReferenceEquals(a, Intern(d)) True.
  6. (6) 반복 +=: result += tokens[i] + ","는 매 반복마다 기존 result 전체를 복사해 더 긴 새 string을 만든다(불변이라 제자리 수정 불가). N개 토큰이면 길이가 1,2,3,…,N으로 커지며 누적 복사량이 O(N²) 에 가깝고, 매 반복이 가비지(중간 string) 를 남겨 GC 압박이 된다. StringBuilder는 내부 가변 char 버퍼(필요 시 분할 증가)에 이어붙여 복사·할당을 줄여 O(N) 으로 만든다.

흔한 오해·함정

  • "내용이 같으면 ReferenceEquals도 True" → 인터닝된 리터럴끼리만. 런타임 생성 문자열은 보통 False. 동등성 판단엔 ==/Equals를 써라(참조 비교에 의존 금지).
  • "==는 참조 비교" → object는 그렇지만 string은 값 비교로 오버로드된다.
  • "인터닝하면 항상 이득" → 인턴 풀은 프로세스 수명 동안 유지되어 많은 동적 문자열을 인턴하면 메모리가 안 줄어든다. 남용 금지.
  • 반복 연결을 +=로 → 핫 패스(로그/직렬화/채팅)에서 O(N²)·가비지. StringBuilder/string.Join/Span 기반 포맷 사용.
  • 불변이라 string은 본질적으로 스레드 안전하고 딕셔너리 키로 안전(해시·내용이 안 바뀜)하다 — 이게 불변의 큰 이점이자 매 수정 할당이라는 대가의 근거.

면접 포인트

  1. string은 불변 참조 타입 + 리터럴/상수 인터닝. ReferenceEquals는 인터닝 여부에 민감, ==는 값 비교(오버로드). 런타임 연결은 인턴되지 않는다.
  2. 반복 +=는 불변성 때문에 매번 새 string 복사 → O(N²)·가비지. 핫 패스는 StringBuilder/string.Join.
  3. 불변성의 이점: 스레드 안전 공유·딕셔너리 키 안정성·인터닝 가능, 대가는 매 "수정"마다 할당.