포스트

제네릭 — 파라미터 다형성과 그 구현의 갈림길

타입을 매개변수로 받아 여러 타입에 같은 코드를 재사용하는 제네릭(파라미터 다형성)을 언어 공통 개념으로 정리한다. 구현이 갈리는 지점(단형화 vs 타입 소거), 아무 타입이나 막는 제약(constraints)을 짚고, C++ 템플릿·Go generics·Rust 제네릭이 같은 개념을 어떻게 다르게 구현하는지 대응시킨다.

제네릭 — 파라미터 다형성과 그 구현의 갈림길

난이도 중급 · 선행 없음

한 줄 요약

제네릭은 타입을 매개변수로 받아, 여러 타입에 같은 코드를 재사용하는 기법이다. 정식 명칭은 파라미터 다형성(parametric polymorphism). maxList 같은 걸 타입마다 다시 짜지 않게 해준다.

어떤 문제를 푸는가

두 값 중 큰 걸 반환하는 max를 생각하자. 제네릭이 없으면 타입마다 똑같은 로직을 복붙해야 한다.

1
2
3
int    max_int(int a, int b)          { return a > b ? a : b; }
double max_double(double a, double b) { return a > b ? a : b; }
// string용도... 또 복붙?

로직은 완전히 같고 타입만 다르다. 이 중복을 없애는 게 제네릭이다.

1
2
template<typename T>
T max(T a, T b) { return a > b ? a : b; }   // 모든 타입에 하나로

제네릭 ≠ 상속 다형성. 상속(subtyping)은 “여러 타입을 부모 하나로 다룸(virtual/interface)”, 제네릭은 “하나의 코드를 여러 타입에 찍어냄”이다. 둘은 다른 축이다.

구현의 갈림길 — 단형화 vs 타입 소거

여기가 언어별 성능·특성이 갈리는 핵심이다. “제네릭 코드를 실제 기계어로 어떻게 바꾸느냐”에서 두 갈래다.

① 단형화(Monomorphization) — 타입마다 코드 복제

컴파일러가 사용된 타입마다 특수화된 코드를 따로 생성한다. max<int>, max<double>이 각각 별도 함수로 찍혀 나온다.

  • 장점: 런타임 비용 0. 각 타입에 최적화·인라인되어 손으로 쓴 코드만큼 빠르다.
  • 단점: 타입 종류만큼 코드가 불어난다(code bloat). 컴파일이 느려진다.
  • : C++ 템플릿, Rust 제네릭

② 타입 소거(Type Erasure) / 박싱 — 코드 하나로 공유

컴파일 후 타입 정보를 지우고, 하나의 코드가 여러 타입을 처리한다. 값은 참조/박스로 간접 접근한다.

  • 장점: 코드가 작다. 컴파일이 빠르다.
  • 단점: 박싱·간접 참조에 런타임 비용이 있다. 런타임에 실제 타입 정보가 제한된다.
  • : Java 제네릭(소거), Go의 옛 interface{} 방식

Go는 그 중간

Go 제네릭(1.18+)은 GC shape stenciling이라는 절충을 쓴다. 메모리 레이아웃(shape)이 같은 타입들끼리는 코드를 공유하고, 다른 shape은 나눠 찍는다. 단형화의 code bloat와 소거의 런타임 비용 사이에서 타협한 방식이다.

 단형화타입 소거
코드 크기큼(타입마다 복제)작음(공유)
런타임 비용없음박싱·간접
컴파일 속도느림빠름
대표C++, RustJava, (Go는 중간)

아무 타입이나는 안 된다 — 제약(constraints)

max> 비교가 되는 타입에만 의미가 있다. 제네릭에 “이런 능력을 갖춘 타입만”이라는 조건을 거는 게 제약이다. 제약이 없으면 에러가 사용 지점에서 늦게, 난해하게 터진다(옛 C++ 템플릿 에러의 악명).

언어제약 방식
C++concepts (C++20) — template<std::totally_ordered T>
Rusttrait bounds — fn max<T: PartialOrd>(...)
Go인터페이스 제약 — func Max[T constraints.Ordered](...)

세 언어 모두 “타입 T는 비교/출력/복제 등 특정 능력을 갖춰야 한다“를 명시하게 한다. 제약을 붙이면 에러가 정의 지점에서 명확히 잡히고, 문서 역할도 한다.

언어별 정리

언어도입방식제약
C++원조(템플릿)단형화concepts (C++20)
Rust처음부터단형화trait bounds
Go1.18 (2022)중간(shape)인터페이스 제약
  • C++: 템플릿은 제네릭 그 이상(메타프로그래밍까지). → 템플릿
  • Go: 오래 제네릭 없이 interface{}로 버티다 1.18에서 도입. Go 철학상 최소한으로 넣었다. → Go 학습 로드맵
  • Rust: 제네릭 + 트레이트가 언어의 중심. 단형화라 C++급 성능.

C++ 템플릿을 아는 사람에게 Rust 제네릭은 익숙하다 — 둘 다 단형화다. Go 제네릭은 “훨씬 절제된 버전”으로 느껴질 것이다.

스스로 점검

1. 제네릭(파라미터 다형성)과 상속 다형성(subtyping)의 차이는?

상속 다형성은 여러 타입을 부모/인터페이스 하나로 다룬다(virtual·interface, 런타임 디스패치). 제네릭은 하나의 코드를 여러 타입에 찍어낸다(대개 컴파일 타임). 방향이 다른 축이다.

2. 단형화의 장점과 대가는?

장점은 런타임 비용 0 — 타입마다 특수화·인라인되어 손으로 쓴 코드만큼 빠르다. 대가는 타입 종류만큼 코드가 불어나는 code bloat와 느려지는 컴파일이다. C++·Rust가 이 방식이다.

3. 제네릭에 제약(constraints)을 거는 이유는?

아무 타입이나 허용하면 >·출력 같은 연산이 되는지 보장할 수 없다. 제약을 붙이면 에러가 사용 지점이 아니라 정의 지점에서 명확히 잡히고, “이 타입은 이런 능력이 필요하다”는 문서 역할도 한다. C++ concepts, Rust trait bounds, Go 인터페이스 제약이 각각 이 역할이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.