BioPlayground

🧬
목록으로

문자열 불변성과 메모리 재할당

Python에서 문자열이 불변인 이유, 메모리에서 어떻게 동작하는지, 성능에 미치는 영향을 배웁니다.

중급
|
10
|
검증 완료 (2026-07)
문자열 불변성immutable메모리 할당인터닝id 함수
진행률0/12 (0%)

문자열 불변성과 메모리 재할당

이 토픽을 마치면

Python 문자열이 왜 불변(immutable)인지 이해하고, 문자열 연산이 메모리에서 어떻게 동작하는지 알며, 대량 문자열 처리 시 성능 함정을 피할 수 있습니다.


문자열은 수정할 수 없다

python
name = "Hello"
name[0] = "h"
# TypeError: 'str' object does not support item assignment

Python의 문자열은 불변(immutable) 객체입니다. 한 번 생성되면 내용을 바꿀 수 없습니다. "수정"처럼 보이는 모든 연산은 실제로 새 문자열을 생성합니다.

python
name = "Hello"
print(id(name)) # 4385123456
name = name.lower()
print(id(name)) # 4385123520 ← 다른 객체!

id()는 객체의 메모리 주소를 반환합니다. lower() 후에 id가 바뀌었으므로, 기존 문자열을 수정한 것이 아니라 새 문자열을 만든 것입니다.


가변 vs 불변

타입변경 가능예시
str불변"hello"
tuple불변(1, 2, 3)
int불변42
float불변3.14
list가변[1, 2, 3]
dict가변{"a": 1}
set가변{1, 2, 3}
python
# 리스트: 가변 — 내용을 직접 수정 가능
items = [1, 2, 3]
items[0] = 99
print(items) # [99, 2, 3] — 같은 객체, 내용만 변경
# 문자열: 불변 — 수정 불가, 새 객체 생성
text = "hello"
new_text = text.replace("h", "H")
print(text) # "hello" — 원본 그대로
print(new_text) # "Hello" — 새 객체

왜 불변으로 설계했는가

1. 해시 가능 — 딕셔너리 키로 사용

python
# 문자열은 딕셔너리 키로 사용 가능 (불변이므로)
scores = {"alice": 90, "bob": 85}
# 리스트는 딕셔너리 키로 사용 불가 (가변이므로)
# scores = {[1, 2]: "value"} # TypeError: unhashable type: 'list'

딕셔너리는 키의 해시값으로 데이터를 찾습니다. 키가 변경되면 해시값도 변하므로, 데이터를 찾을 수 없게 됩니다. 불변 객체만 해시 가능합니다.

2. 안전성 — 공유해도 괜찮음

python
def greet(name):
greeting = "Hello, " + name
return greeting
original = "World"
result = greet(original)
print(original) # "World" — 함수가 original을 수정할 수 없음

함수에 문자열을 전달해도, 함수 안에서 원본이 바뀔 걱정이 없습니다. 리스트는 함수 안에서 .append()로 원본이 수정될 수 있습니다.

3. 스레드 안전성

여러 스레드가 같은 문자열을 동시에 읽어도, 아무도 수정할 수 없으므로 락(lock)이 필요 없습니다.


문자열 인터닝

Python은 자주 쓰는 문자열을 재사용합니다. 이것을 인터닝(interning)이라고 합니다.

python
a = "hello"
b = "hello"
print(a is b) # True — 같은 객체를 공유!
print(id(a) == id(b)) # True
c = "hello world!"
d = "hello world!"
print(c is d) # False or True — 구현에 따라 다름

짧고 단순한 문자열(식별자 형태)은 인터닝됩니다. 같은 내용의 문자열이 여러 번 등장해도 메모리에 하나만 존재합니다. 이것은 불변이기 때문에 가능합니다 — 공유해도 누군가 수정할 수 없으니까요.

python
# is vs ==
a = "hello"
b = "hello"
print(a == b) # True — 값이 같은가?
print(a is b) # True — 같은 객체인가? (인터닝 때문)
# 문자열 비교는 항상 == 사용. is는 예측 불가능

