Skip to main content

제네릭

제네릭이란?#

제네릭은 어떤 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 프로그래밍 기법을 말한다.
동적타입 언어(javascript, python) 와 달리 정적타입 언어(typescript, java, c++)는 클래스 또는 함수를 선언하는 시점에서 매개변수와 리턴에 타입을 지정해야 한다. 이때 제네릭을 사용하면 범용적인 매개변수 또는 리턴타입을 지정할 수 있다.

동적타입 언어
동적타입 언어(Dynamically typed languages)는 컴파일 시 자료형을 정하는 것이 아니라 런타임시 결정 된다.
대표적인 동적타입 언어로는 Groovy, Python, JavaScript, Ruby, Smalltalk, Lisp, Objective-C, PHP, Prolog 등이 있다

장점 :
  • 런타임까지 타입에 대한 결정을 끌고 갈 수 있기 때문에 유연성이 높다.
  • 컴파일시 타입을 명시해주지 않아도 되기 때문에 빠르게 코드를 작성할 수 있다.
단점 :
  • 실행 도중에 변수에 예상치 못한 타입이 들어와 타입에러가 발생할 수 있다

정적타입 언어
정적타입 언어(Statically typed language)는 컴파일 시 변수의 타입이 결정되는 언어를 말한다.
따라서, 프로그래머가 변수에 들어갈 값의 형태에 따라 직접 변수의 타입을 명시해줘야 한다.
대표적인 정적타입의 언어로는 Java, C, C++, typescript, C#, Scala, Fortran, Haskell, ML, Pascal 등이 있다.

장점 :
  • 타입 에러로 인한 문제점을 초기에 발견할 수 있어 타입의 안정성이 높다
  • 컴파일 시에 미리 타입을 결정하기 때문에 실행속도가 빠르다
단점 :
  • 매번 코드 작성시 변수형을 결정해줘야 하는 번거로움이 있다

제네릭의 Hello World !#

먼저 제네릭의 "hello world"인 identity 함수를 해보자. identity 함수는 인수로 무엇이 오던 그대로 반환하는 함수이다.

제네릭이 없다면, identity 함수에 특정 타입을 주어야 한다.

function identity(arg: number): number {  return arg;}

또는 any 타입을 사용하여 identity 함수를 만들 수 있다.

function identity(arg: any): any {  return arg;}

any를 쓰는 것은 함수의 arg가 어떤 타입이든 받을 수 있다는 점에서 제네릭이지만, 실제로 함수가 반환할 때 어떤 타입인지에 대한 정보는 잃게 된다. 만약 number 타입을 넘긴다고 해도 any 타입이 반환된다는 정보만 얻을 뿐이다.

대신에 우리는 무엇이 반환되는지 표시하기 위해 인수의 타입을 저장할 방법이 필요하다. 여기서는 값이 아닌 타입에 적용되는 타입 변수를 사용할 것이다.

function identity<T>(arg: T): T {  return arg;}

identity 함수에 T 라는 타입 변수를 추가했다. T는 유저가 준 인수의 타입을 저장하고 (예 : number, string), 이 정보를 나중에 사용할 수 있게 해준다. 여기에서는 T를 반환 타입으로 다시 사용한다. 인수와 반환 타입이 같은 타입을 사용하고 있는 것을 확인할 수 있다.

이 identity 함수는 타입을 불문하고 동작하므로 제네릭이라 할 수 있다.
any를 쓰는 것과는 다르게 인수와 반환 타입에 number를 사용한 첫 번째 identity 함수만큼 정확하다(즉, 어떤 정보도 잃지 않는다)

일단 제네릭 identity 함수를 작성하고 나면, 두 가지 방법 중 하나로 호출할 수 있다.
첫 번째 방법은 함수에 타입 인수를 포함한 모든 인수를 전달하는 방법이다.

let output = identity<string>('mystring');

여기서 우리는 함수를 호출할 때의 인수 중 하나로써 Tstring으로 명시해 주고 인수 주변에 () 대신 <>로 감싸주었다.

두 번째 방법은 아마 가장 일반적인 방법이다. 여기서는 타입 인수 추론을 사용한다. 즉, 우리가 전달하는 인수에 따라서 컴파일러가 T의 값을 자동으로 정하게 하는 것이다.

let output = identity('mystring');

타입 인수를 꺽쇠괄호(<>)에 담아 명시적으로 전달해 주지 않은 것을 주목하자.
컴파일러는 값인 "mystring"을 보고 그것의 타입으로 T를 정한다. 인수 추론은 코드를 간결하고 가독성 있게 하는데 있어 유용하지만, 더 복잡한 예제에서 컴파일러가 타입을 유추할 수 없는 경우엔 명시적인 타입 인수 전달이 필요할 수도 있다.

제네릭 타입 변수 작업#

제네릭을 사용하기 시작하면, identity와 같은 제네릭 함수를 만들 때, 컴파일러가 함수 본문에 제네릭 타입화된 매개변수를 쓰도록 강요한다.즉, 이 매개변수들은 실제로 any나 모든 타입이 될 수 있는 것처럼 취급할 수 있게 된다.

앞에서 본 identity 함수를 살펴보자

function identity<T>(arg: T): T {  return arg;}

함수 호출 시마다 인수 arg의 길이를 로그에 찍으려면 어떻게 해야 할까 ? 아마 이것을 쓰고 싶을 거다.

function loggingIdentity<T>(arg: T): T {  console.log(arg.length); // 오류: T에는 .length 가 없습니다.  return arg;}

이렇게하면, 어떤 곳에서도 arg.length를 가지고 있다는 것을 명시하지 않았기 때문에, 컴파일러는 arg.length를 사용하고 있다는 오류를 낼 것이다.

실제로 이 함수가 T가 아닌 T의 배열에서 동작하도록 했다고 가정해보자. 그럼 배열로 사용했기 때문에 .length를 사용 가능하다.

function loggingIdentity<T>(arg: T[]): T[] {  console.log(arg.length); // 배열은 .length를 가지고 있다. 따라서 오류는 없다.  return arg;}

위 예제를 이렇게도 대체할 수 있다.

function logginIdentity<T>(arg: Array<T>): Array<T> {  console.log(arg.length); // 배열은 .length를 가지고 있다. 따라서 오류는 없다.  return arg;}