다형성
위키피디아에서는 다형성을 크게 두 가지 관점에서 설명합니다. 프로그래밍 언어 이론과 타입 이론에서 다형성은 여러 다른 타입을 표현하기 위한 하나의 상징(symbol)입니다. 객체 지향 프로그래밍의 관점에서는 여러 타입을 가진 독립된 객체들을 위한 하나의 인터페이스를 제공하는 것을 다형성이라고 합니다. 하나의 입구로 들어와서 여러 갈래로 갈라지는 느낌이네요.
ad-hoc polymorphism 이란
저는 이 방식은 '동명이인'이라는 하나의 단어로 표현하고 싶습니다.
ad-hoc polymorphism 은 함수 이름은 같은데 들어오는 argument 에 따라서 동작 방식이 아예 다릅니다. 이름만 같을 뿐 내부 행동이 어떻게 돌아가는지, 결과물이 그래서 어떻게 되는 건지는 전혀 알 수가 없죠. 어떻게 보면 이름만 같을 뿐이지 추상화와는 일절 관계가 없는 이야기입니다. 말 그대로 동명이인인 셈이에요. 일반적으로 ad-hoc polymorphism 이 구현된 언어에서의 동작 방식을 살펴보면, 같은 이름의 함수가 여러 개 정의되어 있고 컴파일할 때 실제 함수에 들어온 인자들의 타입을 확인해서 그 여러 개 중에서 누가 당첨인지 결정합니다. 흔히들 부르는 funciton/method overloading 이 ad-hoc polymorphism 의 예시입니다.
정수와 부동 소수점이 분리되어 있는 언어에서는 사칙연산을 통해 이걸 잘 설명할 수 있는데요. 1 + 1.5
와 같은 식을 예로 들 수 있습니다. 컴파일러는 이 표현식을 보고 덧셈을 주관하는 많고 많은 함수들 중 첫 번째 인자는 정수, 두 번째 인자는 부동 소수점을 받을 수 있는 녀석을 정확히 불러서 계산합니다. 그리고 그 결과는 당연하게도 부동 소수점이 되겠죠. 하지만 덧셈을 주관하는 또다른 함수의 반환값은 부동 소수점이 아닌 정수일 수도 있습니다. 이름만 '덧셈'으로 같지 내부 행동이나 결과가 일관된 타입을 가질 필요는 없는 것이죠. 자바스크립트는 정수와 부동 소수점을 모두 Number로 통합해서 말하고 있기 때문에 이런 설명이 와닿지 않을 수 있습니다. 비슷한 예시를 찾아보자면 자바스크립트에서 Number와 BigInt 끼리 덧셈(1 + 2n
처럼)이 안 되는 이유는 자바스크립트에서는 이 두 종류를 인자로 받는 무언가가 없기 때문이라고 볼 수 있겠네요.
자바스크립트에서 ad-hoc polymorphism
아쉽게도 자바스크립트는 ad-hoc polymorphism 을 지원하지 않습니다. 그도 그럴 것이 위에서 ad-hoc polymorphism 은 '컴파일할 때' 함수에 들어온 인자를 확인해서 분기처리를 해 준다고 했는데 우리의 자바스크립트는 컴파일러라는 게 없으니까요.
하지만 console.log
같은 함수는 인자의 타입이 진짜 다양하게 들어올 수 있습니다. 이런 함수는 ad-hoc polymorphism 이 아닌 걸까요? 결론부터 말하자면 아닙니다. 이 함수는 ad-hoc polymorphism 을 흉내냈을 뿐이거든요. 크게 네 가지 이유가 있습니다.
- 컴파일할 때 결정하는 게 아닙니다.
앞에서 말했던 것처럼 자바스크립트는 컴파일을 하는 언어가 아니니까요. - 인자의 타입을 갖고 여러 함수 중 택일을 하는 게 아닙니다.
자바스크립트는 타입이 없는 언어니까요. console.log
의 스펙에도 관련된 이야기가 없습니다.
언어 자체에서 지원을 하지 않아서기도 하지만, 인자의 타입 역시 따로 나눠둔 게 아니라 any로 몇 개든 받을 수 있다는 표시만 되어 있습니다.- 타입을 다양하게 받을 수 있는 이유는 애초에 하나의 함수에서 이거저거 다 들어와도 처리가 가능하게끔 구현했기 때문입니다.
이거는 node.js에서 구현 코드를 보면 알 수 있는데 굳이 여기에 적을 필요는 없을 것 같아 보고 싶으신 분들을 위해 링크만 남겨둡니다. (코드1, 코드2, 코드3)
console.log
말고도 자바스크립트에서 구현한 overloading 들도 엄밀하게 말하면 ad-hoc polymorphism 은 아니고, 그걸 흉내낸 형태라고 볼 수 있겠네요.
타입스크립트에서 ad-hoc polymorphism
그렇다면 컴파일러도 있는 타입스크립트에서는 ad-hoc polymorphism 이 가능할까요? 아쉽게도 아닙니다. 타입스크립트에서 function overloading을 지원하지만 ad-hoc polymorphism 과는 거리가 있는데요. 인자의 타입으로 분기처리하는 과정을 컴파일러가 아니라 개발자가 직접 해야 하기 때문입니다.
function fn(x: string): string;
function fn(): void;
function fn(a: number, b?: boolean): void;
function fn(xOrA?: string | number, b?: boolean): string | void {
// 실제 함수 동작 구현은 여기에...
}
타입스크립트에서 함수 오버로딩은 이렇게 작성할 수 있는데요. 위쪽 세 개는 overload signature라고 하고, 마지막 함수가 실제 구현을 담은 함수(implementation signature)입니다. 여기서 ad-hoc polymorphism 과의 차이가 보입니다. ad-hoc polymorphism 은 '구현체'가 여러개입니다. 하지만 타입스크립트의 함수 오버로딩은 구현체가 하나뿐이에요. 타입만 적혀 있을 뿐이지 자바스크립트의 오버로딩과 마찬가지로 인자의 타입에 따른 행동은 결국 하나의 구현체 내부에서 분기를 해서 처리해야 합니다.
따라서 타입스크립트에서 함수 오버로딩은 ad-hoc polymorphism 이라기보다는 일종의 '인자들의 조합 가능성'을 풀어 쓴 느낌에 가깝습니다. 이런 '가짜' 오버로딩도 사용할 만한 이유가 있을지 고민해 봤는데요. 진짜로 '모두 고려' 함수를 만들어야 해서 인자의 조합이 다양할 경우 구현부 위쪽에 오버로드 시그니처를 두면 가독성이 좋을 것 같기는 합니다. 특히 다른 사람이 그 함수를 사용해야 한다면 더욱 그렇겠죠. 하지만 그런 함수의 구현부는 지저분할 텐데 과연 그게 궁극적으로 가독성이 좋은 코드를 만들 수 있는지에 대해서는 의문이 있습니다. 저는 일반적인 상황에서는 사용할 일이 없다고 생각하고, 라이브러리를 만들 때 정도만 고려해봄직하지 않나 하는 생각이 듭니다.
요약
일반적인 ad-hoc polymorphism 은 이름이 같은 함수의 구현체가 여러 개고, 사용한 함수의 인자 타입에 따라서 컴파일러가 어느 함수의 정의를 불러올 지 결정합니다. 자바스크립트나 타입스크립트에서는 하나의 구현체 안에서 인자 타입에 따라 분기 처리를 해야만 하기 때문에 ad-hoc polymorphism 을 사용할 수 없습니다.
참고 자료
Wikipedia - Polymorphism (computer science)
Wikipedia - Ad hoc polymorphism
GeeksForGeeks - Function Overloading in JavaScript
TypeScript: Documentation - More on Functions