← 블로그 목록

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

불변 자료구조는 매번 전체를 복사하는 비효율적인 방식처럼 보이지만, 실제 구현은 구조적 공유와 영속 자료구조를 바탕으로 훨씬 더 실용적으로 동작한다.

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

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

함수형 프로그래밍을 처음 접하면 불변 자료구조가 가장 먼저 낯설다. 값을 바꾸지 말고 새 값을 만들라고 하면, 직관적으로는 메모리를 낭비하는 방식처럼 보이기 때문이다.

하지만 실제 구현은 그렇게 단순하지 않다. 불변 자료구조가 실용적으로 쓰이는 이유는 전체 복사가 아니라 구조적 공유에 있다.


불변 자료구조는 “복사본을 매번 통째로 만든다”와 다르다

Clojure 공식 문서는 모든 컬렉션이 immutable and persistent, 즉 불변이고 영속적이라고 설명한다. 여기서 영속적이라는 말은 디스크에 영구 저장된다는 뜻이 아니라, 이전 버전을 유지하면서도 새 버전을 효율적으로 만들 수 있다는 뜻에 가깝다.

이 방식의 장점은 명확하다.

즉 불변성은 안전성을 위한 태도이고, 영속 자료구조는 그 태도를 너무 비싸지 않게 구현하는 기술이다.


핵심은 구조적 공유다

값 하나가 바뀌었다고 해서 전체 구조를 매번 새로 만드는 것은 비효율적이다. 그래서 불변 자료구조 구현체는 바뀌지 않은 부분을 공유한다. Clojure의 transient 문서도 내부적으로 배열을 바꾸고 나서 다시 불변한 값으로 되돌려 주는 식의 최적화를 설명한다.

이 말은 곧, 겉으로는 “새 값을 만든다”처럼 보여도 내부에서는 가능한 많은 부분을 재사용한다는 뜻이다. 그래서 불변성은 곧바로 “매번 O(N) 전체 복사”와 같지 않다.


불변성의 장점은 성능보다 먼저 예측 가능성에 있다

불변 자료구조가 계속 쓰이는 이유는 단지 빠르기 때문이 아니다. 더 중요한 이유는 예측 가능성이다.

이 특성은 규모가 커질수록 더 중요해진다. 작은 스크립트에서는 가변 구조가 더 편할 수 있지만, 여러 모듈과 스레드가 얽히는 코드에서는 불변성이 오히려 비용을 줄인다.


현실적인 코드는 불변성과 가변성을 섞어 쓴다

이 점도 중요하다. 불변 자료구조를 쓴다고 해서 내부 최적화나 일시적 가변성을 완전히 금지하는 것은 아니다. Clojure의 transients나 C#의 record처럼, 바깥 API에서는 불변성을 유지하되 내부에서 효율적으로 처리하는 식의 절충이 흔하다.

Microsoft의 C# record 문서도 레코드가 주로 불변 데이터 모델을 위해 설계되었다고 설명한다. 결국 중요한 것은 “절대 가변 금지”가 아니라, 어디까지를 안전하게 고정하고 어디에서만 바꿀 것인가를 정하는 일이다.


핵심 정리

불변 자료구조는 매번 전체를 통째로 복사하는 비효율적 방식처럼 보이지만, 실제로는 영속 자료구조와 구조적 공유를 바탕으로 훨씬 더 실용적으로 동작한다. 그래서 불변성이 중요한 이유도 단순한 성능 경쟁보다, 상태를 더 예측 가능하게 만들고 코드의 안전성을 높이는 데 있다.

결국 불변성은 “아무것도 바꾸지 말자”는 금욕이 아니다. 무엇이 언제 어떻게 바뀌는지 더 분명하게 만들자는 설계 선택이다.

참고 자료

← 목록으로
Related

함께 읽으면 좋은 글

함수형 프로그래밍게임 개발불변성
함수형 프로그래밍이 게임 개발에서 유용한 곳은 상태를 없애는 곳이 아니라 상태 변화를 분리하는 곳이다

함수형 프로그래밍은 게임 코드를 전부 다시 쓰라는 명령이 아니다. 불변 데이터와 순수 함수의 관점을 이용해 규칙 계산, 상태 전이, 서버 메시지 처리처럼 변화를 분리해야 하는 영역을 더 다루기 쉽게 만든다.

함수형 프로그래밍재귀순수 함수
함수형 프로그래밍에서 재귀가 중요한 이유는 루프를 금지해서가 아니라 상태를 드러내기 위해서다

함수형 프로그래밍에서 재귀는 루프의 대체재라기보다 상태 변화를 인자로 드러내는 방식에 가깝다. 특히 꼬리 재귀와 누산기 패턴을 이해하면 이 차이가 분명해진다.

객체지향상속델리게이트
상속을 줄이고 델리게이트와 시그널로 푸는 이유

상속은 ‘무엇의 하위 타입인가’를 표현하는 데 강하지만, UI 이벤트나 객체 간 통신처럼 호출 관계가 자주 바뀌는 영역에서는 델리게이트나 시그널과 슬롯 같은 느슨한 연결이 더 잘 맞는다. C# 델리게이트와 Qt 시그널의 사례를 빌려, 좋은 설계는 상속을 줄이는 신념이 아니라 ‘타입 관계’와 ‘실행 시 연결 관계’를 분리해 보는 습관에서 나온다는 점을 정리한다.