[Kotlin] Generic! 넌 뭐냐?
평소에 안드로이드 개발 시에 제네릭을 사용하여 Event라는 클래스를 만들어 서버와의 API 요청 시에 다양한 상태를 타입 안정성을 유지하면서 통신 상태를 표현하고자 제네릭을 사용하기도 했고 동일한 로직을 줄이고자 제네릭 함수로 만들어서 사용해 봤지만 제대로 아는 거 같지 않아 이렇게 정리한다.
Generic
- 제네릭(Generic)?
제네릭(Generic)이란 프로그래밍 언어들에서 제공해주는 기능 중 하나인데 제네릭은 클래스나 인터페이스 혹은 함수 등에서 동일한 코드로 여러 타입을 지원해 주기 위해 존재하는 기능이다. 코틀린에서는 코드에 타입 안정성을 주기 위해 많은 노력을 하고 있는데 코틀린을 사용해 개발 시에 위에서 말한 제네릭을 사용한다면 타입의 안정성을 높여 코드를 작성할 수 있게 도와줄뿐더러 타입 캐스팅 연산을 줄여 프로그램의 성능을 향상시킬수 있다.
- 제네릭(Generic)의 사용 방법
보통은 꺽쇠<> 사이에 형식 매개변수를 사용해 선언하게 되는데 이때 형식 매개변수란 자료형을 대표하는 용어로 T와 같이 특정 영문의 대문자를 사용한다. 간단한 예시코드와 표를 보자 (하지만?(물음표)를 제외한 한국어등 다른 언어를 사용해도 크게 문제가 되지 않는다.)
class Box<T>(t: T) {
var name = t
}
fun main() {
val box1: Box<Int> = Box(1)
val box2: Box<String> = Box("호랑이")
println(box1.name)
println(box2.name)
}
위의 코드와 같이 box1, box2의 타입을 명시적으로 지정해주지 않아도 컴파일러는 객체의 타입을 추론할 수 있다.
- 제네릭 클래스(Generic Class)
형식 매개변수를 한 개 이상 받는 클래스를 제네릭 클래스(Generic Class)라고 한다. 제네릭 클래스는 인스턴스를 생성하는 시점에서 클래스의 자료형을 지정하게 되고 제네릭 클래스 내의 메서드에서도 형식 매개변수를 사용하게 된다.
class Aclass<T> {
fun aMethod(a: T) {
. . .
}
}
- 제네릭 함수(Generic Function)
함수나 메서드의 앞에 <T>와 같이 형식 매개변수를 지정한 함수를 제네릭 함수(Generic Function)라고 한다. 제네릭 함수의 자료형은 컴파일러가 함수가 호출될 때 추론한다. 여기서 선언한 자료형은 리턴 타입이나 파라미터 타입으로 사용될 수 있다.
// 매개변수와 리턴 타입에 사용됨.
fun <T> genericFunc(arg: T): T? { . . . }
// 형식 매개변수가 여러 개인 경우
fun <K, V> input(key: K, value: V): Unit { . . . }
- 공변성(Convariance), 반공변성(Contravaraiance)?
간단하게 제네릭에 대해 알아보았으니 이제 공변성과 반공변성, 그리고 in, out 키워드에 대해 알아보자. 하지만 공변성과 반공변성을 알아보기 전에 제공자(Producer)와 소비자(Consumer)에 대해 알아둬야 이해하기 조금 더 수월할 것이다.
제공자(Producer)는 특정 타입의 객체를 생성하고 제공하는 역할을 하는 것을 말한다.
소비자(Consumer)는 특정 타입의 데이터를 소비하거나 사용하는 역할을 하는 것을 말한다.
- 공변성(Convariance)
- 공변성은 하위 타입을 상위 타입으로 사용할 수 있도록 허용한다. 즉 'A'가 'B'의 하위 타입이라면 Producer <A>를 Producer <B>로 사용할 수 있는 것을 의미한다. 공변성은 out 키워드를 사용하여 나타낸다.
예제 코드
open class Fruit
class Banana : Fruit()
class Orange : Fruit()
interface Producer<out T> {
fun produce(): T
}
class BananaProducer : Producer<Banana> {
override fun produce(): Banana {
return Banana()
}
}
fun receiveFruitProducer(producer: Producer<Fruit>) {
val fruit: Fruit = producer.produce()
println("Produced: ${fruit::class.simpleName}")
}
fun main() {
val bananaProducer: Producer<Banana> = BananaProducer()
receiveFruitProducer(bananaProducer) // 에러가 발생하지 않는다. 그 이유는 out 키워드때문이다.
}
- 코드 설명 -
- Producer <out T> 인터페이스는 produce 메서드를 통해 T 타입의 객체를 생성한다.
- BananaProducer 클래스는 Banana를 생성하는 Producer이다.
- receiveFruitProducer 함수는 Producer <Fruit> 타입의 인자를 받는다.
- bananaProducer는 Producer <Banana> 타입이지만, out 키워드 덕분에 Producer <Fruit>로 사용할 수 있다.
- 반공변성(Contravariance)
반공변성은 상위 타입을 하위 타입으로 사용할 수 있도록 허용한다. 즉, A가 B의 상위 타입이라면 Consumer <B>를 Consumer <A>로 사용할 수 있는 것을 의미합니다. 반공변성은 in 키워드를 사용하여 나타낸다.
예제 코드
open class Fruit
class Banana : Fruit()
class Orange : Fruit()
interface Consumer<in T> {
fun consume(item: T)
}
class FruitConsumer : Consumer<Fruit> {
override fun consume(item: Fruit) {
println("Consuming: ${item::class.simpleName}")
}
}
fun feedBananaConsumer(consumer: Consumer<Banana>) {
consumer.consume(Banana())
}
fun main() {
val fruitConsumer: Consumer<Fruit> = FruitConsumer()
feedBananaConsumer(fruitConsumer) // 에러가 발생하지 않는다. 그 이유는 in 키워드때문이다.
}
위의 in과 out은 말 그대로 제네릭으로 지정한 타입 이외에도 개발자가 더 유연하게 사용하고 싶을 때 사용하는 개념이다.
- 코드 설명 -
- Consumer <in T> 인터페이스는 consume 메서드를 통해 T 타입의 객체를 소비한다.
- FruitConsumer 클래스는 Fruit를 소비하는 Consumer이다.
- feedBananaConsumer 함수는 Consumer <Banana> 타입의 인자를 받는다.
- fruitConsumer는 Consumer <Fruit> 타입이지만, in 키워드 덕분에 Consumer <Banana>로 사용할 수 있다.
참고한 자료
https://kotlinlang.org/docs/generics.html#variance-and-wildcards-in-java