본문 바로가기
Language/Kotlin

제네릭(Generic)

by pan5158 2022. 5. 18.

제네릭(Generic)

제네릭(Generic) 은 클래스 내부에서 사용할 자료형을 나중에 인스터스를 생성할 때 확정합니다. 

제네릭이 나오게 된 배경은 자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 시간에 자료형을 검사해 적당한 자료형을 선택할 수 있도록 하기 위해서 입니다. 

 

제네릭 장점

  • 제네릭을 사용하면 객체의 자료형을 컴파일할 때 체크하기 때문에 객체 자료형의 안전성을 높이고 형 변환의 번거로움이 줄어듭니다.

1. 타입 파라미터 제약

타입 파라미터 제약(type paramter constraint) 은 클래스나 함수에 사용할 수 있는 타입 인자를 제한하는 기능이다.

어떤 타입을 제네릭 타입의 타입 파라미터에 대한 상한(upper bound)으로 지정하면 그 제네릭 타입을 인스터스화할 때 시용하는 타입 인자는 반드시 그 상한 타입이거나 상한 타입의 하위 타입이어야 한다.

 

2. 실행 시  제네릭스의 동작: 소거된 타입 파라미터와 실체화된 타입 파라미터 

JVM의 제네릭스 보통 타입 소거(type erasure)를 사용해 구현된다. 이는 실행 시점에 제네릭 클래스의 인스턴스에 타입 인자 정보가 들어있지 않다는 뜻이다. 함수를 Inline으로 만들면 타입 인자가 지워지지 않게 할 수 있다. 이를 코틀린에서 "실체화" 라고한다.

 

1). 실행 시점의 제네릭: 타입 검사와 캐스트

자바와 마찬가지로 코틀린 제네릭 타입 인자 정보는 런타임에 지워진다. 이는 제네릭 클래스 인스턴스가 그 인스턴스를 생성할 때 쓰인 타입 인자에 대한 정보를 유지하지 않는다는 뜻이다. 예를 들어 List<String> 객체를 만들고 그 안에 문자열 여러 넣더라도 실행 시점에는 그 객체를 오직 List로만 볼 수 있다. 그 List 객체가 어떤 타입의 원소를 저장하는지 실행 시점에는 알 수 없다. 그래서 타입인자에 서로 각각 다른 원소들이 있으므로 검사를 해야만 한다. 

 

2). 실체화한 타입 파라미터를 사용한 함수 선언

위에서 말했던 코틀린 제네릭 타입의 타입 인자 정보는 실행 시점에 지워진다. 인라인 함수선언으로 타입인자의 파라미터는 실체화되므로 실행 시점에 인라인 함수의 타입 인자를 알수 있다.

실체화한 타입 파라미터를 활용하는 가장 간단한 예제 중 하나는 표준 라이브러디 함수인 filterIsInstance다. 활용하여 확인해 보겠습니다.

변성(Variance): 제네릭과 하위 타입

변성(variance) 개념은 List<String>와 List<Any>와 같이 기저 타입이 같고 타입 인자가 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념이다. 변성을 잘 활용하면 사용에 불편하지 않으면서 타입 안정성을 보장하는 API를 만들수 있다.

 

1. 변성이 있는 이유: 인자를 함수에 넘기기

List<Any> 타입의 파라미터를 받는 함수에 List<String>을 넘기면 안전할까? 

  • String 클래스는 Any를 확장하므로 Any 타입 값을 파라미터로 받는 함수에 String 값을 넘겨도 절대로 안전하다.
  • Any와 String이 List 인터페이스 타입 인자로 들어가는 경우 그렇게 자신 있게 안전성을 말할 수 없다.

위에 예시를 통해, MutableList<Any>가 필요한 곳에 MutableList<String>을 넘기면 안 된다는 사실을 알 수 있다.

코틀린에서는 리스트의 변경 가능성에 따라 적절한 인터페이스를 선택하면 안전하지 못한 함수 호출을 막을 수 있다.함수가 읽기 전용 리스트를 받는다면 더 구체적인 타입의 원소를 갖는 리스트를 그 함수에  넘길 수 있다.ListMutableList의 변성이 다른지 살펴본다.

 

클래스, 타입, 하위 타입

하위 타입(subtype) 이라는 개념은 어떤 타입 A의 값이 필요한 모든 장소에 어떤 타입의 B의 값을 넣어도 아무 문제가 없다면 타입BA의 하위 타입이다.

상위 타입(supertype)

은 하위타입의 반대다. A 타입이 B 타입의 하위 타입이라면 BA의 상위 타입이다. 

  • Number의 하위 타입은 Int이다. == Int의 상위 타입은 Number이다.
  • 즉, 타입 A를 상속 및 구현한 타입 B는, 타입 A의 하위 타입이다.
  • null이 될 수 없는 타입은 null 이 될 수 있는 타입의 하위 타입이다.

제네릭 타입을 인스턴스 화할 때 타입 인자로 서로 다른 타입이 들어가면 인스턴스 타입 사이의 하위 타입 관계가 성립하지 않으면 그 제네릭 타입을 무공변성(invariance)이라고 말한다.

예를 들어, MutableList<Any> - MutableList<String> 의 관계. 이 둘은 서로 하위 타입이 아니다.

A가 B의 하위 타입이면 List<A>는 List<B>의 하위 타입이다. 이런 클래스나 인터페이스를 공변적(covariance)라고 한다.

 

2. 공변성: 하위 타입 관계를 유지

AB의 하위 타입일 때 Producer<A>Producer<B>의 하위 타입이면 Producer는 공변적이다. 이를 하위 타입 관계가 유지된다고 말한다. 코틀린에서는 제네릭 클래스가 타입 파라미터에 대해 공변적임을 표시하려면 타입 파라미터 이름 앞에 out을 넣어야 한다.

클래스의 타입 파라미터를 공변적으로 만들면, 함수 정의에 사용한 파라미터 타입과 타입 인자의 타입이 정확히 일치하지 않더라도 그 클래스의 인스턴스를 함수 인자나 반환 값으로 사용할 수 있다.

공변성이 언제 필요할까? 왜 쓸까?

그렇다면, 공변성을 사용하지 않는, 무공변성 코드 예시를 살펴보자. 이때 발생하는 오류를 살펴보자.

 

'Language > Kotlin' 카테고리의 다른 글

Kotlin - 확장함수(Extension Functions)  (0) 2022.05.15
코틀린(Kotlin) 범위 지정 함수(Scope Function) 정리  (0) 2022.05.14
Inline ,Infix 함수  (0) 2022.05.13
Kotlin Function  (0) 2022.05.12
Sequence  (0) 2022.05.06