이 글은 제네릭 구현의 두 가지 기본 아이디어인 ‘박싱(Boxing)’과 ‘모노모피즘(Monomorphization)’을 중심으로 다양한 언어의 접근 방식을 설명합니다.
1. 박싱(Boxing)
박싱은 모든 데이터를 균일한 ‘상자’에 넣어 동일하게 작동하도록 만드는 방식입니다. 이는 런타임 유연성을 제공하지만, 추가 메모리 할당, 동적 조회, 캐시 미스 등의 성능 비용이 발생할 수 있습니다.
-
기본 접근: C의
void*, Go의interface{}, 제네릭 이전 Java의Object처럼 모든 값을 통일된 타입으로 처리합니다. 런타임에 타입 안전성이 낮습니다. -
타입 소거 및 균일 표현: Java는 컴파일 타임에 제네릭 타입 정보를 추가하여 타입 안전성을 확보하며, OCaml은 모든 것을 포인터 크기 정수로 표현하여 추가 할당 없이 균일하게 처리합니다.
-
인터페이스 및 VTable: Go 인터페이스, Rust
dyn trait는 VTable(가상 메서드 테이블)을 통해 타입별 함수를 통일된 방식으로 호출합니다. 이는 객체 지향 언어(Java)의 VTable 사용과 유사합니다. -
리플렉션 및 동적 언어: Java, C
, Go는 런타임에 타입 정보를 검사하고 조작하는 리플렉션 기능을 제공합니다. Python, Ruby와 같은 동적 타입 언어는 이러한 리플렉션 시스템을 확장하여 강력한 메타프로그래밍 기능을 지원합니다.
2. 모노모피즘(Monomorphization)
모노모피즘은 각 데이터 타입별로 코드의 여러 복사본을 생성하는 방식입니다. 이는 최적의 런타임 성능을 제공하지만, 코드 크기 증가 및 컴파일 시간 증가를 초래할 수 있습니다.
-
코드 생성 기반: C의 전처리기 매크로, Go의 코드 생성 스크립트처럼 소스 코드 수준에서 복사본을 만듭니다. D의 문자열 믹스인, Rust의 절차적 매크로는 컴파일 과정 중 코드 생성을 지원합니다.
-
템플릿 및 구문 트리 매크로: C++ 및 D의 템플릿은 타입 매개변수를 사용하여 코드를 인스턴스화하며, D는 제약 조건을 통해 컴파일 에러를 개선합니다. Template Haskell, Nim, Lisp 계열 언어는 추상 구문 트리(AST) 수준에서 매크로를 처리합니다.
-
컴파일 타임 함수: Zig와 Terra는 컴파일 타임에 실행되는 함수를 통해 제네릭을 구현하여 강력한 메타프로그래밍 능력을 제공합니다.
-
타입 안전성 강조: Rust는
trait bounds를 통해 타입 안전성을 보장하며, 박싱과 모노모피즘을 동일 시스템 내에서 지원합니다. Swift는Witness Tables를 사용하여 박싱 없이 균일한 타입 처리를 구현하고, 필요시 모노모피즘 최적화를 수행합니다.