TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 33 READS

클래스 전체가 아니라 메서드에만 제약을 거는 법: OCaml의 가드 메서드

객체지향 언어에서 리스트를 나타내는 제네릭 클래스를 만들었다고 하자. 여기에 [[1, 2, 3], [4, 5]]를 [1, 2, 3, 4, 5]로 펼치는 flatten 메서드를 넣으려면 곤란한 문제가 생긴다. 이 메서드는 원소가 다시 리스트일 때만 의미가 있는데, 자바의 제네릭처럼 타입 변수에 제약을 걸 수 있는 언어에서도 그 제약은 클래스 전체에 적용된다. 즉 'T는 S의 서브타입'이라는 조건을 붙이는 순간 그 리스트는 언제나 '리스트의 리스트'여야만 하는, 대단히 제한적인 자료구조가 되어 버린다. 정작 우리가 원하는 것은 특정 메서드에만 걸리는 제약이다.

이 문제를 다루는 방법은 이론적으로 세 가지가 있다. 첫째는 가장 손쉬운 편법으로, 해당 메서드를 클래스 본문 밖으로 빼내는 것이다. 정적(static) 컨텍스트나 동반 객체(companion object)에 두면 특별한 절차 없이 동작한다. 다만 개발자는 어떤 메서드가 클래스 안에 있고 어떤 것이 밖에 있는지를 계속 기억해야 하며, 무엇보다 '인스턴스에 메시지를 보낸다'는 객체지향의 핵심 감각이 깨진다. 둘째는 코틀린이나 C#이 제공하는 확장 메서드다. String처럼 final로 닫힌 클래스에도 동작을 덧붙일 수 있고 수신자(receiver)를 더 유연하게 한정할 수 있어 거의 완벽해 보인다. 그러나 메서드가 여전히 클래스 밖에 정의되는 탓에, 확장에서 접근하려고 클래스의 일부 멤버를 public으로 열어야 하는 상황, 즉 추상화가 새는 문제가 생길 수 있다.

가드 메서드라는 세 번째 길

세 번째가 이 글의 주인공인 가드 메서드(guarded method)다. 발상은 이념적으로 가장 순수하다. 메서드 정의를 클래스 안에 그대로 두되, 제네릭 매개변수에 대한 제약을 '메서드 정의 수준에서' 붙이는 것이다. 추상화를 밖으로 흘리지 않으면서도 메시지 전달 방식을 온전히 지킬 수 있어 앞의 문제들을 한꺼번에 해소한다. 문제는, 저자가 아는 한 이 기능을 문법적으로 지원하는 주류 언어가 없다는 점이다. 저자 역시 이 개념을 최근에야 접했는데, 통계적 타입을 가진 함수형 프로그래밍과 객체지향 사이의 대칭성을 다룬 Gabriel Scherer의 발표 'The Object-Oriented/Functional-Programming symmetry: theory and practice' 슬라이드를 읽다가 발견했다고 한다. 발표 시간 제약으로 다루어지지 못한 이 슬라이드에서 Scherer는 method 이름 : 반환타입 with 제네릭타입 = 다른타입 형태의 문법을 제안한다. 모듈의 치환처럼 and로 여러 제네릭에 제약을 걸 수도 있는 이 문법은, 예컨대 원소가 정수일 때만 쓸 수 있는 sum 메서드도 우아하게 표현한다.

타입 등가 증거로 문법을 대신하다

OCaml에는 이런 문법이 없지만, 저자는 OCaml 커뮤니티의 Florian Angeletti(별칭 Octachron)의 도움을 받아 이를 인코딩하는 방법을 정리한다. 핵심 도구는 일반화 대수적 자료형(GADT)이 도입되면서 가능해진 타입 등가 증거(type equality witness)다. 생성자가 Refl 하나뿐인 eq 타입을 정의하면, Refl 값은 서로 같은 두 타입에 대해서만 만들 수 있다. 따라서 어떤 스코프 안에서 Refl 값을 구성했다는 사실 자체가, 문법적으로는 달라 보이는 두 타입이 실제로는 같다는 보증이 된다. 이는 컴파일러가 타입을 알 수 없는 경우, 예컨대 데이터가 런타임에 주어지거나 타입의 표현이 추상화로 감춰진 경우에 특히 쓸모가 있다.

제약을 모델링한다는 것은 결국 그 제약을 강제하는 추가 인자, 곧 증거를 요구하는 일로 환원된다. 리스트에 객체 API를 부여한 obj_list 클래스를 보자. flatten이 성립하려면 클래스의 타입 매개변수 'a가 어떤 'b list라는 증거가 필요하다. 방법은 간단하다. flatten이 ('a, 'b list) eq 타입의 값을 인자로 받도록 하면 된다. length, append, uncons 같은 메서드는 평범하게 구현되고, flatten은 리스트를 재귀적으로 순회하며 각 원소를 이어 붙이는데, 그 과정에서 Refl을 인스턴스화해 'a = 'b list라는 증거를 공급하는 것이 트릭의 전부다. 원소가 정수일 때만 유효한 sum 메서드도 같은 원리로, ('a, int) eq를 증거로 받아 fold_left로 구현한다. sum은 새로운 타입 변수를 도입하지 않아 flatten보다 논리적으로 더 단순하다.

실무자에게 남는 것

결과는 기대한 그대로다. flatten은 'a list obj_list 객체에, sum은 int obj_list 객체에만 호출할 수 있으며, 정수 리스트가 아닌 값에 sum을 부르면 아예 컴파일되지 않는다. 제약을 어긴 호출이 정적 단계에서 걸러진다는 뜻이다. 저자는 Nicolas Rinaudo의 지적을 빌려, Scala가 유사한 인코딩을 쓰되 등가 증거를 암시적(implicit)으로 공급해 사용자가 Refl을 손수 넘길 필요가 없도록 호출을 가볍게 만든다는 점도 덧붙인다.

실무적으로 이 기법의 효용은 냉정하게 볼 필요가 있다. 인코딩은 다소 번잡하고, 네이티브 문법 지원이 있었다면 훨씬 간결했을 것이며, OCaml 자체가 객체지향 스타일을 그리 권장하지 않는다. 그러나 이 사례는 두 가지 실천적 시사점을 준다. 하나는 '클래스 단위 제약'과 '메서드 단위 제약'을 구분하는 사고방식으로, 확장 메서드나 정적 헬퍼로 우회하던 문제를 타입 시스템 안에서 다시 바라보게 한다. 다른 하나는 GADT의 타입 등가 증거가 단순한 이론적 장치가 아니라, 런타임 정보나 추상화로 가려진 타입 정보를 안전하게 복원하는 구체적 도구라는 점이다. 정적 타입 객체지향 언어들이 메시지 전달 의미를 지키면서 표현할 수 있는 메서드의 폭을 넓혀 주는 이 기능을 왜 대부분 갖추지 않았는지는, 이 인코딩을 따라가 보면 오히려 더 뚜렷하게 의문으로 남는다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://xvw.lol/en/articles/oop-refl.html
SHARE
처리 중...