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)가 끼면 런타임 연결이라 새 객체. ReferenceEqualsvs==:ReferenceEquals는 두 참조가 같은 객체인지(인터닝 여부에 민감).==는 string에서 값 비교로 오버로드 → 내용이 같으면 True.string.Intern: 주어진 문자열과 값이 같은 풀의 정규 인스턴스를 반환(없으면 추가). 그래서Intern(d)는a와 같은 참조가 된다.
단계별 내부 동작
- (1)
a,b는 같은 리터럴"guild_master"→ 컴파일 타임에 인턴 풀의 동일 인스턴스를 가리킴 → 참조 동일 → True. - (2)
c = "guild_" + "master"는 두 피연산자가 상수라 컴파일러가"guild_master"로 폴딩 →a와 같은 인턴 인스턴스 → True. - (3)
d = prefix + "master"는 변수prefix가 끼어 런타임에 새 string을 힙에 생성 → 인턴 풀과 무관한 별도 객체 →ReferenceEquals(a, d)False. - (4)
a == d: string의==는 값 비교라 내용("guild_master")이 같으므로 True. - (5)
string.Intern(d)는 풀에서 값이 같은 정규 인스턴스(=a)를 찾아 반환 →ReferenceEquals(a, Intern(d))True. - (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은 본질적으로 스레드 안전하고 딕셔너리 키로 안전(해시·내용이 안 바뀜)하다 — 이게 불변의 큰 이점이자 매 수정 할당이라는 대가의 근거.
면접 포인트
- string은 불변 참조 타입 + 리터럴/상수 인터닝.
ReferenceEquals는 인터닝 여부에 민감,==는 값 비교(오버로드). 런타임 연결은 인턴되지 않는다. - 반복
+=는 불변성 때문에 매번 새 string 복사 → O(N²)·가비지. 핫 패스는StringBuilder/string.Join. - 불변성의 이점: 스레드 안전 공유·딕셔너리 키 안정성·인터닝 가능, 대가는 매 "수정"마다 할당.