안녕하세요, '소프트웨어 공장'에 오신 것을 환영합니다! 오늘은 Kotlin에서 굉장히 중요한 개념 중 하나인 제네릭(Generics) 에 대해 알아보겠습니다. 제네릭은 코드의 재사용성을 높이고, 타입 안정성을 유지하는 데 큰 역할을 합니다. 이 글에서는 제네릭이 무엇인지, 어떻게 사용하는지, 그리고 여러 가지 예제들을 통해 이해를 돕도록 하겠습니다.
제네릭이란 무엇인가요?
제네릭(Generics) 은 타입을 매개변수로 받아 코드의 중복을 줄이고, 타입 안전성을 높이는 기능입니다. 제네릭을 사용하면 다양한 타입을 처리할 수 있는 범용적인 함수를 작성할 수 있습니다. 예를 들어, 동일한 로직을 처리하는 리스트나 맵 같은 컬렉션 클래스가 다양한 타입을 가질 수 있는 이유는 바로 제네릭 덕분입니다.
Kotlin에서는 Java와 비슷하게 제네릭을 사용하며, 추가적인 편리함을 제공하는 기능들도 포함되어 있습니다.
다음은 제네릭을 사용하는 기본적인 예입니다:
class Box<T>(val value: T)
fun main() {
val intBox = Box(123)
val stringBox = Box("Hello Kotlin")
println(intBox.value) // 출력: 123
println(stringBox.value) // 출력: Hello Kotlin
}
위 예제에서 Box
클래스는 제네릭 타입 파라미터 T
를 가지며, 이는 생성자에서 초기화할 때 어떤 타입이든 가질 수 있음을 의미합니다.
제네릭의 이점
제네릭은 다음과 같은 이점들을 제공합니다:
- 코드의 재사용성 증가: 같은 코드를 여러 타입에 대해 사용할 수 있습니다.
- 타입 안정성: 컴파일 단계에서 타입 체크가 이루어지므로 런타임 오류를 줄일 수 있습니다.
- 유연성: 제네릭을 사용하면 다양한 타입을 유연하게 처리할 수 있습니다.
제네릭 함수
제네릭을 클래스뿐만 아니라 함수에도 사용할 수 있습니다. 다음은 제네릭 함수를 정의하는 예입니다:
fun <T> printItem(item: T) {
println(item)
}
fun main() {
printItem(42) // 출력: 42
printItem("Hello World") // 출력: Hello World
}
위 코드에서 printItem
함수는 T
라는 제네릭 타입을 사용하여 어떤 타입의 인자도 받을 수 있습니다. 덕분에 Int
나 String
을 넘겨도 잘 동작합니다.
제네릭 제약 (Constraints)
때로는 제네릭 타입이 특정한 상위 클래스를 상속받거나 인터페이스를 구현하도록 제한하고 싶을 때가 있습니다. 이를 제네릭 제약 (Constraints) 이라고 합니다.
fun <T : Number> addNumbers(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
fun main() {
println(addNumbers(10, 20)) // 출력: 30.0
println(addNumbers(1.5, 2.3)) // 출력: 3.8
// println(addNumbers("Hello", "World")) // 컴파일 오류: String은 Number의 서브클래스가 아님
}
위 예제에서 addNumbers
함수는 T
가 Number
를 상속받는 타입으로 제한하고 있습니다. 따라서 Int
, Double
등의 숫자 타입은 사용 가능하지만, String
과 같은 다른 타입은 사용할 수 없습니다.
제네릭 클래스의 활용
제네릭은 클래스에도 자주 사용됩니다. Kotlin의 표준 라이브러리인 List
, Set
, Map
등은 모두 제네릭 클래스를 기반으로 하고 있습니다.
다음은 제네릭 클래스를 사용하는 예입니다:
class Pair<K, V>(val key: K, val value: V)
fun main() {
val pair = Pair("name", "Kotlin")
println("Key: ${pair.key}, Value: ${pair.value}") // 출력: Key: name, Value: Kotlin
val intPair = Pair(1, 100)
println("Key: ${intPair.key}, Value: ${intPair.value}") // 출력: Key: 1, Value: 100
}
Pair
클래스는 두 개의 제네릭 타입 K
와 V
를 받아 키와 값을 관리합니다. 이렇게 하면 다양한 타입의 조합으로 Pair
객체를 생성할 수 있습니다.
공변성과 반공변성
Kotlin에서 제네릭 타입은 공변성(covariance) 과 반공변성(contravariance) 을 통해 다룰 수 있습니다. 이를 통해 클래스 간의 상속 관계를 더 세밀하게 조절할 수 있습니다.
- 공변성 (
out
키워드): 공변성은 제네릭 타입이 특정 타입의 서브타입일 수 있도록 합니다.out
키워드는 제네릭 타입을 공변으로 만듭니다.
class Box<out T>(val value: T)
fun main() {
val strBox: Box<String> = Box("Hello")
val anyBox: Box<Any> = strBox // 공변성이 있기 때문에 가능
println(anyBox.value) // 출력: Hello
}
위 코드에서 Box<out T>
는 T
가 공변임을 의미합니다. 즉, Box<String>
은 Box<Any>
의 서브타입이 될 수 있습니다.
- 반공변성 (
in
키워드): 반공변성은 제네릭 타입이 특정 타입의 슈퍼타입으로 동작할 수 있게 합니다.in
키워드는 제네릭 타입을 반공변으로 만듭니다.
class Printer<in T> {
fun print(value: T) {
println(value)
}
}
fun main() {
val stringPrinter: Printer<String> = Printer()
val anyPrinter: Printer<Any> = stringPrinter // 반공변성이 있기 때문에 가능
anyPrinter.print(123) // 출력: 123
}
위 코드에서 Printer<in T>
는 T
가 반공변임을 의미합니다. 따라서 Printer<String>
은 Printer<Any>
로 할당될 수 있습니다.
스타 프로젝션 (Star Projection)
제네릭 타입을 특정하지 않고 사용하는 방법으로 스타 프로젝션(Star Projection) 이 있습니다. 이는 Java의 와일드카드(?
)와 비슷한 개념입니다.
fun printList(list: List<*>) {
for (item in list) {
println(item)
}
}
fun main() {
val stringList: List<String> = listOf("A", "B", "C")
val intList: List<Int> = listOf(1, 2, 3)
printList(stringList) // 출력: A B C
printList(intList) // 출력: 1 2 3
}
printList
함수는 리스트의 타입을 알 수 없을 때 List<*>
를 사용하여 어떤 타입의 리스트든 처리할 수 있도록 합니다.
마무리
오늘은 Kotlin에서 중요한 개념인 제네릭(Generics) 에 대해 알아보았습니다. 제네릭을 사용하면 코드를 더욱 재사용성 있게 만들고, 타입 안정성을 유지하면서 유연하게 다양한 타입을 처리할 수 있습니다.
코틀린 제네릭은 자주 사용하는 개념이므로, 잘 익혀두면 실제 프로젝트에서도 큰 도움이 될 것입니다.
'kotlin' 카테고리의 다른 글
Kotlin 함수형 프로그래밍 (0) | 2024.12.19 |
---|---|
Kotlin 애노테이션과 리플렉션 (0) | 2024.12.18 |
Kotlin 배열(Array), 리스트(List), 맵(Map) (0) | 2024.12.16 |
Kotlin 클래스와 객체지향 프로그래밍 (0) | 2024.12.15 |
Kotlin 문자열 처리 - 문자열 템플릿과 함수 활용하기 (0) | 2024.12.14 |