코틀린 + 스프링을 사용하는 회사 코드에 제네릭 타입이 많이 사용되는데 이참에 제네릭에 대해 정리하고 넘어가고자 한다 !
✅ 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' 카테고리의 다른 글
[Kotlin] - Channels (0) | 2025.02.09 |
---|---|
[Kotlin] - 기본 문법 정리 (1) | 2025.02.02 |
[Kotlin] 코루틴 (2) - Cancellation and Timeouts (1) | 2025.02.01 |
[Kotlin] 코루틴 (1) - Basic (1) | 2025.02.01 |