모든 이야기

다형성: 하나의 인터페이스로 여러 타입을 다루는 원리

같은 이름의 동작을 여러 형태의 값에 적용할 수 있다면 코드를 매번 새로 작성할 필요가 줄어든다. 다형성은 하나의 표현이나 인터페이스가 여러 타입과 형태에 대해 동작할 수 있는 성질이다. 객체지향의 메서드 재정의가 널리 알려져 있지만 그것만이 전부는 아니다. 매개변수 다형성, 하위 타입 다형성, 특정 타입별 구현을 고르는 다형성은 서로 다른 원리로 작동한다.

공통 Shape 인터페이스의 area 동작을 원과 사각형이 서로 다른 계산으로 구현하는 구조

다형성은 공통 약속을 유지하면서 여러 타입과 구현을 다룰 수 있게 한다.

타입을 변수로 두고 같은 규칙을 쓴다

매개변수 다형성은 특정 타입을 고정하지 않고 타입 변수로 표현한다. first<T>(list<T>) -> T는 리스트 원소가 정수든 문자열이든 같은 관계를 유지한다. 반환값은 입력 리스트의 원소 타입과 연결된다. 단순히 아무 객체나 받아 아무 객체나 돌려주는 함수보다 정밀한 약속이다. 인자와 결과 사이의 타입 관계를 검사할 수 있기 때문이다.

Java의 제네릭 교육 자료는 타입 매개변수가 컴파일 시점의 검사와 재사용을 지원하는 방식을 설명한다. 제네릭 함수가 내부에서 어떤 연산을 할 수 있는지는 제약에 달려 있다. 모든 타입에 적용되는 first와 달리 원소를 비교해 최소값을 찾으려면 비교 가능한 동작이 추가로 필요하다. 타입 변수를 썼다고 어떤 메서드든 마음대로 호출할 수는 없다.

같은 계약, 서로 다른 구현

하위 타입 다형성에서는 공통 인터페이스를 만족하는 여러 구현을 같은 사용 위치에 넣는다. Shape의 area를 호출하는 코드가 원과 사각형의 면적을 구할 수 있다. 동적으로 메서드를 선택하는 언어라면 실제 객체가 원인지 사각형인지에 따라 실행할 구현이 결정된다. 호출자는 세부 계산법보다 공통 계약에 의존한다.

이 관계가 이름만 같다고 자동으로 성립하는 것은 아니다. 공통 동작을 기대하는 곳에서 구현을 바꿔 넣어도 약속을 지켜야 한다. 파일 저장 인터페이스를 구현한 객체가 특정 상황에서 데이터를 임의로 버린다면 메서드 이름과 타입 서명이 같아도 올바른 대체가 되기 어렵다. 예외 조건, 부수 효과, 반환값의 의미도 계약의 일부다.

과적재는 또 다른 선택 방식이다

같은 함수 이름에 정수용과 문자열용 구현을 따로 두는 과적재는 보통 호출의 타입 등으로 구현을 고른다. 사용자 정의 연산자나 타입 클래스처럼 타입별 동작을 선택하는 방식도 있다. 이런 특별한 구현 선택을 임시적 다형성이라고 부르기도 한다. 모든 경우를 런타임 가상 메서드 호출로 설명하면 컴파일 시점에 결정되는 선택을 놓친다.

표현 방식에 따른 성능 차이도 있다. 실행 시 인터페이스를 통해 호출하면 간접 호출 비용이 생길 수 있고, 컴파일러가 실제 구현을 알아내면 이를 줄일 수 있다. 제네릭은 타입마다 코드를 생성하거나 공통 표현을 사용하는 등 언어별 구현이 다르다. 한 가지 기법이 항상 느리거나 빠르다는 결론보다 생성 코드, 코드 크기, 최적화 조건을 확인하는 것이 정확하다.

다형성은 변경 가능성이 있는 부분과 공통 알고리즘을 분리할 때 특히 유용하다. 여러 저장소에 같은 처리 흐름을 적용하거나, 원소 타입에 상관없는 자료구조를 작성하는 경우가 예다. 다만 실제 변형이 없는 곳에 과도한 인터페이스를 추가하면 이해 비용이 커질 수 있다. 어떤 형태가 달라지는지와 무엇이 공통으로 보장되는지를 먼저 정하면 다형성의 종류도 자연스럽게 선택할 수 있다.

참고 자료

다형성: 하나의 인터페이스로 여러 타입을 다루는 원리 · ariser