pattern

다형성(polymorphism)

보통 객체지향에 대해 공부를 하다보면 캡슐화/상속/추상화/다형성에 대해 많이 듣게 됩니다. 이 글에서는 다형성에 대해 정리해 보려합니다. 다형성(Polymorphism)이란, 한 객체가 여러 가지 모습을 갖는다는 것을 의미합니다. 즉, 한 객체가 여러 가지 타입을 가질 수 있다는 것을 의미합니다.

Typescript에서의 다형성

타입스크립트에서 다형성을 가능하게 해주는 것이 제네릭(Generics) 이라고 할 수 있습니다. 제네릭은 선언 시점이 아니라 생성 시점에 타입을 명시하여, 단일 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법입니다. 그리하여 인터페이스, 함수 등의 재사용성을 높일 수 있다는 장점이 있습니다. 제네릭 사용 시 관용적으로 식별자를 T나 V를 많이 사용하고 이를 타입 파라미터(Type parameter)라고 합니다.

간단한 예시를 생각해 봅시다. 아래의 함수를 확인해 봅시다.

polymorphism
const arrayCheck = (array) => {
  array.forEach((item) => console.log(item))
}

위의 예는 JavaScript로 작성이 되었습니다. 간단히 보면 array를 넣고, 하나씩 값을 뽑아내어 console.log를 찍습니다. JavaScript에서는 아무 문제가 없어 보이지만, TypeScript로 변경을 하면 이야기가 달라지게 됩니다. arr의 특정 타입을 어떻게 하면 지정할 수 있을까요? 한번 작성해 보도록 하겠습니다.

polymorphism
interface ArrayCheck {
  (array: string[]): void
  (array: number[]): void
  (array: (string | number)[]): void
}
 
const arrayCheck: ArrayCheck = (array) => {
  array.forEach((item) => console.log(item))
}
 
arrayCheck([3, 16])
arrayCheck(['han', 'py'])
arrayCheck(['kch', 316])
 
// console output
3
16
han
py
han
py
316

위 내용을 확인해 보면, TypeScript에서 특정 타입을 지정하여 type을 설정할 수 있습니다. 만약 새로운 타입을 받고 싶다면, 계속 추가를 해줘야만 할까요? 이럴 때 사용가능한 것이 제너릭 입니다.

TypeScript Generics 사용하기

polymorphism
interface ArrayCheck<T> {
  (array: T[]): void
}
 
const arrayNumberCheck: ArrayCheck<number> = (array) => {
  array.forEach((item) => console.log(item))
}
 
const arrayCheck: ArrayCheck<string | number> = (array) => {
  array.forEach((item) => console.log(item))
}
 
arrayNumberCheck([3, 16])
arrayCheck([3, 'py'])

interface에서 제너릭 설정을 한 후에, 사용 부에서 상황에 맞게 추가해 줄 수 있습니다. 다형성을 적용하여, number array 뿐만 아니라 다른 string도 추가할 수 있게 되었습니다. 물론 타입 추론을 사용하여 더 간단하게 적용하는 것도 가능합니다.

polymorphism
interface ArrayCheck {
  <T>(arr: T[]): void
}
 
const arrayCheck: ArrayCheck = (arr) => {
  arr.forEach((elem) => console.log(elem))
}
 
arrayCheck([3, 'py'])

객체에 인자를 넣어서 아래와 같이 사용도 가능하게 됩니다.

polymorphism
// TYPE
type Detection<E> = {
  type: string
  resource: E
}
 
type FaceDetection = Detection<string>
 
const faceDetection: FaceDetection = {
  type: 'face',
  resource: '/image.png',
}
 
// INTERFACE
interface Detection<E> {
  type: string
  resource: E
}
 
interface FaceDetection extends Detection<string> {}
 
const faceDetection: FaceDetection = {
  type: 'face',
  resource: '/image.png',
}

우리는 지금까지 다형성이란 하나의 객체에 여러가지 타입을 넣을 수 있는 것이라고 알아보았다. 이러한 관점은 타입의 관점에서 접근한 것이라고 할 수 있습니다. 다형성을 구현하는 대표적인 방법으로는 오버로딩, 오버라이딩, 함수형 인터페이스를 사용하는 방법으로 가능합니다. 다형성을 사용하는 다양한 이유가 있지만, 가장 큰 이유는 변화에 유연한 소프트웨어를 만들기 위해 사용된다고 할 수 있습니다. Number/String 타입이 다르다고 반복된 함수를 적는 것이 아니라, 반복된 코드를 줄이고 필요한 코드만 변경하여 변화에 유연한 코드를 만들 수 있도록 고민해 보면 좋을 것 같습니다.