이 장을 읽기 전에
타입 시스템에서 다룬 정적 타입 언어가 컴파일 시점에 타입 불일치를 잡아낸다는 원리와, 함수형 프로그래밍 패러다임에서 다룬 “같은 로직을 여러 데이터에 재사용"하는 감각을 안다고 가정한다. 이 챕터는 정적 타입 언어에서 그 재사용을 타입 안전하게 하는 방법을 다룬다.
정적 타입 언어의 딜레마: 재사용이냐 안전이냐
타입 시스템에서 다룬 것처럼 정적 타입 언어는 컴파일 시점에 타입 오류를 잡아준다는 장점이 있지만, 그 대가로 “숫자 배열에서 최댓값을 찾는 함수"와 “문자열 배열에서 최댓값을 찾는 함수"를 타입마다 따로 작성해야 하는 것처럼 보인다. 함수 시그니처에 타입을 고정해버리면 재사용성이 크게 떨어진다. **제네릭(Generics)**은 함수나 클래스를 정의할 때 구체적인 타입 대신 T 같은 **타입 매개변수(Type Parameter)**를 두어, 실제로 그 함수·클래스를 사용하는 시점에 타입이 채워지도록 하는 기법이다. 이렇게 하면 타입마다 로직을 중복 작성하지 않으면서도, 컴파일러가 여전히 타입을 검사할 수 있다.
| |
first 함수는 단 한 번만 작성되었지만, 호출 시점에 T가 number로 채워지면 반환값도 number로, string으로 채워지면 반환값도 string으로 컴파일러가 추론한다. 그 덕분에 마지막 줄처럼 number 배열에 문자열 전용 메서드를 잘못 호출하려 하면 컴파일 시점에 바로 오류가 난다 — 코드는 한 번만 썼지만 타입 안전성은 그대로 유지된다.
다형성의 세 형태: 오버로딩, 오버라이딩, 제네릭
**다형성(Polymorphism)**은 “여러(poly) 형태(morph)“라는 어원 그대로, 하나의 이름이나 인터페이스로 서로 다른 타입에 대해 다르게 동작하는 능력을 가리키는 포괄적 개념이다. 흔히 세 가지 형태로 구분한다. **오버로딩(Overloading)**은 같은 이름의 함수를 매개변수의 타입이나 개수를 다르게 해 여러 버전으로 정의하는 것으로, 어떤 버전이 실행될지는 컴파일 시점에 인자 타입을 보고 정해진다(정적 다형성). **오버라이딩(Overriding)**은 상위 클래스가 정의한 메서드를 하위 클래스가 자신에 맞게 다시 정의하는 것으로, 실제로 어떤 버전이 실행될지는 그 객체의 실제 타입에 따라 실행 시점에 정해진다(동적 다형성). 제네릭은 앞서 본 것처럼 타입을 매개변수화해 하나의 코드가 여러 타입에 대해 동작하게 하는 것으로, 오버로딩·오버라이딩과 달리 코드 자체는 하나만 존재한다.
| |
Shape 타입 변수에 Circle이나 Square 객체를 담아 area()를 호출하면, 실제로 어떤 클래스의 area()가 실행될지는 프로그램이 그 줄에 도달하는 순간의 실제 객체 타입을 보고 결정된다 — 이것이 오버라이딩이 “동적” 다형성으로 불리는 이유다. 반면 Calc의 print 메서드는 어떤 버전이 호출될지가 인자로 int를 넘겼는지 double을 넘겼는지에 따라 컴파일 시점에 이미 정해진다.
flowchart LR
Call["호출 코드"] -->|"컴파일 시점 결정"| OL["오버로딩: 인자 타입으로 버전 선택"]
Call -->|"컴파일 시점 결정"| GN["제네릭: T를 구체 타입으로 치환"]
Call -->|"실행 시점 결정"| OR["오버라이딩: 실제 객체 타입으로 메서드 선택"]
제네릭의 한계: 타입 소거
제네릭이 컴파일 시점에만 존재하고 실행 시점에는 사라지는 언어도 있다. Java가 대표적인데, 이를 **타입 소거(Type Erasure)**라 한다. List<String>과 List<Integer>는 컴파일 시점에는 서로 다른 타입으로 검사되지만, 컴파일이 끝나 실제로 실행되는 바이트코드에서는 둘 다 그냥 List로 처리되어 타입 매개변수 정보 자체가 사라진다. 이 때문에 실무에서 흔히 마주치는 함정이 있다 — 리플렉션으로 런타임에 “이 리스트에 담긴 원소의 타입이 무엇인가"를 직접 조회할 수 없고, new T[10]처럼 타입 매개변수로 배열을 직접 생성할 수도 없다(제네릭 배열 생성 제약). 반면 C#이나 TypeScript(컴파일된 JS는 제외)처럼 타입 정보를 런타임까지 유지하는(reified generics) 언어도 있어, “제네릭은 항상 소거된다"고 일반화할 수는 없다 — 이는 언어별 구현 선택의 문제다.
비교: 오버로딩 vs 오버라이딩 vs 제네릭
| 특성 | 오버로딩 | 오버라이딩 | 제네릭 |
|---|---|---|---|
| 결정 시점 | 컴파일 시점(정적) | 실행 시점(동적) | 컴파일 시점(정적) |
| 코드 개수 | 시그니처마다 별도 작성 | 클래스마다 별도 작성 | 하나의 코드로 여러 타입 지원 |
| 관계 | 같은 클래스 내 여러 메서드 | 상속 관계(부모-자식) | 타입 매개변수화 |
| 목적 | 인자 형태별 처리 분기 | 하위 클래스별 동작 특화 | 타입 중복 제거 |
언제 무엇을 쓰는가
세 형태는 서로 대체재가 아니라 다른 문제에 답한다. 처리할 타입에 따라 완전히 다른 로직이 필요하다면(정수는 콤마 없이, 부동소수점은 소수점 둘째 자리까지 출력) 오버로딩이 맞다 — 타입별로 별도 구현이 필요하다는 뜻이기 때문이다. 반대로 로직 자체는 같고 타입만 다르다면(리스트의 첫 원소를 꺼내는 first 함수처럼) 제네릭이 적합하다 — 코드를 한 번만 쓰고 타입 안전성도 유지할 수 있다. 상속 계층에서 부모가 정의한 동작을 자식마다 다르게 특화해야 한다면(도형마다 다른 넓이 계산) 오버라이딩을 쓴다. 셋을 헷갈려 오버로딩으로 풀어야 할 문제를 제네릭으로 무리하게 일반화하면, 타입마다 실제로 달라야 할 로직이 하나의 코드에 조건문으로 뒤섞이는 역효과가 난다.
흔한 오개념
“제네릭은 다형성의 한 형태일 뿐 서로 무관하다” — 제네릭과 오버라이딩은 실제로 함께 쓰이는 경우가 많다. 예를 들어 제네릭 컬렉션 클래스(List<T>)에 담긴 객체의 메서드를 호출할 때, 그 메서드가 오버라이딩되어 있다면 컴파일 시점에는 T로만 알려진 타입이라도 실행 시점에는 실제 객체 타입에 맞는 오버라이딩된 메서드가 호출된다 — 정적 다형성(제네릭)과 동적 다형성(오버라이딩)이 한 코드 안에서 겹쳐 동작하는 것이다.
“제네릭은 아무 타입이나 다 받는다는 뜻이다” — Any 타입으로 받는 것과 제네릭은 다르다. Any는 타입 정보를 버려서 컴파일러가 이후 검사를 포기하지만, 제네릭은 T가 무엇이든 호출 시점에 하나의 구체적인 타입으로 고정되고 그 타입에 맞는 검사를 계속 수행한다. 앞의 first<number>([1, 2, 3]) 예시에서 반환값에 문자열 메서드를 호출하면 오류가 나는 것이 이 차이를 보여준다.
다른 개념과의 연결
제네릭이 컴파일 시점에 타입을 검사한다는 점은 타입 시스템에서 다룬 정적 타입 검사의 연장선이며, 함수형 프로그래밍의 map/filter(예: map<T, U>)도 내부적으로 제네릭 타입 시그니처로 정의된다. 다음 챕터에서는 정적 타입 언어가 컴파일 시점에 잡아내지 못하는 또 다른 종류의 메모리 버그를, 가비지 컬렉션과 다른 제3의 접근인 소유권 모델로 원천 차단하는 방법을 다룬다.
평가 기준
이 챕터를 읽은 후에는 다음을 할 수 있어야 한다. 제네릭이 타입 중복 작성 문제를 해결하는 원리를 설명할 수 있다. 오버로딩·오버라이딩·제네릭을 결정 시점(정적/동적)과 코드 개수 기준으로 구분할 수 있다. 제네릭과 Any 타입의 차이를 타입 안전성 관점에서 설명할 수 있다.
참고 자료
Cardelli, L., & Wegner, P. (1985). “On Understanding Types, Data Abstraction, and Polymorphism.” ACM Computing Surveys, 17(4), 471–523.
- TypeScript Handbook: Generics — TypeScript 제네릭 함수·클래스·제약 조건의 공식 문서
- Oracle Java Tutorials: Polymorphism — Java에서 오버라이딩 기반 동적 다형성이 동작하는 방식
![Featured image of post [Computer Terms] 제네릭과 다형성 (Generics, Polymorphism)](/post/computerterms/generics-and-polymorphism/wordcloud_hu_b498d2bd7a33015c.webp)
![[Computer Terms] 클로저와 스코프 (Closure, Scope)](/post/computerterms/closures-and-scope/wordcloud_hu_2e820d4be23cd354.webp)
![[Computer Terms] 함수형 프로그래밍 패러다임 (Functional Programming)](/post/computerterms/functional-programming-paradigm/wordcloud_hu_3bef8e60ff39932e.webp)
![[Computer Terms] 제네릭과 다형성 (Generics, Polymorphism)](/post/computerterms/generics-and-polymorphism/wordcloud_hu_232c150df4efcb7b.webp)
![[Computer Terms] 메모리 안전성과 소유권 (Memory Safety, Ownership)](/post/computerterms/memory-safety-and-ownership/wordcloud_hu_e7ab58081571a099.webp)
![[Computer Terms] 코드 리뷰 (Code Review)](/post/computerterms/code-review/wordcloud_hu_3a7f712125632a84.webp)
![[Computer Terms] 타입 시스템: 정적/동적, 강/약 타입](/post/computerterms/type-systems/wordcloud_hu_11e612e21280e94c.webp)
![[Rust] Comprehensive Rust 무료 강의 정리 및 코스 구조](/post/2022-12-30-comprehensive-rust/wordcloud_hu_d1420ff38434cdb6.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[Computer Terms] 가비지 컬렉션 (Garbage Collection)](/post/computerterms/garbage-collection/wordcloud_hu_22d0f6f1dc2ef90e.webp)