공부가 필요한 문법에 대해서 정리한 글입니다.
Generic이란 사전적인 의미로 포괄적인, 일반적인 이라는 의미를 가지고 있습니다.
가끔 오류가 발생하여 해당 함수를 들어가보면 다음과 같이<T>를 가지고 있는 것을 보았을텐데
이번 포스트에서는<> Generic에 대해 자세히 알아보려고 합니다.

Generic은 쉽게 말해 타입을 파라미터화 할 수 있도록 해주는 기능입니다.
우리가 사용하는 타입을 변수처럼 사용해서 코드의 재사용성과 안정성을 높이는 방법입니다.
이렇게 들으면 쉽게 알아듣지 못할 것 입니다.
비유를 들어봅시다.
마트에 사과박스가 존재한다고 생각합시다.
그럼 기본 클래스에서는 다음과 같이 표현할 수 있습니다.
class AppleBox(val apple: Apple)
그럼 여기에는 사과만 담을 수 있는 박스입니다.
마트 주인이 이제 사과 박스를 재활용하기 위해 다용도 박스로 용도를 바꿨다고 생각해봅시다.
이것이 바로 제너릭 클래스입니다!
class Box<T>(val item: T)
그럼 이 안에는 사과도 다른 어떠한 과일도 아니면 어떠한 물건들도 들어갈 수 있습니다.
val appleBox = Box(Apple())
val bananaBox = Box(Banana())
val bookBox = Box(Book())
박스에 사과를 넣으면 꺼낼 때 무조건 사과만 나옵니다.
코드로 확인해보면
마트에서 사과 박스에는 사과만 들어가야 하는데 책을 넣으면 안됩니다.
컴파일 단계에서 잘못된 타입은 아예 실행되지 않습니다.
//제너릭이 없는 경우
class AppleBox(val item: Any)
fun main() {
val appleBox = AppleBox("책")
val apple = appleBox.item as String // 런타임 오류 가능
}
//제너릭이 있는 경우
class Box<T>(val item: T)
fun main() {
val appleBox = Box("사과")
val apple: String = appleBox.item // 컴파일 타임에 타입 체크
}
코드는 중복이 절대 금지됩니다.
그래서 제너릭은 같은 코드로 여러 타입을 처리할 수 있습니다.
class Box<T>(val item: T)
fun main() {
val appleBox = Box("사과")
val bananaBox = Box("바나나")
val bookBox = Box("책")
}
코틀린은 타입 간의 상속 관계에 따라 제너릭을 어떻게 다룰지 결정하는 중요한 개념입니다.
- 공변성(out) : 물건을 꺼낼 수만 있는 박스
- 반공변성(in) : 물건을 넣을 수만 있는 박스
공변성 -> out
- 읽기 전용 제너릭
- 자식 타입을 부모 타입에 담을 수 있음
- 생성자에서는 사용 불가
- 반환값(return) 에서만 사용 가능
open class Fruit
class Apple : Fruit()
class Banana : Fruit()
class Box<out T>(val item: T)
fun main() {
val appleBox: Box<Apple> = Box(Apple())
val fruitBox: Box<Fruit> = appleBox // 부모 타입으로 대입 가능 (공변성)
println(fruitBox.item)
}
fun getItem(): T // 가능
fun putItem(item: T) // 컴파일 에러
반공변성 -> in
- 쓰기 전용 제너릭
- 부모 타입을 자식 타입에 담을 수 있음
- 매개변수에서만 사용 가능
- 반환값에서는 사용 불가능
open class Fruit
class Apple : Fruit()
class Banana : Fruit()
class Box(in T){
fun put(item: T){
println("$item 넣기")
}
}
fun main() {
val fruitBox: Box<Fruit> = Box<Fruit>()
val appleBox: Box<Apple> = fruitBox // 부모 → 자식 대입 가능 (반공변성)
appleBox.put(Apple())
}
fun getItem(): T // 컴파일 에러
fun putItem(item: T) // 가능
박스에 특정 조류의 물건만 담도록 제한할 수 있습니다
open class Fruit
fun <T : Fruit> getFruit(item: T) {
println("과일: $item")
}
fun main() {
getFruit(Apple()) // 가능
getFruit(Book()) // 에러
}
비유하면 모든 물건을 담을 수 있는 만능 박스 같은 역할을 합니다
fun printList(list: List<*>) {
list.forEach { println(it) }
}
fun main() {
printList(listOf(1, "Hello", 3.5))
}
// 출력
// 1
// Hello
// 3.5
class Box<T>(val item: T)
| 타입 기호 | 의미 |
|---|---|
| T | Type (어떤 타입이든 들어갈 수 있음) |
| E | Element (리스트 같은 자료구조) |
| K | Key (맵의 키) |
| V | Value(맵의 값) |
| N | Number (숫자 타입) |
//예시
class Pair<K, V>(val key: K, val value: V)
fun main(){
val pair = Pair("이름", "Hood")
println(pair.key) // 이름
println(pair.value) // Hood
}
제너릭을 정리해보면 타입을 코드에 명시하지 않고도 다양한 타입의 객체를 처리할 수 있는
기능입니다. 이를 잘 활용하면 코드의 재사용성과 타입 안정성 등을 챙길 수 있고
코드 유지보수에도 용이한 중요한 기능입니다.