🎁 제네릭
📖 제네릭이란?
**제네릭(Generic)**은 타입을 파라미터로 받아 재사용 가능한 코드를 작성하는 기능입니다. 타입 안전성을 유지하면서 코드를 일반화합니다.
💡 기본 제네릭
제네릭 클래스
// 타입 파라미터 T
class Box<T>(var value: T) {
fun get(): T = value
fun set(newValue: T) {
value = newValue
}
}
fun main() {
val intBox = Box(123)
println(intBox.get()) // 123
val stringBox = Box("Hello")
println(stringBox.get()) // Hello
// 타입 안전
// intBox.set("world") // ❌ 오류!
}
제네릭 함수
fun <T> printItem(item: T) {
println("아이템: $item")
}
fun <T> swap(a: T, b: T): Pair<T, T> {
return Pair(b, a)
}
fun main() {
printItem(42) // 아이템: 42
printItem("Hello") // 아이템: Hello
val (x, y) = swap(1, 2)
println("$x, $y") // 2, 1
}
🔧 실전 활용
스택
class Stack<T> {
private val items = mutableListOf<T>()
fun push(item: T) {
items.add(item)
}
fun pop(): T? {
return if (items.isNotEmpty()) {
items.removeAt(items.size - 1)
} else {
null
}
}
fun peek(): T? = items.lastOrNull()
fun isEmpty(): Boolean = items.isEmpty()
fun size(): Int = items.size
}
fun main() {
val stack = Stack<Int>()
stack.push(1)
stack.push(2)
stack.push(3)
println(stack.pop()) // 3
println(stack.pop()) // 2
println(stack.peek()) // 1
}
API 응답
data class Response<T>(
val success: Boolean,
val data: T?,
val message: String = ""
)
data class User(val id: Int, val name: String)
data class Product(val id: String, val name: String, val price: Int)
fun main() {
// 성공 응답
val userResponse = Response(
success = true,
data = User(1, "홍길동")
)
println(userResponse)
// 실패 응답
val errorResponse = Response<Product>(
success = false,
data = null,
message = "상품을 찾을 수 없습니다"
)
println(errorResponse)
}
리스트 유틸
fun <T> List<T>.second(): T? {
return if (size >= 2) this[1] else null
}
fun <T> List<T>.swap(i: Int, j: Int): List<T> {
val result = toMutableList()
val temp = result[i]
result[i] = result[j]
result[j] = temp
return result
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
println(numbers.second()) // 2
println(numbers.swap(0, 4)) // [5, 2, 3, 4, 1]
}
🎯 제약 조건
상한 제약
// T는 Number의 하위 타입이어야 함
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
fun main() {
println(sum(1, 2)) // 3.0
println(sum(1.5, 2.5)) // 4.0
// println(sum("1", "2")) // ❌ 오류!
}
여러 제약
interface Named {
val name: String
}
fun <T> printName(item: T) where T : Named, T : Comparable<T> {
println(item.name)
}
🎨 공변성과 반공변성
out (공변성)
생산자 - 값을 내보내기만 함
class Producer<out T>(private val value: T) {
fun produce(): T = value
// fun consume(value: T) { } // ❌ 불가능
}
fun main() {
val stringProducer: Producer<String> = Producer("Hello")
val anyProducer: Producer<Any> = stringProducer // ✅ 가능
println(anyProducer.produce())
}
in (반공변성)
소비자 - 값을 받기만 함
class Consumer<in T> {
fun consume(value: T) {
println("소비: $value")
}
// fun produce(): T { } // ❌ 불가능
}
fun main() {
val anyConsumer: Consumer<Any> = Consumer()
val stringConsumer: Consumer<String> = anyConsumer // ✅ 가능
stringConsumer.consume("Hello")
}
🎯 실전 예제
저장소 패턴
interface Repository<T> {
fun save(item: T)
fun findById(id: Int): T?
fun findAll(): List<T>
}
data class User(val id: Int, val name: String)
class UserRepository : Repository<User> {
private val users = mutableMapOf<Int, User>()
override fun save(item: User) {
users[item.id] = item
println("${item.name} 저장됨")
}
override fun findById(id: Int): User? {
return users[id]
}
override fun findAll(): List<User> {
return users.values.toList()
}
}
fun main() {
val repo = UserRepository()
repo.save(User(1, "홍길동"))
repo.save(User(2, "김철수"))
val user = repo.findById(1)
println(user) // User(id=1, name=홍길동)
val allUsers = repo.findAll()
println(allUsers)
}
결과 타입
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
}
fun divide(a: Int, b: Int): Result<Int> {
return if (b != 0) {
Result.Success(a / b)
} else {
Result.Error("0으로 나눌 수 없습니다")
}
}
fun main() {
val result1 = divide(10, 2)
val result2 = divide(10, 0)
when (result1) {
is Result.Success -> println("결과: ${result1.data}")
is Result.Error -> println("오류: ${result1.message}")
}
when (result2) {
is Result.Success -> println("결과: ${result2.data}")
is Result.Error -> println("오류: ${result2.message}")
}
}
캐시
class Cache<K, V> {
private val cache = mutableMapOf<K, V>()
fun put(key: K, value: V) {
cache[key] = value
}
fun get(key: K): V? {
return cache[key]
}
fun contains(key: K): Boolean {
return cache.containsKey(key)
}
fun clear() {
cache.clear()
}
fun size(): Int = cache.size
}
fun main() {
val cache = Cache<String, User>()
cache.put("user1", User(1, "홍길동"))
cache.put("user2", User(2, "김철수"))
val user = cache.get("user1")
println(user) // User(id=1, name=홍길동)
println("크기: ${cache.size()}") // 2
}
🔍 리파이드 타입
inline과 reified
// 타입 정보 유지
inline fun <reified T> isInstance(value: Any): Boolean {
return value is T
}
inline fun <reified T> filterByType(list: List<Any>): List<T> {
return list.filterIsInstance<T>()
}
fun main() {
println(isInstance<String>("Hello")) // true
println(isInstance<Int>("Hello")) // false
val mixed = listOf(1, "two", 3, "four", 5)
val strings = filterByType<String>(mixed)
println(strings) // [two, four]
}
🤔 자주 묻는 질문
Q1. 언제 제네릭을 쓰나요?
A: 여러 타입에서 동작하는 코드를 만들 때!
// ❌ 타입마다 중복
class IntBox(var value: Int)
class StringBox(var value: String)
// ✅ 제네릭으로 하나로
class Box<T>(var value: T)