[Kotlin] Generic

Hood·2025년 3월 5일

Kotlin

목록 보기
14/18
post-thumbnail

✍  코틀린과 친해지자

공부가 필요한 문법에 대해서 정리한 글입니다.


들어가기 전

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


What is Generic?

Generic은 쉽게 말해 타입을 파라미터화 할 수 있도록 해주는 기능입니다.
우리가 사용하는 타입을 변수처럼 사용해서 코드의 재사용성과 안정성을 높이는 방법입니다.
이렇게 들으면 쉽게 알아듣지 못할 것 입니다.
비유를 들어봅시다.

예를 들면?

1. 기본 클래스

마트에 사과박스가 존재한다고 생각합시다.
그럼 기본 클래스에서는 다음과 같이 표현할 수 있습니다.

class AppleBox(val apple: Apple)

그럼 여기에는 사과만 담을 수 있는 박스입니다.

2. 제너릭 클래스

마트 주인이 이제 사과 박스를 재활용하기 위해 다용도 박스로 용도를 바꿨다고 생각해봅시다.
이것이 바로 제너릭 클래스입니다!

class Box<T>(val item: T)

그럼 이 안에는 사과도 다른 어떠한 과일도 아니면 어떠한 물건들도 들어갈 수 있습니다.

val appleBox = Box(Apple())
val bananaBox = Box(Banana())
val bookBox = Box(Book())

제너릭 특징

1. 타입 안정성(Type Safety)

박스에 사과를 넣으면 꺼낼 때 무조건 사과만 나옵니다.
코드로 확인해보면
마트에서 사과 박스에는 사과만 들어가야 하는데 책을 넣으면 안됩니다.
컴파일 단계에서 잘못된 타입은 아예 실행되지 않습니다.

//제너릭이 없는 경우
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 // 컴파일 타임에 타입 체크
}

2. 코드 재사용성

코드는 중복이 절대 금지됩니다.
그래서 제너릭은 같은 코드로 여러 타입을 처리할 수 있습니다.

class Box<T>(val item: T)

fun main() {
    val appleBox = Box("사과")
    val bananaBox = Box("바나나")
    val bookBox = Box("책")
}

3. 가변성

코틀린은 타입 간의 상속 관계에 따라 제너릭을 어떻게 다룰지 결정하는 중요한 개념입니다.

  • 공변성(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) // 가능

4. 타입 제약 조건

박스에 특정 조류의 물건만 담도록 제한할 수 있습니다

open class Fruit

fun <T : Fruit> getFruit(item: T) {
    println("과일: $item")
}

fun main() {
    getFruit(Apple())  // 가능
    getFruit(Book())   // 에러
}

5. 스타 프로젝션(*)

비유하면 모든 물건을 담을 수 있는 만능 박스 같은 역할을 합니다

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)
타입 기호의미
TType (어떤 타입이든 들어갈 수 있음)
EElement (리스트 같은 자료구조)
KKey (맵의 키)
VValue(맵의 값)
NNumber (숫자 타입)
//예시
class Pair<K, V>(val key: K, val value: V)

fun main(){
    val pair = Pair("이름", "Hood")
    println(pair.key)   // 이름
    println(pair.value) // Hood
}

📌결론

제너릭 을 정리해보면 타입을 코드에 명시하지 않고도 다양한 타입의 객체를 처리할 수 있는
기능입니다. 이를 잘 활용하면 코드의 재사용성과 타입 안정성 등을 챙길 수 있고
코드 유지보수에도 용이한 중요한 기능입니다.

profile
달을 향해 쏴라, 빗나가도 별이 될 테니 👊

0개의 댓글