불변 자료구조가 비효율적으로 보이는데도 계속 쓰이는 이유

함수형 프로그래밍을 처음 접하면 불변 자료구조가 가장 먼저 낯설다. 값을 바꾸지 말고 새 값을 만들라고 하면, 직관적으로는 메모리를 낭비하는 방식처럼 보이기 때문이다.
하지만 실제 구현은 그렇게 단순하지 않다. 불변 자료구조가 실용적으로 쓰이는 이유는 전체 복사가 아니라 구조적 공유에 있다.
불변 자료구조는 “복사본을 매번 통째로 만든다”와 다르다
Clojure 공식 문서는 모든 컬렉션이 immutable and persistent, 즉 불변이고 영속적이라고 설명한다. 여기서 영속적이라는 말은 디스크에 영구 저장된다는 뜻이 아니라, 이전 버전을 유지하면서도 새 버전을 효율적으로 만들 수 있다는 뜻에 가깝다.
이 방식의 장점은 명확하다.
- 이전 상태를 그대로 비교할 수 있다.
- 다른 코드가 같은 데이터를 예상치 못하게 바꾸지 못한다.
- 상태 추적이 쉬워진다.
즉 불변성은 안전성을 위한 태도이고, 영속 자료구조는 그 태도를 너무 비싸지 않게 구현하는 기술이다.
핵심은 구조적 공유다
값 하나가 바뀌었다고 해서 전체 구조를 매번 새로 만드는 것은 비효율적이다. 그래서 불변 자료구조 구현체는 바뀌지 않은 부분을 공유한다. Clojure의 transient 문서도 내부적으로 배열을 바꾸고 나서 다시 불변한 값으로 되돌려 주는 식의 최적화를 설명한다.
이 말은 곧, 겉으로는 “새 값을 만든다”처럼 보여도 내부에서는 가능한 많은 부분을 재사용한다는 뜻이다. 그래서 불변성은 곧바로 “매번 O(N) 전체 복사”와 같지 않다.
불변성의 장점은 성능보다 먼저 예측 가능성에 있다
불변 자료구조가 계속 쓰이는 이유는 단지 빠르기 때문이 아니다. 더 중요한 이유는 예측 가능성이다.
- 함수가 받은 데이터를 몰래 바꾸지 않는다.
- 병렬 처리나 비동기 처리에서 공유 상태 문제를 줄인다.
- 디버깅할 때 값이 언제 바뀌었는지 추적하기 쉽다.
이 특성은 규모가 커질수록 더 중요해진다. 작은 스크립트에서는 가변 구조가 더 편할 수 있지만, 여러 모듈과 스레드가 얽히는 코드에서는 불변성이 오히려 비용을 줄인다.
현실적인 코드는 불변성과 가변성을 섞어 쓴다
이 점도 중요하다. 불변 자료구조를 쓴다고 해서 내부 최적화나 일시적 가변성을 완전히 금지하는 것은 아니다. Clojure의 transients나 C#의 record처럼, 바깥 API에서는 불변성을 유지하되 내부에서 효율적으로 처리하는 식의 절충이 흔하다.
Microsoft의 C# record 문서도 레코드가 주로 불변 데이터 모델을 위해 설계되었다고 설명한다. 결국 중요한 것은 “절대 가변 금지”가 아니라, 어디까지를 안전하게 고정하고 어디에서만 바꿀 것인가를 정하는 일이다.
핵심 정리
불변 자료구조는 매번 전체를 통째로 복사하는 비효율적 방식처럼 보이지만, 실제로는 영속 자료구조와 구조적 공유를 바탕으로 훨씬 더 실용적으로 동작한다. 그래서 불변성이 중요한 이유도 단순한 성능 경쟁보다, 상태를 더 예측 가능하게 만들고 코드의 안전성을 높이는 데 있다.
결국 불변성은 “아무것도 바꾸지 말자”는 금욕이 아니다. 무엇이 언제 어떻게 바뀌는지 더 분명하게 만들자는 설계 선택이다.