본문 바로가기
카테고리 없음

문자열(String) 자료구조의 비밀 (불변성, 메모리)

by kguidebook0001 2026. 1. 31.

 

모든 프로그래밍 언어에서 가장 빈번하게 사용되는 자료구조를 꼽으라면 단연 문자열(String)일 것입니다. 하지만 아이러니하게도 가장 많이 사용되기에 가장 오해받기 쉬운 자료구조이기도 합니다. 많은 초급 개발자들이 문자열을 단순한 '문자들의 배열' 정도로 생각하고 무분별하게 사용하다가, 메모리 누수(Memory Leak)나 심각한 성능 저하를 겪곤 합니다. 특히 대규모 트래픽을 처리하는 서버 환경에서 문자열을 어떻게 다루느냐는 전체 시스템의 퍼포먼스를 결정짓는 중요한 척도가 됩니다. 이 글에서는 문자열이 왜 불변(Immutable)의 특성을 가지는지, 그리고 언어 차원에서 메모리를 효율적으로 관리하기 위해 어떤 비밀스러운 기술(String Pool)을 사용하는지 심층 분석합니다.

1. 문자열(String)의 본질: 단순 배열 그 이상

기본적으로 문자열은 연속된 메모리 공간에 저장된 문자(Character)들의 집합입니다. C언어와 같은 저수준 언어에서는 이를 `char` 배열로 직접 다루지만, Java, Python, C# 등 현대의 고수준 언어들은 문자열을 원시 타입(Primitive Type)이 아닌 참조 타입(Reference Type)의 객체로 관리합니다.

1-1. 참조 타입으로서의 특징

문자열 변수는 실제 데이터를 가지고 있는 것이 아니라, 데이터가 저장된 메모리 주소(Reference)를 가리킵니다. 이는 두 문자열 변수가 동일한 문장을 담고 있더라도, 생성 방식에 따라 서로 다른 메모리 주소를 가리킬 수 있음을 의미합니다. 이 차이를 이해하는 것이 문자열 최적화의 첫걸음입니다.

2. 불변성(Immutability): 왜 문자열은 변하지 않는가?

Java나 Python에서 한 번 생성된 문자열 객체는 절대로 변경할 수 없습니다(Immutable). 우리가 문자열을 수정하는 것처럼 보이는 연산(예: str + "abc")은 사실 기존 문자열을 수정하는 것이 아니라, 새로운 문자열 객체를 생성하여 재할당하는 과정입니다.

2-1. 불변성을 택한 기술적 이유

언어 설계자들이 굳이 수정 비용이 비싼 불변성을 택한 데에는 몇 가지 결정적인 이유가 있습니다.

  • 스레드 안전성(Thread-Safety): 데이터가 변하지 않기 때문에 멀티 스레드 환경에서 동기화(Synchronization) 처리 없이도 안전하게 공유할 수 있습니다.
  • 보안(Security): 데이터베이스 연결 정보, 파일 경로, 패스워드 등 민감한 정보가 문자열로 저장됩니다. 만약 참조를 통해 값이 변경 가능하다면, 보안 취약점이 발생할 수 있습니다.
  • 캐싱(Caching): 문자열이 불변이라면 해시코드(HashCode) 역시 변하지 않습니다. 이는 해시맵(HashMap)의 키(Key)로 문자열을 사용할 때 매번 해시를 계산할 필요 없이 캐싱된 값을 사용할 수 있게 하여 성능을 극대화합니다.

3. 메모리 최적화의 비밀: String Pool과 Interning

문자열의 불변성은 String Pool(문자열 상수 풀)이라는 독특한 메모리 관리 기법을 가능하게 합니다. 이는 동일한 문자열 리터럴을 매번 새로 생성하지 않고, 메모리 공간에 하나만 두고 재사용하는 기술입니다.

3-1. 리터럴 vs 객체 생성 (Java 예시)

이 메커니즘을 이해하기 위해 Java 코드를 예로 들어보겠습니다. Python의 interning 기법도 이와 매우 유사하게 동작합니다.


String str1 = "Hello";             // 리터럴 방식
String str2 = "Hello";             // 리터럴 방식
String str3 = new String("Hello"); // new 연산자 방식

// 주소값 비교 (==)
System.out.println(str1 == str2); // true (String Pool의 같은 객체 참조)
System.out.println(str1 == str3); // false (Heap 영역에 별도 생성됨)

위 코드에서 str1str2는 메모리를 절약하기 위해 String Pool에 있는 같은 객체를 바라봅니다. 반면 new 연산자를 사용한 str3는 강제로 힙(Heap) 메모리에 새로운 객체를 생성하므로 메모리 낭비가 발생합니다. 따라서 특별한 이유가 없다면 리터럴 방식을 사용하는 것이 좋습니다.

4. 성능 킬러와 해결책: StringBuilder의 활용

불변성은 안정성을 주지만, 빈번한 수정 연산에서는 치명적인 성능 저하를 일으킵니다. + 연산자를 반복문(Loop) 안에서 사용할 경우, 매 반복마다 새로운 객체를 생성하고 기존 데이터를 복사하는 과정이 발생하여 시간 복잡도가 O(n^2)까지 치솟을 수 있습니다.

4-1. 가변(Mutable) 객체의 사용

이 문제를 해결하기 위해 가변(Mutable) 특성을 가진 StringBuilder(Java)나 list 기반의 join(Python)을 사용해야 합니다. 이들은 내부 버퍼(Buffer)를 두어 객체 생성 없이 문자열을 직접 수정합니다.


// [Java] 나쁜 예: 매번 객체 생성
String result = "";
for (int i = 0; i < 1000; i++) {
    result += "*"; 
}

// [Java] 좋은 예: 내부 버퍼 활용
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append("*");
}
String fastResult = sb.toString();
[핵심 요약 : 문자열 자료구조의 비밀]
1. 불변성(Immutability): 문자열은 한 번 생성되면 변경되지 않으며, 이는 스레드 안전성과 캐싱 효율을 위한 설계입니다.
2. 메모리 관리: String Pool을 통해 동일한 리터럴을 재사용하여 메모리 낭비를 막습니다.
3. 성능 주의: 잦은 문자열 연산 시에는 + 연산자 대신 StringBuilder나 Buffer 기능을 활용해야 불필요한 객체 생성(GC 부하)을 방지할 수 있습니다.