구글링을 하거나, 라이브러리의 소스 등을 보면 <T>, <K, V>, <*> 이런 코드들이 보입니다.
도대체 뭘까요?
이번 게시물에서는 이 코드들에 대해서 알아보고, 예제를 통해 사용법을 익히고, 친해져보도록 하겠습니다.
개요
- 명칭은 제네릭이라고 합니다. 말그대로 데이터 타입을 일반화 한다라는 의미
- 클래스나 메소드(함수), 인터페이스 등에서 사용이 가능
- 데이터 타입을 컴파일시 미리 지정하는 방법으로 사용
- 코드 재사용성이 높아짐
- 잘못된 데이터가 들어오는 경우, 컴파일시에 에러가 발생하기때문에 미리 문제를 수정할 수 있음
(Object를 사용할 경우, 컴파일시엔 문제가 없고 런타임시에 에러가 발생하면 자칫 앱이 배포된 이후에 문제를 확인될 수 있음)
재네릭 타입에 대해서 실제 예제 코드를 보면서 설명 드리겠습니다.
왜 사용해야 하나?
예를 들어, 숫자를 표시하는 클래스가 있습니다. 그런데, 동일한 기능을 하는 텍스트를 표시하는 클래스가 필요해 졌습니다.
그렇다면 기존에는 숫자를 표시하는 클래스, 텍스트를 표시하는 클래스 2개가 있어야 합니다.
/**
* 동일한 동작을 하는 클래스에 입력 파라미터 타입이 다르다면, 타입별로 클래스를 만들어야 함.
*/
IntegerPrinter(123).runPrint()
StringPrinter("Re:Mind").runPrint()
class IntegerPrinter(private val printValue: Int) {
fun runPrint() {
println("$printValue")
}
}
class StringPrinter(private val printValue: String) {
fun runPrint() {
println(printValue)
}
}
위와 같이 비효율적으로 중복 코드가 생성되는 것을 줄이기위해 <T>를 사용합니다.
/**
* 제네릭 타입으로 클래스를 만들면, 입력 파라미터 타입이 다른더라도 하나의 클래스로 사용이 가능함.
*/
EveryPrinter<Int>(123).runPrint()
EveryPrinter<String>("Re:Mind").runPrint()
class EveryPrinter<T>(private val printValue: T) {
fun runPrint() {
println("$printValue")
}
}
위의 EveryPrinter 클래스를 보시면 클래스명 뒤에 <T> 가 붙고, 파라메터의 변수 타입에 T 가 들어갑니다.
이제 호출하는 부분을 보시면, 클래스를 생성할때 <Int> 또는 <String>이 들어가는 것을 보실 수 있습니다.
감이 오신분도 계실텐데, 클래스 생성시 파라미터로 들어갈 데이터의 타입을 선언해 주는 것입니다.
EveryPrinter<Int> 라고 클래스 생성을 하면 printValue 의 변수 타입은 Int 라는 것으로 명시해주시는 것입니다.
이를 통해 중복 기능의 클래스를 줄일수 있고, 클래스 생성시 선언된 타입 외의 파라미터 타입을 입력할 경우, 컴파일 시에 에러를 확인할 수 있게됩니다.
그러면, <T>에 원하는 데이터 타입만 선언하면 다~ 돌아가는 거야?!! 라고 생각할수도 있겠지만, 실제로 해당 클래스나 함수가 동작을 하기위해서는 특정 데이터 타입만 받을 수 있도록 제한을 해야 합니다.
class Dog(name: String, age: Int): Animal(name, age)
class Cat(name: String, age: Int): Animal(name, age)
open class Animal(private val name: String, private val age: Int) {
override fun toString(): String {
return "${name}는 ${age}살 입니다."
}
}
Dog, Cat 클래스를 만들고 Animal 클래스를 상속 했습니다.
/**
* 타입 제한을 위한 코드
*/
AnimalPrinter(Dog("바둑이", 3)).runPrint()
AnimalPrinter(Cat("나비", 5)).runPrint()
/**
* 타입 제한 (Animal 클래스 타입만 가능)
*/
class AnimalPrinter<T : Animal>(private val printAnimal: T) {
fun runPrint() {
println(printAnimal.toString())
}
}
위의 코드와 같이 AnimalPrinter<T : Animal> 와 같이 추가하여 Animal 클래스와 관련된(?) 클래스만 입력을 받겠다라고 제한을 할수가 있습니다.
이렇게하면 Animal 외에 다른 데이터 타입을 입력시 컴파일 오류가 발생하게 됩니다.
제네럴 타입은 하나만 되는거야? 여러개를 할수 없어? 라는 의문이 생기실수 있습니다.
당연히 가능하죠.
/**
* 여러개의 재네릭 타입을 사용할 수 있음.
* (함수, 클래스, 인터페이스 모두 가능)
*/
printHello("바둑이", 3)
printHello("나비", "다섯")
private fun <T1, T2> printHello(name: T1, age: T2) {
println("${name}는 ${age}살 입니다.")
}
<T1, T2, ....> 와 같이 제네럴 타입을 콤마(,)로 여러개를 사용할 수 있습니다.
위의 코드는 메소드로 구현한 예제로 클래스나 인터페이스에도 동일하게 사용이 가능합니다.
그리고, 첫번째 호출시엔 (String, Int) 였던 반면, 두번째 호출시엔 (String, String) 으로 호출 되었음을 참고하세요.
제네럴 타입은 Type Parameter Naming Conventions(유형 매개변수 명명 규칙)이 있습니다.
- E - 요소 (Collections Framework에서 사용)
- K - 키
- N - 숫자
- T - 타입
- V - 값
- S,U,V etc. - 2, 3, 4 번째 types
다른 개발자들과 함께 개발을 한다면 위의 몀명 규칙을 지켜서 구현하면 좋을것 같습니다.
이제 <*> 에 대해서 알아보겠습니다.
사실, 저도 많이 알지는 못해서 잘 사용하지는 않습니다.
아는 부분정도만 설명 드릴게요.
이름은 Star-Projection(스타 프로젝션) 이라고 부릅니다.
Java에서 <?> 와일드 카드 타입과 동일하게 사용되는 것 같습니다.
/**
* 제네릭 파라미터 유형을 모르는 경우 <스타 프로젝션>
*/
val animals = listOf<Animal>(
Dog("바둑이", 3),
Cat("나비", 5)
)
printList(animals)
printList(listOf<Int>(123, 456))
private fun printList(list: List<*>) {
list.map { println(it.toString()) }
}
위의 printList 함수의 파라미터 타입에 List<*> 와 같이 사용됩니다.
파라미터 유형을 모르는 경우에 사용한다고 하는데.... <T> 를 사용했을 때와의 차이점은 잘 모르겠습니다.😩
위의 예제 코드 실행 결과 화면입니다.
전체 소스 코드는 아래 Github 링크를 확인해 주세요.
'Android + Kotlin' 카테고리의 다른 글
[Android Kotlin] crossinline, reified 를 알아보자. (0) | 2022.01.21 |
---|---|
[Android Kotlin] inline 함수 어떤 경우에 사용하나요? (0) | 2022.01.20 |
[Android Kotlin] stetho를 이용하여 REST API 데이터 디버깅하기 (0) | 2022.01.11 |
[Android Kotlin] Paging 3.0 리스트에 Empty View와 Item Listener 구현하기 (0) | 2022.01.11 |
[Android Kotlin] 페이징 리스트에 리플래시(SwipeRefreshLayout) 구현하기 (0) | 2022.01.09 |