성능 함정 — 문자열 연결

python
# Bad: 루프에서 문자열 연결 — O(n²)
result = ""
for i in range(10000):
result += str(i) + "," # 매번 새 문자열 생성!

매 반복마다 새 문자열을 만들고, 이전 내용을 복사합니다. 10,000번 반복하면 약 5천만 번의 문자 복사가 발생합니다 (1 + 2 + 3 + ... + 10000).

python
# Good: join 사용 — O(n)
result = ",".join(str(i) for i in range(10000))

join()은 최종 크기를 미리 계산하고, 메모리를 한 번만 할당합니다. 복사 횟수가 O(n)으로 줄어듭니다.

성능 비교

python
import time
# 방법 1: += (느림)
start = time.time()
result = ""
for i in range(100000):
result += str(i)
print(f"+=: {time.time() - start:.3f}s")
# 방법 2: join (빠름)
start = time.time()
result = "".join(str(i) for i in range(100000))
print(f"join: {time.time() - start:.3f}s")
# 방법 3: io.StringIO (대량 텍스트)
import io
start = time.time()
buf = io.StringIO()
for i in range(100000):
buf.write(str(i))
result = buf.getvalue()
print(f"StringIO: {time.time() - start:.3f}s")
# 결과 예시:
# +=: 0.852s
# join: 0.031s (27배 빠름)
# StringIO: 0.029s

실전 패턴

문자열 "수정" 방법들

python
text = "Hello, World!"
# 대소문자
text.upper() # "HELLO, WORLD!"
text.lower() # "hello, world!"
text.title() # "Hello, World!"
# 교체
text.replace("World", "Python") # "Hello, Python!"
# 문자 단위 수정 (리스트 경유)
chars = list(text) # ['H', 'e', 'l', 'l', 'o', ...]
chars[0] = 'h'
result = "".join(chars) # "hello, World!"

모든 메서드가 새 문자열을 반환합니다. 원본은 변하지 않습니다.

f-string 조합

python
name = "Hoon"
age = 30
greeting = f"Hello, {name}! You are {age} years old."

f-string은 내부적으로 한 번에 최종 문자열을 만듭니다. +로 여러 번 연결하는 것보다 효율적이고 가독성도 좋습니다.


다른 언어와의 비교

java
// Java — String은 불변, StringBuilder는 가변
String s = "Hello";
s = s + " World";  // 새 String 객체 생성

StringBuilder sb = new StringBuilder("Hello");
sb.append(" World");  // 같은 객체에서 수정 (가변)
String result = sb.toString();
javascript
// JavaScript — 문자열은 불변 (Python과 동일)
let s = "Hello";
s[0] = "h";      // 에러는 안 나지만 무시됨
console.log(s);  // "Hello" (변경 안 됨)
c
// C — char 배열은 가변
char s[] = "Hello";
s[0] = 'h';      // OK — "hello"로 변경됨

대부분의 현대 언어(Python, Java, JavaScript, Go, C#)에서 문자열은 불변입니다. 이것은 안전성과 최적화를 위한 공통된 설계 결정입니다. Java의 StringBuilder, Go의 strings.Builder는 Python의 join()이나 io.StringIO와 같은 역할 — 가변 버퍼에서 조립한 뒤 최종 불변 문자열을 만드는 패턴입니다.


핵심 정리

개념정리
불변(immutable)생성 후 내용 변경 불가. 모든 "수정"은 새 객체 생성
id()객체의 메모리 주소 확인
인터닝같은 문자열은 메모리에서 재사용 (불변이라 가능)
join()문자열 연결의 올바른 방법. += 루프는 O(n²)
is vs ==is는 객체 동일성, ==는 값 동등성. 문자열은 == 사용

문자열 불변성은 Python의 설계 결정입니다. 딕셔너리 키로 사용 가능, 함수 간 안전한 전달, 인터닝을 통한 메모리 최적화 — 이 이점들이 "수정할 수 없다"는 제약을 상쇄합니다. 실무에서 기억할 것은 하나: 루프에서 +=로 문자열을 쌓지 말고 join()을 쓸 것.