문자열은 왜 불변인가?
문자열은 보통 불변입니다.
대부분의 언어에서 문자열은 불변(Immutable)입니다. 변하지 않는다고 하면 이해하기 어려워하는 사람들이 있을텐데. 정확하게 말하면 문자열을 수정할 때, 새로운 문자열을 복사해서 만든다는 이야기다. 이걸 Copy on Write 라고도 한다. 파이썬에서 가변(mutable)한 자료형인 List와 비교해서 알아보자.
# 문자열
a = "hello"
# 문자 List. 타입은 사실 List[str] 이지만...
b = ['h', 'e', 'l', 'l', 'o']
# 값을 복사한다.
a_copy = a
# 레퍼런스를 복사한다.
b_copy = b
# ", world"를 뒤에 붙인다.
a_copy += ", world"
# 마찬가지. 이번에는 겹 따옴표를 써봤다.
b_copy += [",", " ", "w", "o", "r", "l", "d"]
print(a)
# "hello" 원본은 그대로
print(a_copy)
# "hello, world" 복사본은 수정됐다.
print(b)
# ['h','e','l','l','o',',',' ','w','o','r','l','d'] 원본도 같이 변했다!!!
print(b_copy)
# ['h','e','l','l','o',',',' ','w','o','r','l','d']
변수는 메모리에 특정한 위치를 가리키는 레퍼런스를 들고 있다. 그런데 불변인 값은 메모리의 내용을 수정할 수 없다. 그래서 값을 수정하면 메모리에 새로운 위치에 값을 복사해서 쓰고, 변수에 새로운 위치를 가리키는 레퍼런스를 넣어놓는다. a_copy를 수정해도 a는 예전 위치를 가리키고 있기 때문에, 아무런 영향이 없다.
반면에 리스트는 메모리의 값을 수정할 수 있어서, 항상 같은 위치를 가리키고 있다. 그러니 복사한 다른 변수가 값을 수정하면 원본도 바뀌는 것입니다.
불변성의 성능 이슈.
사실 불변 자료형은 함수형 프로그래밍에서는 많이 쓰지만, 다른 언어에는 흔하지 않습니다. 파이썬에서도 List는 가변입니다.
불변 자료형은 매번 값을 새로 쓰기 때문에 성능에 불리한 면이 있었습니다. 내가 책을 하나 새로 샀다고 합시다. 가변 책장이라면 책을 새로 꽂으면 끝납니다. 하지만 불변 책장에서는 책장을 새로 만들어서 기존에 있던 책들을 모두 옮겨야 합니다...
실제로 매우 많은 문자열을 합칠 때, 이 문제는 심각한 고민입니다. Python에서 문자열을 합칠 때 앞에서 본 + 연산자를 쓸 수 있습니다. 한 두 개는 이래도 괜찮지만, 문자열 수백 수천 개를 합칠 때에는 같은 문자열을 지우고 만들기를 수백 수천 번 반복하니 비효율적일 수 밖에 없습니다.
그래서 Python에서는 이렇게 많은 문자열을 합칠 때에는 join을 쓰기를 권장합니다. join은 미리 모든 문자열을 합칠 수 있는 공간을 만들어두고, 한 번에 새 문자열을 만들기 때문에 빠릅니다. JS도 비슷하게 for이나 reduce보다 join을 사용하라 합니다. Java에서는 StringBuilder나 StringBuffer를 추천합니다.
very_large_data_list = ["장원영", "홍다희", "나북희", ..., "김민주"]
result_1 = ""
for name in very_large_data_list:
# 합쳐서 새 문자열을 만든다.
line = name + "\n"
# result를 지우고 새 문자열을 또 만든다.
result_1 += line
# 한 번에 모든 문자열을 합친다.
result_2 = "\n".join(very_large_data_list)
하지만 애초에 String이 가변이었다면 이런 고민은 하지 않아도 됩니다. 그러면 왜 불변성을 유지하는 걸까요?
불변 자료구조는 해싱, 캐싱할 수 있고, 안전하다.
앞서 말했듯이 함수형 프로그래밍에서는 적극적으로 불변 자료구조를 쓴다. 심지어 Clojure나 Elixir 같은 언어에서는 기본적으로 불변 Linked List를 사용한다. (대부분 주류 언어에서는 ArrayList를 쓴다.) 흔히 함수형의 장점이라 알려진 것들이 불변성의 장점인 경우가 많다.
먼저 불변 값은 쉽게 Hash key로 쓸 수 있다. 문자열은 보통 HashMap이나 dict, HashSet 등등에서, key로 많이 쓰인다. 특히 js의 객체는 모두 HashTable로 되어 있다. 문자열이 가변이라면 매번 hash 값이 변할 수 있어서 믿을 수 없을 것입니다. 하지만 불변하는 값은 hash를 한 번 계산해두면 변할 일이 없어서 안심할 수 있다.
그래서 불변 자료구조는 캐싱으로 재사용하기 쉽다. 똑같은 값을 가진 새로운 데이터를 만들 때, 메모리에 새로 공간을 만들 필요가 없다. 원래 있던 변수 중에 똑같은 값을 찾아서 메모리에 똑같은 위치를 가리키는 레퍼런스만 달아주면 끝입니다. 예를 들어 "Hello, World" 라는 문자열 1천 개를 만든다면 가변 자료구조는 1천 개의 공간이 필요하지만, 불변 자료구조는 원본 하나 저장할 공간만 있으면 된다. 앞서 말한 hash함수를 이용하면 O(1) 만에 값을 찾을 수 있으니 성능 상으로도 유리하다.
또 멀티 쓰레드나 동시성을 쉽게 다룰 수 있다. 요즘처럼 멀티코어가 당연해지는 세상에서는 정말 중요한 성질입니다. 가변 상태는 디버깅하기 매우 귀찮기 때문입니다.
이런 이유로 파이썬에서는 str 뿐만 아니라 int, float, bool, tuple 같은 많은 기본 데이터 타입을 모두 불변 객체로 쓰고 있다.
이유는 이 밖에도 더 있겠지만. 일단 "불변이 생각보다 나쁘지 않구나, 함수형 언어도 좀 배워봐야겠다"는 정도만 알고 넘어가자.