TECH 으로 돌아가기
TECH HACKER NEWS 오늘 6분 읽기 21 READS

가변과 불변은 왜 서로의 하위 타입이 될 수 없는가

프로그래밍 언어 포럼에서 주기적으로 반복되는 질문이 있다. 왜 많은 언어들이 어떤 자료구조의 불변(immutable) 버전과 가변(mutable) 버전을 서로의 하위 타입이나 상위 타입으로 제공하지 않느냐는 것이다. 얼핏 자연스러워 보이는 요구지만, 이렇게 설계하는 것은 불가능하지는 않아도 형식적으로 올바르지 않으며, 언어가 평소 제공하던 타입 검사 보장 중 일부를 잃게 만든다. 실무에서 컬렉션의 읽기 전용 뷰와 수정 가능한 뷰를 다뤄본 개발자라면 한 번쯤 부딪혀봤을 문제다.

리스코프 치환 원칙이라는 출발점

핵심은 하위 타입의 정의에 있다. 리스코프 치환 원칙에 따르면, 타입 T의 값이 기대되는 모든 문맥에서 타입 S의 값을 쓸 수 있을 때 S는 T의 하위 타입이다. 형식적인 논의가 늘 그렇듯 여기서 '모든'은 문자 그대로 모든 경우를 뜻하며, 대부분이 아니다. 객체지향 수업에서 이를 권장 설계 패턴 정도로 배운 사람도 많지만, 형식적으로 참된 하위 타입은 이 기준을 예외 없이 충족해야 한다. 하위 타이핑을 지원하는 정적 타입 시스템은 프로그램을 통과시키기 위해 이 조건을 실제로 증명해내야 한다.

원문은 가장 단순한 복합 자료구조인 '쌍(pair)'으로 이를 설명한다. 불변 쌍에는 car, cdr 같은 조회 연산이 있고, 가변 쌍은 여기에 값을 바꾸는 두 개의 변경 연산과 새로운 생성자를 더한다. 불변 쌍을 가변 쌍이 필요한 자리에 넣을 수 없다는 것은 분명하다. 그 자리에서는 변경 연산을 쓰려 할 텐데 불변 쌍에는 그 연산이 정의되어 있지 않으니 타입 오류가 난다.

계약이 다르기 때문이다

그렇다면 반대 방향은 왜 안 될까. 가변 쌍은 불변 쌍의 모든 연산을 갖고 있으니 불변 쌍이 필요한 자리에 넣어도 될 것처럼 보인다. 이유는 더 미묘하다. 치환 원칙은 타입이 제공하는 연산의 집합뿐 아니라 그 연산에 딸린 암묵적 계약(contract)까지 확장되어 적용되기 때문이다. 불변 쌍의 car나 cdr을 호출하면 언제 호출하든 같은 결과가 나온다는 계약을 신뢰할 수 있다. 이 계약 덕분에 내용을 기반으로 해시값을 계산해 다른 자료구조에 저장해두고, 나중에 다시 계산해 조회해도 값이 달라지지 않으리라 확신할 수 있다. 다시 말해 불변성은 해시 콘싱(hash consing)의 전제 조건이다.

가변 쌍은 이 계약을 지키지 못한다. 언제든 내용이 바뀔 수 있으므로 조회 결과가 항상 같으리라는 보장이 깨진다. 따라서 두 종류의 쌍은 하위 타입 관계를 이룰 수 없고, 완전히 별개의 타입으로서 각자 별도의 연산 집합을 가져야 한다. 이는 단순한 구현 편의의 문제가 아니라, 타입이 보장하는 불변식을 지키기 위한 필연적 결론이다.

임시 다형성이라는 우회로

다행히 대부분의 언어는 계층 관계를 이루지 않는 여러 타입에 같은 연산을 정의하는 임시 다형성(ad hoc polymorphism) 메커니즘을 제공한다. 정적 타입 쪽에서는 와들러와 블롯이 이를 형식적으로 다루고 타입 검사기가 건전성을 증명하도록 하는 방법을 제시했다. 그들의 용어로 가변 쌍과 불변 쌍은 서로 다른 타입이지만, car와 cdr을 연산으로 갖는 공통의 쌍 타입 클래스에 둘 다 속할 수 있다. 이 연산은 가변 쌍에서는 내부의 가변 조회 연산으로, 불변 쌍에서는 불변 조회 연산으로 연결된다.

이것이 형식적으로 건전한 이유는 쌍 타입 클래스가 가변성에 대해 아무것도 말하지 않는 새로운 계약을 정의하기 때문이다. 입력을 '가변이든 불변이든 어떤 쌍'이라고만 선언한 함수 안에서 변경 연산을 쓰려 하면 타입 시스템이 막아준다. 반면 조회 연산이 불변이라 기대하고 쓰는 것은 막지 못하지만, 대신 함수의 입력 타입을 가변 쌍, 불변 쌍, 혹은 둘 다로 명시적으로 골라 선언할 수 있게 해준다. 하위 타입 관계였다면 한쪽 방향만 허용되어 어느 한 타입을 일관되게 배제할 수 없었을 것이다. 이런 타입 클래스에 준하는 장치는 인터페이스, 트레이트, 롤 등의 이름으로 여러 정적 언어에 존재하지만, 실제 타입 시스템마다 검사를 강제하는 엄격함은 크게 다르다.

동적 타입 객체지향 언어에서는 보통 덕 타이핑 형태로 나타난다. 여러 타입에 같은 이름의 메서드를 정의하고 런타임 디스패치에 맡기는 방식이다. 함수 호출 전에 변경 연산의 존재나 부재를 명시적으로 검사하면 같은 이점을 얻을 수 있지만, 실제로 이렇게 하는 경우는 드물며 특히 변경 연산의 '부재'를 확인하는 일은 거의 없다. 스킴 사용자인 원문 저자는 동적 타입 문맥에서의 임시 다형성이 데이터 타입의 흐름에 대한 추론을 훨씬 복잡하게 만들어 오류를 부르기 쉽다고 본다. 실무에 옮겨보면 교훈은 분명하다. 읽기 전용과 수정 가능을 단순한 상속 관계로 뭉뚱그리려는 유혹을 경계하고, 언어가 제공하는 인터페이스나 명시적 검사를 통해 각 함수가 실제로 요구하는 계약의 수준을 스스로 드러내는 편이 안전하다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://crumbles.blog/posts/2026-09-17-immutable-mutable.htm...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...