본문 바로가기

Language

[Kotlin] 제네릭 generic타입

코틀린 + 스프링을 사용하는 회사 코드에 제네릭 타입이 많이 사용되는데 이참에 제네릭에 대해 정리하고 넘어가고자 한다 !

 

What is Generic  

먼저 제네릭이란,

자료형의 객체들을 다루는 메서드나 클래스에서 컴파일 시간에 자료형을 검사해 적당한 자료형을 선택할 수 있게 해주는 것 이다

보통 앵글 브래킷(<>) 사이에 형식 매개변수(자료형을 대표하는 용어, T와 같이 특정 영문의 대문자 사용)를 사용해 선언한다

fun main(){
    val appleStr = Fruit("apple")
    val bananaInt = Fruit(111)
}

class Fruit<T>(t: T) {
    val value = t
}

정해진건 아니지만 일종의 규칙처럼 적용됨

 

그렇다면 굳이 Any 를 사용하지 않고 generic 을 사용하는 이유가 뭘까

 

Any type vs Generic type  

1. 타입 안정성

Any를 사용하면 실제 타입을 알 수 없기 때문에, 값을 사용할 때 캐스팅이 필요하고, 런타임에서 오류가 발생할 가능성이 높다

제네릭을 사용할 경우 런타임 전인 컴파일 타임에서 오류가 미리 잡히기 때문에 안정성에 적합하다

 

2. 재사용성

제네릭을 사용하면 여러 타입을 처리하는 중복 코드를 줄일 수 있다

fun printString(value: String) { println(value) }
fun printInt(value: Int) { println(value) }
fun printDouble(value: Double) { println(value) }

 

위 코드는 모든 타입에 대해 개별 함수를 작성 해야함

fun <T> printValue(value: T) {
    println(value)
}

fun main() {
    printValue("Hello")  // String
    printValue(123)      // Int
    printValue(12.34)    // Double
}

제네릭 타입을 사용하면 함수 하나로 정의 가능

 

이외에도 가독성 및 유지보수성, 성능 최적화 등의 이유로 Any 대신 Generic 타입을 사용한다

 

 

Generic class  

클래스 내부에서 사용할 데이터 타입을 정하지 않고, 객체를 생성할 때 타입을 지정할 수 있도록 한다

fun main(){
    val appleClass = Fruit("apple")
    println(appleClass.getValue())      // apple
    appleClass.setvalue("empty apple")
    println(appleClass.getValue())      // empty apple

    val bananaClass = Fruit(111)
    println(bananaClass.getValue())     // 111
    bananaClass.setvalue(222)
    println(bananaClass.getValue())     // 222
}

class Fruit<T>(t: T) {
    private var value = t

    fun getValue(): T{
        return value
    }

    fun setvalue(newV: T){
        value = newV
    }
    fun printValue() {
        println(value)
    }
}

객체를 생성할 때 타입을 지정, setValue() 메서드를 사용하여 타입을 변경할 수 있지만 지정된 타입(T)만 허용됨

 

 

또한 여러개의 타입을 받을 수도 있다

fun main(){
    val apple = Fruit("apple", "red")
    apple.printValue()
    
    val banana = Fruit("banana", 222)
    banana.printValue()
}

class Fruit<A, B>(val first: A, val second: B) {
    fun printValue(){
        println("A is ${first} /  B is ${second}")
    }
}

A와 B라는 두 개의 제네릭 타입을 정의

fun main(){
    val aMember = Member("aPeter", 20)
    val bMember = Member("bPeter", 30)

    val car = MyVehicle("bmw", false)
    val train = MyVehicle("ktx", true)
    val group = Group<Member, MyVehicle>()
    group.printAType(aMember)
    group.printBType(car)
}

class Member(
        val name: String,
        val age: Number
)

class MyVehicle(
        val name: String,
        val isExpensive: Boolean
)

class Group<A,B>{
    fun printAType(a: A){
        if(a is Member){
            println("a member's name is ${a.name}")
        }
    }

    fun printBType(b: B){
        if(b is MyVehicle){
            println("b vehicle name is ${b.name}")
        }
    }
}

 

 

 

공변성(out) 반공변성(in)  

타입의 유연성을 보장하면서도 타입 안정성을 유지하는 데 도움되는 개념이다

 

 

out을 사용하면 해당 타입을 반환만 가능하고, 값을 설정할 수 없다 List<T>처럼 읽기 전용 객체를 만들 때 사용한다

in을 사용하면 해당 타입을 입력(consume)만 가능하고, 반환할 수 없다 Comparator<T>처럼 입력 전용 객체를 만들 때 사용한다

 

 

 

 

 

 

 Generic method  

해당 함수나 메서드 앞쪽에 <T>와 같이 지정한다

// 매개변수와 리턴 타입에 사용됨. 
fun <T> genericFunc(arg: T): T? { ... }

// 형식 매개변수가 여러 개인 경우
fun <K, V> put(key: K, value: V): Unit { ... } 
fun main(){
    getGenericValueAndPrint("hello")
    getGenericValueAndPrint(123)
}

fun <T >getGenericValueAndPrint(value: T) {
    println(value)
}

 

 

 

 Generic type for Return  

제네릭으로 선언된 함수에 리턴값을 받을 땐 타입을 알 수 없어 오류가 발생한다

fun <T> add(a: T, b: T): T {
    //return a + b // 타입을 알 수 없어서 에러 발생 
}

 

아래처럼 람다식을 사용하면 위의 문제를 해결할 수 있다

fun <T> add(a: T, b: T, op: (T, T) -> T): T{
    return op(a, b)
}

fun main() {
    val result = add(2, 3) { a, b -> a + b }	//add(2, 3, { a, b -> a + b }) 와 동일
    println(result)
}

 

 

 

'Language' 카테고리의 다른 글