Notice
Recent Posts
Recent Comments
Link
반응형
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- 알고리즘
- k8s
- 김영한
- 자바
- 알고리즘정렬
- 함수형프로그래밍
- Sort
- Kotlin
- Effective Java 3
- Effective Java
- java
- ElasticSearch
- 예제로 배우는 스프링 입문
- 스프링부트
- 이펙티브 자바
- 자바스크립트
- 스프링 핵심원리
- 티스토리챌린지
- 클린아키텍처
- 스프링핵심원리
- kubernetes
- 엘라스틱서치
- 이차전지관련주
- 카카오
- 오블완
- 스프링
- 이펙티브자바
- Spring
- JavaScript
- effectivejava
Archives
- Today
- Total
Kim-Baek 개발자 이야기
Kotlin 제네릭스 완벽 가이드 | in, out, reified 마스터하기 본문
반응형
이 글을 읽으면: Java 제네릭의 복잡함을 넘어 Kotlin의 강력한 타입 시스템을 배울 수 있습니다. 공변성(out), 반공변성(in), reified 타입 파라미터까지 실전 예제로 완벽하게 마스터하세요.
📌 목차
- 들어가며 - Java 제네릭의 한계
- 제네릭 기본 - 타입 파라미터
- 타입 제약 - where, upper bound
- 공변성 out - 생산자
- 반공변성 in - 소비자
- reified - 타입 소거 해결
- 스타 프로젝션 *
- 실전 패턴 모음
- 마무리 - 다음 편 예고
들어가며 - Java 제네릭의 한계
Java 제네릭을 사용할 때 이런 경험 있으신가요?
// Java - 타입 소거 때문에 런타임에 타입 확인 불가
public <T> void process(List<T> list) {
// if (list instanceof List<String>) { } // ❌ 컴파일 에러
// T instance = new T(); // ❌ 불가능
}
// Java - 공변성 문제
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // ❌ 컴파일 에러
// 해결책 - 와일드카드 (복잡함)
public void print(List<? extends Object> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
// Java - 불변성 강제
List<String> strings = Arrays.asList("a", "b");
List<? extends CharSequence> sequences = strings; // OK (읽기만)
// sequences.add("c"); // ❌ 컴파일 에러
Kotlin은 이렇게 간단합니다:
// Kotlin - reified로 런타임 타입 확인
inline fun <reified T> process(list: List<T>) {
if (list is List<String>) { // ✅ 가능!
// ...
}
}
// Kotlin - out으로 공변성
fun print(list: List<out Any>) { // List<String>도 OK
for (item in list) {
println(item)
}
}
// Kotlin - in으로 반공변성
fun addStrings(list: MutableList<in String>) {
list.add("Hello") // ✅ 가능
}
실제 프로젝트에서 겪은 문제:
상황: API 응답을 제네릭으로 처리하려고 함
문제: 런타임에 응답 타입을 알 수 없어서 역직렬화 실패
해결: Kotlin의 reified를 사용해 타입 정보 유지
오늘은 Kotlin의 제네릭을 완전히 마스터해보겠습니다!

제네릭 기본 - 타입 파라미터
제네릭 클래스
// 기본 제네릭 클래스
class Box<T>(val value: T) {
fun get(): T = value
}
fun main() {
val intBox = Box(42)
println(intBox.get()) // 42
val stringBox = Box("Kotlin")
println(stringBox.get()) // Kotlin
// 타입 추론
val autoBox = Box(3.14) // Box<Double>
}
제네릭 함수
// 제네릭 함수
fun <T> singletonList(item: T): List<T> {
return listOf(item)
}
fun <T> swap(a: T, b: T): Pair<T, T> {
return b to a
}
fun main() {
val list = singletonList(42)
println(list) // [42]
val (first, second) = swap("A", "B")
println("$first, $second") // B, A
}
여러 타입 파라미터
class Pair<K, V>(val key: K, val value: V) {
override fun toString() = "$key: $value"
}
fun <K, V> createPair(key: K, value: V): Pair<K, V> {
return Pair(key, value)
}
fun main() {
val pair1 = Pair("name", "규철")
val pair2 = Pair(1, "첫 번째")
println(pair1) // name: 규철
println(pair2) // 1: 첫 번째
}
Java와 비교
// Java - 타입 파라미터 명시
Box<String> box = new Box<>("Kotlin");
List<Integer> list = Arrays.asList(1, 2, 3);
// 다이아몬드 연산자 (Java 7+)
Box<String> box = new Box<>("Kotlin");
// Kotlin - 타입 추론
val box = Box("Kotlin") // Box<String>
val list = listOf(1, 2, 3) // List<Int>
타입 제약 - where, upper bound
Upper Bound (상한)
// Number 또는 그 하위 타입만
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
fun main() {
println(sum(10, 20)) // 30.0
println(sum(3.14, 2.86)) // 6.0
// println(sum("A", "B")) // ❌ 컴파일 에러
}
여러 제약 - where
// 여러 인터페이스 구현 필요
fun <T> process(item: T) where T : Comparable<T>, T : CharSequence {
println("Length: ${item.length}")
println("Compare: ${item.compareTo(item)}")
}
fun main() {
process("Kotlin") // String은 Comparable & CharSequence
// process(42) // ❌ Int는 CharSequence 아님
}
실전 예제 - 정렬 가능한 컬렉션
fun <T : Comparable<T>> List<T>.sorted(): List<T> {
return this.sortedWith(compareBy { it })
}
fun <T : Comparable<T>> findMax(list: List<T>): T? {
return list.maxOrNull()
}
fun main() {
val numbers = listOf(5, 2, 8, 1, 9)
println(numbers.sorted()) // [1, 2, 5, 8, 9]
println(findMax(numbers)) // 9
val words = listOf("Kotlin", "Java", "Python")
println(words.sorted()) // [Java, Kotlin, Python]
}
실전 예제 - Repository 패턴
interface Entity {
val id: Long
}
data class User(override val id: Long, val name: String) : Entity
data class Product(override val id: Long, val name: String, val price: Int) : Entity
class Repository<T : Entity> {
private val items = mutableListOf<T>()
fun save(item: T) {
items.add(item)
}
fun findById(id: Long): T? {
return items.find { it.id == id }
}
fun findAll(): List<T> = items.toList()
}
fun main() {
val userRepo = Repository<User>()
userRepo.save(User(1, "규철"))
userRepo.save(User(2, "영희"))
println(userRepo.findById(1)) // User(id=1, name=규철)
println(userRepo.findAll())
}
공변성 out - 생산자
문제 상황
fun main() {
val strings: List<String> = listOf("A", "B", "C")
// List<String>은 List<Any>의 하위 타입?
// val objects: List<Any> = strings // Kotlin에서는 OK!
// 하지만 가변 리스트는?
val mutableStrings: MutableList<String> = mutableListOf("A")
// val mutableObjects: MutableList<Any> = mutableStrings // ❌ 에러
// 왜? mutableObjects.add(123)을 하면 타입 안전성 깨짐
}
out - 공변성 선언
"이 타입은 생산만 하고 소비하지 않습니다"
// 읽기 전용 (생산자)
interface Producer<out T> {
fun produce(): T
// fun consume(item: T) // ❌ out 위치에서 in 사용 불가
}
class StringProducer : Producer<String> {
override fun produce(): String = "Kotlin"
}
fun printProducer(producer: Producer<Any>) {
println(producer.produce())
}
fun main() {
val stringProducer = StringProducer()
printProducer(stringProducer) // ✅ OK (공변성)
}
실전 예제 - API 응답
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun handleUserResult(result: Result<User>) {
when (result) {
is Result.Success -> println("User: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
Result.Loading -> println("Loading...")
}
}
fun handleAnyResult(result: Result<Any>) {
when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
Result.Loading -> println("Loading...")
}
}
fun main() {
val userResult: Result<User> = Result.Success(User(1, "규철"))
// 공변성 덕분에 가능
handleAnyResult(userResult)
}
List는 공변적
fun main() {
val strings: List<String> = listOf("A", "B", "C")
// List<out T>로 선언되어 있어서 가능
val objects: List<Any> = strings // ✅ OK
// 하지만 추가는 불가 (읽기 전용)
// objects.add(123) // List는 불변이므로 add 자체가 없음
}
반공변성 in - 소비자
in - 반공변성 선언
"이 타입은 소비만 하고 생산하지 않습니다"
// 쓰기 전용 (소비자)
interface Consumer<in T> {
fun consume(item: T)
// fun produce(): T // ❌ in 위치에서 out 사용 불가
}
class AnyConsumer : Consumer<Any> {
override fun consume(item: Any) {
println("Consuming: $item")
}
}
fun processStrings(consumer: Consumer<String>) {
consumer.consume("Kotlin")
consumer.consume("Java")
}
fun main() {
val anyConsumer = AnyConsumer()
// 반공변성 덕분에 가능
processStrings(anyConsumer) // ✅ OK
}
실전 예제 - Comparator
data class Person(val name: String, val age: Int)
fun main() {
val people = listOf(
Person("규철", 30),
Person("영희", 25),
Person("철수", 35)
)
// Comparator<in T>는 반공변적
val anyComparator = Comparator<Any> { a, b ->
a.hashCode().compareTo(b.hashCode())
}
// Person을 비교하는데 Comparator<Any> 사용 가능
val sorted = people.sortedWith(anyComparator)
println(sorted)
}
실전 예제 - 이벤트 핸들러
interface ClickEvent
interface DoubleClickEvent : ClickEvent
interface EventHandler<in T> {
fun handle(event: T)
}
class GeneralClickHandler : EventHandler<ClickEvent> {
override fun handle(event: ClickEvent) {
println("Click handled")
}
}
fun registerDoubleClickHandler(handler: EventHandler<DoubleClickEvent>) {
val event = object : DoubleClickEvent {}
handler.handle(event)
}
fun main() {
val generalHandler = GeneralClickHandler()
// ClickEvent 핸들러를 DoubleClickEvent에 사용 (반공변성)
registerDoubleClickHandler(generalHandler)
}
PECS 원칙 (Producer Extends Consumer Super)
// Producer - out (공변)
fun copy(from: List<out Number>, to: MutableList<Number>) {
for (item in from) {
to.add(item)
}
}
// Consumer - in (반공변)
fun fill(list: MutableList<in Int>, value: Int) {
list.add(value)
}
fun main() {
val ints = listOf(1, 2, 3)
val numbers = mutableListOf<Number>()
copy(ints, numbers) // List<Int> -> List<out Number>
println(numbers) // [1, 2, 3]
val anys = mutableListOf<Any>()
fill(anys, 42) // MutableList<Any> -> MutableList<in Int>
println(anys) // [42]
}
reified - 타입 소거 해결
타입 소거 문제
// ❌ 일반 제네릭 - 런타임에 타입 정보 없음
fun <T> isType(value: Any): Boolean {
// return value is T // ❌ 컴파일 에러
return false
}
reified - 타입 정보 유지
inline 함수에서만 사용 가능
// ✅ reified - 런타임에 타입 확인 가능
inline fun <reified T> isType(value: Any): Boolean {
return value is T
}
fun main() {
println(isType<String>("Kotlin")) // true
println(isType<String>(42)) // false
println(isType<Int>(42)) // true
}
실전 예제 - 타입별 필터링
inline fun <reified T> List<*>.filterIsInstance(): List<T> {
val result = mutableListOf<T>()
for (item in this) {
if (item is T) {
result.add(item)
}
}
return result
}
fun main() {
val mixed = listOf(1, "A", 2, "B", 3.14, "C")
val strings = mixed.filterIsInstance<String>()
println(strings) // [A, B, C]
val numbers = mixed.filterIsInstance<Int>()
println(numbers) // [1, 2]
}
실전 예제 - JSON 파싱
import com.google.gson.Gson
inline fun <reified T> String.fromJson(): T {
return Gson().fromJson(this, T::class.java)
}
data class User(val id: Long, val name: String)
fun main() {
val json = """{"id":1,"name":"규철"}"""
// 타입 명시 없이 파싱 가능
val user = json.fromJson<User>()
println(user) // User(id=1, name=규철)
}
실전 예제 - Activity 시작 (Android)
// Android 예제
inline fun <reified T : Activity> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}
// 사용
// startActivity<MainActivity>()
// startActivity<SettingsActivity>()
실전 예제 - API 클라이언트
class ApiClient {
inline fun <reified T> get(url: String): T {
val response = performRequest(url)
return parseJson<T>(response)
}
inline fun <reified T> parseJson(json: String): T {
return Gson().fromJson(json, T::class.java)
}
private fun performRequest(url: String): String {
// HTTP 요청 수행
return """{"id":1,"name":"규철"}"""
}
}
fun main() {
val client = ApiClient()
// 타입 추론으로 간단하게
val user: User = client.get("/users/1")
println(user)
}
스타 프로젝션 *
기본 사용법
타입을 모를 때 사용하는 와일드카드
fun printList(list: List<*>) {
for (item in list) {
println(item) // item은 Any? 타입
}
}
fun main() {
printList(listOf(1, 2, 3))
printList(listOf("A", "B", "C"))
printList(listOf(User(1, "규철")))
}
out Any? vs *
fun main() {
val list1: List<*> = listOf(1, 2, 3)
val list2: List<out Any?> = listOf(1, 2, 3)
// 둘 다 읽기는 가능
val item1: Any? = list1.first()
val item2: Any? = list2.first()
// 하지만 MutableList에서는 차이
val mutable: MutableList<*> = mutableListOf(1, 2, 3)
// mutable.add(4) // ❌ 컴파일 에러 (타입 모름)
}
실전 예제 - 제네릭 검증
fun validate(list: List<*>): Boolean {
return list.isNotEmpty() && list.all { it != null }
}
fun main() {
println(validate(listOf(1, 2, 3))) // true
println(validate(listOf("A", null, "B"))) // false
println(validate(emptyList<Any>())) // false
}
실전 패턴 모음
패턴 1: Result 타입
sealed class Result<out T, out E> {
data class Success<T>(val value: T) : Result<T, Nothing>()
data class Failure<E>(val error: E) : Result<Nothing, E>()
}
inline fun <T, E, R> Result<T, E>.map(transform: (T) -> R): Result<R, E> {
return when (this) {
is Result.Success -> Result.Success(transform(value))
is Result.Failure -> Result.Failure(error)
}
}
inline fun <T, E, R> Result<T, E>.flatMap(
transform: (T) -> Result<R, E>
): Result<R, E> {
return when (this) {
is Result.Success -> transform(value)
is Result.Failure -> Result.Failure(error)
}
}
fun main() {
val result: Result<Int, String> = Result.Success(42)
val doubled = result.map { it * 2 }
println(doubled) // Success(value=84)
val chained = result.flatMap {
Result.Success(it.toString())
}
println(chained) // Success(value=42)
}
패턴 2: 캐시 구현
class Cache<K, V> {
private val map = mutableMapOf<K, V>()
fun get(key: K): V? = map[key]
fun put(key: K, value: V) {
map[key] = value
}
fun getOrPut(key: K, defaultValue: () -> V): V {
return map.getOrPut(key, defaultValue)
}
}
fun main() {
val cache = Cache<String, User>()
cache.put("user1", User(1, "규철"))
val user = cache.getOrPut("user2") {
User(2, "영희") // 없으면 생성
}
println(user)
}
패턴 3: 빌더 패턴
class QueryBuilder<T> {
private val conditions = mutableListOf<(T) -> Boolean>()
fun where(condition: (T) -> Boolean): QueryBuilder<T> {
conditions.add(condition)
return this
}
fun execute(items: List<T>): List<T> {
return items.filter { item ->
conditions.all { it(item) }
}
}
}
fun <T> query() = QueryBuilder<T>()
fun main() {
data class Product(val name: String, val price: Int, val stock: Int)
val products = listOf(
Product("노트북", 1500000, 5),
Product("마우스", 30000, 50),
Product("키보드", 100000, 20)
)
val result = query<Product>()
.where { it.price >= 50000 }
.where { it.stock >= 10 }
.execute(products)
println(result)
// [Product(name=노트북, price=1500000, stock=5),
// Product(name=키보드, price=100000, stock=20)]
}
패턴 4: 타입 안전한 DSL
@DslMarker
annotation class HtmlDsl
@HtmlDsl
class HTML {
private val children = mutableListOf<Element>()
fun <T : Element> tag(element: T, init: T.() -> Unit): T {
element.init()
children.add(element)
return element
}
fun body(init: Body.() -> Unit) = tag(Body(), init)
override fun toString() = children.joinToString("\n")
}
@HtmlDsl
abstract class Element {
protected val children = mutableListOf<String>()
abstract val tag: String
operator fun String.unaryPlus() {
children.add(this)
}
override fun toString() = "<$tag>${children.joinToString("")}</$tag>"
}
@HtmlDsl
class Body : Element() {
override val tag = "body"
fun h1(init: H1.() -> Unit) {
val h1 = H1()
h1.init()
children.add(h1.toString())
}
fun p(init: P.() -> Unit) {
val p = P()
p.init()
children.add(p.toString())
}
}
@HtmlDsl
class H1 : Element() {
override val tag = "h1"
}
@HtmlDsl
class P : Element() {
override val tag = "p"
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
fun main() {
val page = html {
body {
h1 { +"Kotlin 제네릭" }
p { +"강력하고 타입 안전합니다" }
}
}
println(page)
}
마무리 - 다음 편 예고
오늘 배운 것 ✅
- 제네릭 기본 - 타입 파라미터
- 타입 제약 - where, upper bound
- 공변성 out - 생산자 (PECS의 Producer)
- 반공변성 in - 소비자 (PECS의 Consumer)
- reified - 타입 소거 해결
- 스타 프로젝션 - 알 수 없는 타입
다음 편에서 배울 것 📚
11편: 상속과 인터페이스 완벽 가이드 | open, abstract, sealed
- 클래스 상속 - open, override
- 추상 클래스 - abstract
- 인터페이스와 다중 상속
- Sealed Class - 제한된 클래스 계층
- Delegation - by 키워드
- 실전 디자인 패턴
실습 과제 💪
// 1. 제네릭 클래스 만들기
// - Stack<T> 구현
// - Queue<T> 구현
// 2. 공변성/반공변성 활용
// - Repository<out T> 패턴
// - Validator<in T> 패턴
// 3. reified 활용
// - 타입 안전한 JSON 파서
// - 제네릭 Factory 패턴
// 4. 실전 패턴
// - Result 타입 구현
// - Either 타입 구현
자주 묻는 질문 (FAQ)
Q: out과 in은 언제 사용하나요?
A: PECS 원칙 - Producer는 out, Consumer는 in. 읽기만 하면 out, 쓰기만 하면 in.
Q: reified는 왜 inline 함수에서만 되나요?
A: 인라인 함수는 호출 지점에 코드가 복사되므로 타입 정보가 유지됩니다.
Q: List<out T>와 List<*>의 차이는?
A: List<out T>는 T의 상위 타입, List<*>는 알 수 없는 타입입니다.
Q: 제네릭과 성능 관계는?
A: 컴파일 시 타입 소거되므로 런타임 성능 영향 없습니다. reified는 인라인이므로 약간의 코드 증가 가능.
관련 글
- Kotlin 확장 함수와 프로퍼티 완벽 가이드 (이전 편)
- Kotlin 상속과 인터페이스 완벽 가이드 (다음 편)
- Kotlin 제네릭 고급 패턴
💬 댓글로 알려주세요!
- 제네릭을 어디에 활용하고 계신가요?
- out/in이 헷갈리셨나요?
- 이 글이 도움이 되셨나요?
반응형
'개발 > java basic' 카테고리의 다른 글
| Kotlin 람다와 고차 함수 완벽 가이드 | 함수형 프로그래밍 심화 (0) | 2025.12.26 |
|---|---|
| Kotlin 컬렉션 완벽 가이드 | List, Set, Map 함수형 프로그래밍 (1) | 2025.12.24 |
| Kotlin Null Safety 완벽 가이드 | ?, ?., ?:, !! 마스터하기 (0) | 2025.12.23 |
| Kotlin 변수와 타입 완벽 가이드 | val vs var, 타입 추론, Nullable 타입 마스터하기 (0) | 2025.12.22 |
| Kotlin 클래스와 객체 완벽 가이드 | OOP의 Kotlin 스타일 (0) | 2025.12.22 |
Comments
