본문 바로가기
language/Kotilin In Action

5주차(11~12장)

by abstract.jiin 2025. 9. 1.

11장 제네릭스

타입을 나중에 지정할 수 있게 해주는 문법

코드 재사용 가능(다양한 타입에도 동작함), 타입 안정성 확보(엉뚱한 타입 못넣음)

List<String>은 문자열만 담는 리스트
List<Int>는 숫자만 담는 리스트

// 일반 - String만 담을 수 있음
val stringBox: List<String> = listOf("사과", "바나나")

// 제네릭 - 어떤 타입이든 담을 수 있음
fun <T> makeBox(): List<T> = listOf()

11.1 타입 인자를 받는 타입 만들기: 제네릭 타입 파라미터

11.1 타입 인자를 받는 타입 만들기

기본 개념

*// 이렇게 쓰는 것들이 제네릭!*
val numbers: List<Int> = listOf(1, 2, 3)
val names: List<String> = listOf("철수", "영희")
val pairs: Map<String, Int> = mapOf("나이" to 25)

타입 추론 - 컴파일러가 알아서

*// 타입 명시 안 해도 됨*
val authors = listOf("김철수", "박영희")  
*// List<String>으로 추론
// 빈 리스트는 타입 명시 필요*

val readers: MutableList<String> = mutableListOf()
val readers2 = mutableListOf<String>()  *// 이것도 가능*

11.1.1 제네릭 함수와 프로퍼티

제네릭 함수 만들기

*// 어떤 타입의 리스트든 자를 수 있는 함수*
fun <T> List<T>.slice(indices: IntRange): List<T> {
    *// 구현...*
}

*// 사용*
val letters = ('a'..'z').toList()
println(letters.slice(0..2))  *// [a, b, c]*

제네릭 확장 프로퍼티

*// 리스트의 끝에서 두 번째 원소*
val <T> List<T>.penultimate: T
    get() = this[size - 2]

*// 사용*
println(listOf(1, 2, 3, 4).penultimate)  *// 3*

11.1.2 제네릭 클래스 선언

인터페이스 정의

interface List<T> {
    operator fun get(index: Int): T
    *// ...*
}

클래스 상속과 구현

*// 구체적인 타입으로 구현*
class StringList : List<String> {
    override fun get(index: Int): String = TODO()
}

*// 제네릭 타입으로 구현*
class ArrayList<T> : List<T> {
    override fun get(index: Int): T = TODO()
}

자기 참조 제네릭

interface Comparable<T> {
    fun compareTo(other: T): Int
}

class String : Comparable<String> {
    override fun compareTo(other: String): Int = TODO()
}

11.1.3 타입 파라미터 제약

숫자만 허용하는 함수

fun <T : Number> List<T>.sum(): T {
    *// Number의 하위 타입만 가능*
}

*// 사용*
println(listOf(1, 2, 3).sum())      *// Int는 Number의 하위 타입*
println(listOf(1.5, 2.5).sum())    *// Double도 Number의 하위 타입
// println(listOf("a", "b").sum()) 
// String은 불가능*

비교 가능한 타입만 허용

fun <T : Comparable<T>> max(first: T, second: T): T {
    return if (first > second) first else second
}

*// 사용*
println(max("kotlin", "java"))  *// "kotlin"*
println(max(10, 20))           *// 20
// println(max("kotlin", 42))  // 타입이 다름*

여러 제약 조건

fun <T> ensureTrailingPeriod(seq: T)
    where T : CharSequence,    *// 문자열처럼 읽을 수 있고*
          T : Appendable {     *// 문자를 추가할 수 있어야 함*
    if (!seq.endsWith('.')) {
        seq.append('.')
    }
}

*// StringBuilder는 두 조건 모두 만족*
val sb = StringBuilder("Hello")
ensureTrailingPeriod(sb)
println(sb)  *// Hello.*

11.1.4 널이 될 수 없는 타입 제약

문제 상황

class Processor<T> {
    fun process(value: T) {
        value?.hashCode()  *// null 체크 필요* 
    }
}

val processor = Processor<String?>()
processor.process(null)  *// null 전달 가능*

해결책 - Any 상계 사용

class Processor<T : Any> {  *// T는 널이 될 수 없음*
    fun process(value: T) {
        value.hashCode()    *// null 체크 불필요* 
    }
}

*// val processor = Processor<String?>()  // 컴파일 에러*
val processor = Processor<String>()      *// 가능*

예시

1. 안전한 타입 변환

fun <T> safeCast(value: Any, clazz: Class<T>): T? {
    return if (clazz.isInstance(value)) {
        clazz.cast(value)
    } else {
        null
    }
}

2. 제네릭 결과 클래스

sealed class Result<T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error<T>(val message: String) : Result<T>()
}

fun fetchUser(id: Int): Result<User> {
    *// 네트워크 요청...*
    return if (success) {
        Result.Success(user)
    } else {
        Result.Error("사용자를 찾을 수 없습니다")
    }
}

3. 제네릭 확장 함수

fun <T> List<T>.second(): T = this[1]
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null

val numbers = listOf(1, 2, 3)
println(numbers.second())        *// 2*
println(numbers.secondOrNull())  *// 2*

제네릭스의 장점

  1. 타입 안전성: 컴파일 시점에 타입 체크
  2. 재사용성: 하나의 코드로 여러 타입 지원
  3. 성능: 캐스팅 불필요

주요 문법

*// 클래스*
class Box<T>(val item: T)

*// 함수*  
fun <T> identity(value: T): T = value

*// 제약 - Number만 허용*  
fun <T : Number> sum(items: List<T>): T

*// 널 방지*
fun <T : Any> process(value: T)

코틀린 vs 자바 차이점

  • 로 타입 없음: 코틀린은 항상 타입 인자 필요
  • 널 안전성: Any vs Any? 구분
  • 타입 추론: 더 똑똑한 추론

쉽게 기억하기:

  • <T> = "T라는 타입이 있다고 가정하자"
  • T : Number = "T는 숫자여야 한다"
  • T : Any = "T는 null이면 안 된다"

11.2 실행 시점 제네릭스 동작


11.2.1 타입 소거의 한계

문제: 실행 시점에 타입 정보가 사라짐

val list1: List<String> = listOf("a", "b")
val list2: List<Int> = listOf(1, 2, 3)

*// 실행 시점에는 둘 다 그냥 List*

이런 코드는 NO…

fun printList(list: List<Any>) {
    when (list) {
        is List<String> -> println("문자열 리스트")  *// 컴파일 에러*
        is List<Int> -> println("정수 리스트")      *// 컴파일 에러*
    }
}

에러: Cannot check for an instance of erased type

해결책 1: 스타 프로젝션 사용

fun printList(list: List<Any>) {
    if (list is List<*>) {  *// 가능*
        println("이건 리스트입니다 (원소 타입은 모름)")
    }
}

해결책 2: 컴파일 시점에 타입이 알려진 경우

fun printSum(c: Collection<Int>) {  *// Int가 이미 확정됨*
    when (c) {
        is List<Int> -> println("리스트 합: ${c.sum()}")  *// 가능*
        is Set<Int> -> println("집합 합: ${c.sum()}")    *// 가능*
    }
}

위험한 캐스팅

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int>  *//  unchecked cast 경고*
        ?: throw IllegalArgumentException("리스트가 아님")
    println(intList.sum())
}

fun main() {
    printSum(listOf(1, 2, 3))        *// 6* 
    printSum(setOf(1, 2, 3))         *// IllegalArgumentException*
    printSum(listOf("a", "b", "c"))  *// ClassCastException (나중에 터짐)*
}

문제: 문자열 리스트도 List<Int>로 캐스팅이 성공하지만, sum() 호출할 때 터져


11.2.2 실체화된 타입 파라미터

타입 소거에 대해서 …

컴파일 시점 vs 실행 시점의 차이

*// 자바 코드 - 컴파일 시점*
List<String> stringList = new ArrayList<String>();
List<Integer> intList = new ArrayList<Integer>();

*// 실행 시점에는 둘 다 그냥 List
// 타입 정보 <String>, <Integer>가 사라짐* 

왜 이렇게 했을까?

답: 하위 호환성 때문

*// 자바 1.4 (제네릭 도입 전)*
List oldList = new ArrayList();
oldList.add("hello");
oldList.add(123);

*// 자바 1.5+ (제네릭 도입 후)*
List<String> newList = new ArrayList<String>();
*// 하지만 실행 시점에는 oldList와 newList가 동일한 바이트코드*

타입 소거의 실제 동작

1. 컴파일러가 하는 일

*// 우리가 쓴 코드*
List<String> list = new ArrayList<String>();
list.add("hello");
String item = list.get(0);

*// 컴파일 후 실제 바이트코드 (의사코드)*
List list = new ArrayList();
list.add("hello");
String item = (String) list.get(0);  *// 자동 캐스팅 삽입!*

2. 실행 시점에서 일어나는 일

public class TypeErasureDemo {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();

        *// 실행 시점에는 둘 다 같은 클래스!*
        System.out.println(stringList.getClass());  *// class java.util.ArrayList*
        System.out.println(intList.getClass());     *// class java.util.ArrayList*
        System.out.println(stringList.getClass() == intList.getClass());  *// true!*
    }
}

타입 소거로 인한 문제들

1. instanceof 검사 불가능

*// 컴파일 에러*
List<String> stringList = new ArrayList<>();
if (stringList instanceof List<String>) {  *// 불가능!// ...*
}

*// 이것만 가능*
if (stringList instanceof List) {  *// Raw 타입으로만 가능// ...*
}

2. 배열 생성 불가능

*// 컴파일 에러*
List<String>[] array = new List<String>[10];  *// 불가능!
// 우회 방법*
List<String>[] array = new List[10];  *// Raw 타입으로 생성*

3. 메서드 오버로딩 제한

*// 컴파일 에러*
public class Problem {
    public void method(List<String> list) { }
    public void method(List<Integer> list) { }  *// 불가능*
}

자바에서 타입 정보 유지하는 방법들

1. Class 객체 전달하기

public class GenericService<T> {
    private Class<T> type;

    public GenericService(Class<T> type) {
        this.type = type;
    }

    public T createInstance() throws Exception {
        return type.newInstance();  *// 타입 정보 활용!*
    }
}

*// 사용*
GenericService<String> service = new GenericService<>(String.class);

2. TypeToken 패턴 (Gson, Guava 등에서 사용)

*// Gson 라이브러리 예시*
Type listType = new TypeToken<List<String>>(){}.getType();
List<String> list = gson.fromJson(json, listType);

코틀린에서도 동일한 문제

타입 소거로 인한 제약

fun printList(list: List<Any>) {
    when (list) {
        is List<String> -> println("문자열 리스트")  *// 컴파일 에러*
        is List<Int> -> println("정수 리스트")      *// 컴파일 에러*
    }
}

스타 프로젝션으로 우회

fun printList(list: List<Any>) {
    when (list) {
        is List<*> -> println("리스트입니다 (타입은 모름)")  *// 가능*
    }
}

코틀린의 해결책: reified (발음 : 리어파이)

문제 상황

*// 이런 함수를 만들고 싶지만...*
fun <T> isInstance(value: Any): Boolean {
    return value is T  *//  컴파일 에러. T가 실행 시점에 사라짐*
}

해결책: inline + reified

inline fun <reified T> isInstance(value: Any): Boolean {
    return value is T  *// 가능 reified로 타입 정보 유지*
}

fun main() {
    println(isInstance<String>("hello"))  *// true*
    println(isInstance<String>(123))      *// false*
    println(isInstance<Int>(123))         *// true*
}

실제 활용: filterIsInstance

fun main() {
    val items = listOf("one", 2, "three", 4.5)

    *// 문자열만 골라내기*
    val strings = items.filterIsInstance<String>()
    println(strings)  *// [one, three]*

    *// 숫자만 골라내기*  
    val numbers = items.filterIsInstance<Number>()
    println(numbers)  *// [2, 4.5]*
}

filterIsInstance 구현

inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    val result = mutableListOf<T>()
    for (element in this) {
        if (element is T) {  *// reified 덕분에 가능*
            result.add(element)
        }
    }
    return result
}

11.2.3 클래스 참조 대신하기

Java API를 쉽게 사용하기

기존 방식 (번거로움):

val serviceImpl = ServiceLoader.load(Service::class.java)

개선된 방식 (깔끔함):

inline fun <reified T> loadService(): T {
    return ServiceLoader.load(T::class.java)
}

*// 사용*
val serviceImpl = loadService<Service>()

안드로이드 액티비티 시작하기

*// 기존 방식*
val intent = Intent(this, DetailActivity::class.java)
startActivity(intent)

*// 개선된 방식*
inline fun <reified T : Activity> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

*// 사용*
startActivity<DetailActivity>()  *// 훨씬 간단!*

11.2.4 실체화된 프로퍼티 접근자

inline val <reified T> T.canonical: String
    get() = T::class.java.canonicalName

fun main() {
    println(listOf(1, 2, 3).canonical)  *// java.util.List*
    println(1.canonical)                *// java.lang.Integer*
    println("hello".canonical)          *// java.lang.String*
}

11.2.5 실체화된 타입 파라미터의 제약

할 수 있는 것들

inline fun <reified T> example(value: Any) {
    *// 타입 검사와 캐스팅*
    if (value is T) { ... }
    val casted = value as? T

    *// 클래스 참조 얻기*
    val clazz = T::class
    val javaClass = T::class.java

    *// 다른 reified 함수 호출*
    val filtered = listOf(value).filterIsInstance<T>()
}

할 수 없는 것들

inline fun <reified T> cannotDo() {
    *// val instance = T()  //  인스턴스 생성 불가
    // T.someStaticMethod()  // 동반 객체 메서드 호출 불가*
}

*// 일반 함수에서는 reified 사용 불가*
fun <reified T> normalFunction() { ... }  *// 컴파일 에러*

예시

1. 안전한 타입 변환

inline fun <reified T> safeCast(value: Any?): T? {
    return value as? T
}

val str: String? = safeCast<String>("hello")  *// "hello"*
val num: Int? = safeCast<Int>("hello")        *// null*

2. JSON 파싱 (Gson 스타일)

inline fun <reified T> parseJson(json: String): T {
    return Gson().fromJson(json, T::class.java)
}

val user: User = parseJson<User>(jsonString)
val users: List<User> = parseJson<List<User>>(jsonArrayString)

3. 의존성 주입

inline fun <reified T> inject(): T {
    return serviceLocator.get(T::class.java)
}

val userService: UserService = inject<UserService>()

핵심 정리

타입 소거의 문제

  • 실행 시점에 List<String>List<Int>는 구분 불가
  • is List<String> 같은 체크 불가능
  • 안전하지 않은 캐스팅만 가능

reified의 마법

*// 일반 함수 - 타입 정보 사라짐*
fun <T> normal(value: Any) = value is T  *// X
// 인라인 + reified - 타입 정보 유지*
inline fun <reified T> magic(value: Any) = value is T  *// O*

왜 인라인에서만 가능할까?

  1. 인라인 함수: 호출 지점에 코드가 복사됨
  2. 타입 정보 보존: 복사할 때 구체적인 타입으로 치환
  3. 결과: 마치 직접 value is String 같은 코드를 쓴 것처럼!
  • reified = "실체화된" = "실행 시점에도 타입 정보 살아있음"
  • inline fun <reified T> = 타입 소거의 한계를 없애줌

11.3 변성 - 제네릭과 타입 인자 사이의 하위 타입 관계


11.3.1 변성

변성 = "제네릭 타입들 사이의 관계를 정하는 규칙"

*// 이런 질문들에 대한 답을 변성이 정함*
List<String>을 List<Any>가 필요한 곳에 전달해도 될까? 
MutableList<String>을 MutableList<Any>에 넣어도 안전할까? 

안전한 경우 vs 위험한 경우

안전한 경우 (읽기 전용):

fun printContents(list: List<Any>) {
    println(list.joinToString())  *// 읽기만 함*
}

fun main() {
    printContents(listOf("abc", "bac"))  *// 안전!
    // String은 Any의 하위 타입이므로 문제없음*
}

위험한 경우 (변경 가능):

fun addAnswer(list: MutableList<Any>) {
    list.add(42)  *// 정수를 추가!*
}

fun main() {
    val strings = mutableListOf("abc", "bac")
    *// addAnswer(strings)  // 컴파일 에러
    // 문자열 리스트에 정수가 들어가면 안됨*
}

11.3.2 타입과 하위 타입

하위 타입의 정의

A 타입이 필요한 모든 곳에 B 타입을 넣어도 문제없다면, B는 A의 하위 타입

val n: Number = 42        *// Int는 Number의 하위 타입*
val s: String = 42        *// Int는 String의 하위 타입 아님*

val nullable: String? = "hello"  *// String은 String?의 하위 타입*
val nonNull: String = null       *// String?은 String의 하위 타입 아님*

11.3.3 공변성 (Covariance) - 관계 유지

공변성이란?

하위 타입 관계가 그대로 유지되는 것

*// out 키워드로 공변성 선언
//* 왜 "out"이라고 할까?
*//* **타입 T가 "밖으로 나오기만" 하기 때문!**
interface Producer<out T> {
    fun produce(): T  *// T를 반환 (out 위치)
    // fun consume(t: T)  // T를 받으면 안됨 (in 위치)*
}

*// Cat이 Animal의 하위 타입이면
// Producer<Cat>도 Producer<Animal>의 하위 타입!*

예시: 동물 무리

open class Animal {
    fun eat() = println("냠냠")
}

class Cat : Animal() {
    fun meow() = println("야옹")
}

class Dog : Animal() {
    fun bark() = println("멍멍")
}

// 무공변 클래스 (문제 있음)
class Farm<T : Animal> {
    private val animals = mutableListOf<T>()

    fun addAnimal(animal: T) { animals.add(animal) }
    fun getAnimal(index: Int): T = animals[index]
    fun getAllAnimals(): List<T> = animals
}

fun feedAllAnimals(farm: Farm<Animal>) {
    // 모든 동물에게 먹이 주기
    farm.getAllAnimals().forEach { it.eat() }
}

fun main() {
    val catFarm = Farm<Cat>()
    catFarm.addAnimal(Cat())

    // feedAllAnimals(catFarm)  // 컴파일 에러
    // Farm<Cat>을 Farm<Animal>에 전달할 수 없음
}

해결책: 공변성 사용

// 읽기 전용 농장 (공변성 적용)
class ReadOnlyFarm<out T : Animal> {  // out 키워드!
    private val animals = mutableListOf<T>()

    // fun addAnimal(animal: T) { }  // in 위치라서 불가능
    fun getAnimal(index: Int): T = animals[index]        // out 위치
    fun getAllAnimals(): List<T> = animals               // out 위치
    fun getCount(): Int = animals.size                   // T 사용 안함
}

fun feedAllAnimals(farm: ReadOnlyFarm<Animal>) {
    farm.getAllAnimals().forEach { it.eat() }
}

fun main() {
    val catFarm = ReadOnlyFarm<Cat>()
    feedAllAnimals(catFarm)  // 이제 가능!
    // ReadOnlyFarm<Cat>이 ReadOnlyFarm<Animal>의 하위 타입
}

List의 공변성 (공변적)

interface List<out E> : Collection<E> {
    operator fun get(index: Int): E     // out 위치
    fun size(): Int                     // E 사용 안함
    fun subList(fromIndex: Int, toIndex: Int): List<E>  // out 위치
    // fun add(element: E)              //  없음! in 위치라서
}

MutableList는 공변적이지 않음

//실제 MutableList 정의

interface MutableList<E> : List<E> {
    // List에서 상속받은 것들 (out 위치)
    override fun get(index: Int): E

    // 새로 추가된 것들 (in 위치)
    fun add(element: E): Boolean        // in 위치!
    fun set(index: Int, element: E): E  // in 위치!
    fun remove(element: E): Boolean     //  in 위치!
}

// 따라서 MutableList<T>는 무공변

11.3.4 반공변성 (Contravariance) - 관계 뒤집기

반공변성이란?

하위 타입 관계가 뒤집히는 것

*// in 키워드로 반공변성 선언*
interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int  *// T를 "소비"만 함 (인 위치)*
}

*// Cat이 Animal의 하위 타입이면
// Comparator<Animal>이 Comparator<Cat>의 하위 타입!*

예시: 과일 비교

sealed class Fruit {
    abstract val weight: Int
}

data class Apple(override val weight: Int, val color: String) : Fruit()
data class Orange(override val weight: Int, val juicy: Boolean) : Fruit()

fun main() {
    *// 과일 전체를 비교*
    val fruitComparator = Comparator<Fruit> { a, b -> 
        a.weight - b.weight 
    }

    val apples = listOf(Apple(100, "red"), Apple(150, "green"))

    *// Fruit 비교기로 Apple도 비교 가능 (반공변성)*
    println(apples.sortedWith(fruitComparator))
    *// Comparator<Fruit>이 Comparator<Apple>처럼 사용됨*
}

11.3.5 사용 지점 변성

함수에서 일시적으로 변성 지정

*// 데이터 복사 함수*
fun <T> copyData(
    source: MutableList<out T>,    *// 읽기만 함 (아웃 프로젝션)*
    destination: MutableList<in T>  *// 쓰기만 함 (인 프로젝션)*
) {
    for (item in source) {
        destination.add(item)
    }
}

fun main() {
    val strings = mutableListOf("a", "b", "c")
    val objects = mutableListOf<Any>()

    copyData(strings, objects)  *// String → Any 복사 가능!*
    println(objects)  *// [a, b, c]*
}

11.3.6 스타 프로젝션

타입을 모를 때: 사용

fun printFirst(list: List<*>) {  *// 어떤 타입이든 OK*
    if (list.isNotEmpty()) {
        println(list.first())  *// Any? 타입으로 반환*
    }
}

fun main() {
    printFirst(listOf("a", "b", "c"))    *// 문자열 리스트*
    printFirst(listOf(1, 2, 3))         *// 정수 리스트*
    printFirst(listOf(true, false))     *// 불린 리스트*
}

주의사항

val unknownList: MutableList<*> = mutableListOf("a", "b")
println(unknownList.first())  *// 읽기는 가능 (Any? 반환)
// unknownList.add("c")       // 쓰기는 불가능!*

11.3.7 타입 별명 - 긴 타입 이름 줄이기

*// 복잡한 함수 타입을 간단한 이름으로*
typealias NameCombiner = (String, String, String, String) -> String

val authorsCombiner: NameCombiner = { a, b, c, d -> "$a et al." }
val bandCombiner: NameCombiner = { a, b, c, d -> "$a, $b & The Gang" }

fun combineAuthors(combiner: NameCombiner) {
    println(combiner("김철수", "박영희", "이민수", "최지영"))
}

fun main() {
    combineAuthors(authorsCombiner)  *// 김철수 et al.*
    combineAuthors(bandCombiner)     *// 김철수, 박영희 & The Gang*
}

언제 뭘 사용할까?

1. 데이터를 생산만 하는 경우 → out (공변성)

interface Producer<out T> {
    fun produce(): T        *// 반환만 함
    // fun consume(t: T)    // 파라미터로 받으면 안됨*
}

2. 데이터를 소비만 하는 경우 → in (반공변성)

interface Consumer<in T> {
    fun consume(t: T)       *// 파라미터로만 받음
    // fun produce(): T     // 반환하면 안됨*
}

3. 둘 다 하는 경우 → 변성 없음 (무공변성)

interface MutableList<T> {
    fun get(index: Int): T      *// 생산*
    fun add(element: T)         *// 소비*
}
  • 생산자는 out: "밖으로 내보냄"
  • 소비자는 in: "안으로 받아들임"
  • PECS: Producer-Extends(out), Consumer-Super(in)

12장 어노테이션과 리플렉션

12장 어노테이션과 리플렉션


12.1 어노테이션 선언과 적용

어노테이션이 뭔가요?

어노테이션 = "코드에 붙이는 메타데이터 태그"

*// 이런 @Test가 어노테이션!*
@Test
fun testMethod() {
    assertTrue(1 + 1 == 2)
}

12.1.1 어노테이션 적용하기

기본 사용법

import kotlin.test.*

class MyTest {
    @Test  *// 이 메서드가 테스트임을 표시*
    fun testTrue() {
        assertTrue(1 + 1 == 2)
    }
}

@Deprecated 어노테이션

@Deprecated(
    "Use removeAt(index) instead.",
    ReplaceWith("removeAt(index)")
)
fun remove(index: Int) { 
    *// 구식 구현*
}

fun main() {
    remove(1)  *// IDE가 경고하고 자동 교체 제안!*
}

어노테이션 인자 규칙

*// 가능한 인자 타입들*
@MyAnnotation(
    value = 42,                        *// 기본 타입*
    text = "Hello",                    *// 문자열*
    status = Status.ACTIVE,            *// 이넘*
    clazz = String::class,             *// 클래스 참조 (::class 필요!)*
    other = SomeAnnotation("data"),    *// 다른 어노테이션 (@ 없이!)*
    array = ["a", "b", "c"]           *// 배열 ([] 사용)*
)

12.1.2 어노테이션 타깃 지정

문제: 코틀린 프로퍼티는 여러 자바 요소가 됨

class User {
    var name: String = ""
    *// 자바로 컴파일되면:
    // - private String name 필드
    // - public String getName() 게터
    // - public void setName(String) 세터
    // 어디에 어노테이션을 붙일까?* 
}

해결책: 사용 지점 타깃

class CertificateManager {
    @get:JvmName("obtainCertificate")  *// 게터에만 적용*
    @set:JvmName("putCertificate")     *// 세터에만 적용*
    var certificate: String = "-----BEGIN PRIVATE KEY-----"
}

*// 자바에서 사용
// cert = manager.obtainCertificate();
// manager.putCertificate("new cert");*

타깃 종류들

class Example {
    @property:MyAnnotation    *// 프로퍼티 전체*
    @field:MyAnnotation      *// 백킹 필드*
    @get:MyAnnotation        *// 게터*
    @set:MyAnnotation        *// 세터*
    @param:MyAnnotation      *// 생성자 파라미터*
    var value: String = ""
}

@file:JvmName("Utils")       *// 파일 레벨 (package 선언 위에)*

12.1.3 JSON 직렬화 제어

제이키드 라이브러리 소개

data class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Alice", 29)

    *// 직렬화: 객체 → JSON*
    println(serialize(person))
    *// {"age": 29, "name": "Alice"}*

    *// 역직렬화: JSON → 객체*
    val json = """{"name": "Alice", "age": 29}"""
    println(deserialize<Person>(json))
    *// Person(name=Alice, age=29)*
}

어노테이션으로 제어하기

data class Person(
    @JsonName("alias") val firstName: String,  *// JSON에서 "alias" 키 사용*
    @JsonExclude val age: Int? = null          *// 직렬화에서 제외*
)

fun main() {
    val person = Person("Alice")
    println(serialize(person))
    *// {"alias": "Alice"}  // age는 없고, firstName은 alias로!*
}

12.1.4 어노테이션 선언하기

가장 간단한 어노테이션

annotation class JsonExclude  *// class 앞에 annotation!*

파라미터가 있는 어노테이션

annotation class JsonName(val name: String)  *// 모든 파라미터는 val!// 사용*
@JsonName("firstName")
val name: String = ""

자바 vs 코틀린 비교

*// 자바*
public @interface JsonName {
    String value();  *// 특별한 이름*
}

*// 사용: @JsonName("firstName") 또는 @JsonName(value = "firstName")*
*// 코틀린*
annotation class JsonName(val name: String)

*// 사용: @JsonName("firstName") 또는 @JsonName(name = "firstName")*

12.1.5 메타어노테이션

@Target으로 적용 대상 제한

@Target(AnnotationTarget.PROPERTY)  *// 프로퍼티에만 적용 가능*
annotation class JsonExclude

@Target(AnnotationTarget.CLASS, AnnotationTarget.METHOD)  *// 여러 타깃 가능*
annotation class MyAnnotation

주요 타깃들

AnnotationTarget.CLASS          *// 클래스*
AnnotationTarget.FUNCTION       *// 함수*
AnnotationTarget.PROPERTY       *// 프로퍼티*
AnnotationTarget.FIELD          *// 필드*
AnnotationTarget.FILE           *// 파일*
AnnotationTarget.ANNOTATION_CLASS  *// 어노테이션 클래스*

12.1.6 클래스 참조를 파라미터로

인터페이스 구현체 지정하기

interface Company {
    val name: String
}

data class CompanyImpl(override val name: String) : Company

data class Person(
    val name: String,
    @DeserializeInterface(CompanyImpl::class)  *// ::class 필요!*
    val company: Company
)

어노테이션 정의

annotation class DeserializeInterface(
    val targetClass: KClass<out Any>  *// KClass 사용, out으로 공변성*
)

12.1.7 제네릭 클래스 참조

커스텀 직렬화기

interface ValueSerializer<T> {
    fun toJsonValue(value: T): Any?
    fun fromJsonValue(jsonValue: Any?): T
}

*// 날짜 직렬화기 구현*
class DateSerializer : ValueSerializer<Date> {
    override fun toJsonValue(value: Date) = value.time
    override fun fromJsonValue(jsonValue: Any?) = Date(jsonValue as Long)
}

사용하기

data class Person(
    val name: String,
    @CustomSerializer(DateSerializer::class)  *// 커스텀 직렬화기 지정*
    val birthDate: Date
)

어노테이션 정의 (복잡)

annotation class CustomSerializer(
    val serializerClass: KClass<out ValueSerializer<*>>
    *//                    ↑     ↑                    ↑
    //                  KClass  ValueSerializer     스타 프로젝션
    //                          의 하위 타입만       (어떤 타입이든)*
)

예시

1. 간단한 검증 어노테이션

@Target(AnnotationTarget.PROPERTY)
annotation class NotEmpty

@Target(AnnotationTarget.PROPERTY)  
annotation class Range(val min: Int, val max: Int)

data class User(
    @NotEmpty val name: String,
    @Range(min = 0, max = 150) val age: Int
)

2. 컨트롤러 매핑 (스프링 스타일)

@Target(AnnotationTarget.FUNCTION)
annotation class GetMapping(val path: String)

@Target(AnnotationTarget.CLASS)
annotation class RestController

@RestController
class UserController {
    @GetMapping("/users")
    fun getUsers(): List<User> = listOf()
}

3. 테스트 어노테이션

@Target(AnnotationTarget.FUNCTION)
annotation class Test

@Target(AnnotationTarget.FUNCTION)
annotation class Timeout(val seconds: Long)

class MyTest {
    @Test
    @Timeout(5)
    fun slowTest() {
        Thread.sleep(1000)
    }
}

정리

어노테이션 선언 패턴

@Target(AnnotationTarget.적용대상)
@Retention(AnnotationRetention.RUNTIME)  *// 기본값이라 생략 가능*
annotation class MyAnnotation(
    val param1: String,
    val param2: Int = 0,           *// 기본값 가능*
    val clazz: KClass<*> = Any::class  *// 클래스 참조*
)

사용 가이드

  1. 간단한 마커: annotation class MyMarker
  2. 값 하나: annotation class MyValue(val value: String)
  3. 클래스 참조: val clazz: KClass<out SomeInterface>
  4. 제네릭 클래스: val clazz: KClass<out Generic<*>>

어노테이션의 활용

  • 메타데이터 저장: 설정, 매핑 정보
  • 컴파일 시점 검증: 린트, 정적 분석
  • 런타임 처리: 리플렉션과 함께 사용
  • 코드 생성: 어노테이션 프로세싱

12.2 리플렉션: 실행 시점에 코틀린 객체 내부 관찰


12.2.1 코틀린 리플렉션 API 기본

리플렉션?

리플렉션 = "실행 시점에 객체의 내부를 들여다보는 기술"

*// 컴파일 시점에 알고 있는 방법*
val person = Person("Alice", 29)
println(person.name)  *// 직접 접근
// 리플렉션을 통한 방법 (실행 시점에 동적으로)*
val kClass = person::class
val nameProperty = kClass.memberProperties.find { it.name == "name" }
println(nameProperty?.get(person))  *// 동적 접근*

핵심 인터페이스들

KClass - 클래스 정보

import kotlin.reflect.full.*

class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Alice", 29)
    val kClass = person::class  *// KClass<out Person>*

    println(kClass.simpleName)        *// Person*
    println(kClass.qualifiedName)     *// com.example.Person*

    *// 모든 프로퍼티 나열*
    kClass.memberProperties.forEach { 
        println(it.name) 
    }
    *// age// name*
}

KCallable - 호출 가능한 것들의 공통 인터페이스

interface KCallable<out R> {
    fun call(vararg args: Any?): R  *// 일반적인 호출
    // ...*
}

KFunction - 함수 참조

함수를 변수처럼 다루기

fun foo(x: Int) = println(x)

fun main() {
   // 함수를 변수에 저장
    val kFunction = ::foo  *// KFunction1<Int, Unit>*

    *// 3가지 호출 방법*
    kFunction.call(42)        *// 일반적인 방법(런타임에 체크)*
    kFunction.invoke(42)      *// 타입 안전한 방법(컴파일시점에 체크)*
    kFunction(42)             *// 직접 호출 (가장 간단, 자연스러움)*
}
KFunction0<R>           // 파라미터 0개
KFunction1<P1, R>       // 파라미터 1개  
KFunction2<P1, P2, R>   // 파라미터 2개
KFunction3<P1, P2, P3, R>  // 파라미터 3개
// ... 

KProperty - 프로퍼티 참조

// 최상위 프로퍼티 - 그냥 접근하면 됨 (파라미터 0개)
var counter = 0

fun main() {
    val kProperty = ::counter  *// KMutableProperty0<Int>*

    kProperty.setter.call(21)  *// 세터 호출*
    println(kProperty.get())   *// 21*
}
// 코틀린의 프로퍼티 타입들
KProperty0<T>        // 최상위 프로퍼티 (파라미터 0개)
KProperty1<R, T>     // 클래스 멤버 프로퍼티 (수신객체 1개 필요)
KProperty2<R1, R2, T> // 확장 프로퍼티 등...

// Mutable 버전들
KMutableProperty0<T>     // 변경 가능한 최상위 프로퍼티
KMutableProperty1<R, T>  // 변경 가능한 멤버 프로퍼티
*// 멤버 프로퍼티*
class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Alice", 29)
    val memberProperty = Person::age  *// KProperty1<Person, Int>*

    println(memberProperty.get(person))  *// 29
    // memberProperty.get("Alice")      // 컴파일 에러*
}

12.2.2 직렬화 구현하기

serialize 함수의 기본 구조

fun serialize(obj: Any): String = buildString { 
    serializeObject(obj) 
}

private fun StringBuilder.serializeObject(obj: Any) {
    val kClass = obj::class as KClass<Any>
    val properties = kClass.memberProperties

    properties.joinToStringBuilder(
        this, prefix = "{", postfix = "}"
    ) { prop ->
        serializeString(prop.name)     *// 프로퍼티 이름*
        append(": ")
        serializePropertyValue(prop.get(obj))  *// 프로퍼티 값*
    }
}

실제 동작 예시

어떤 객체든 JSON으로 만들기

data class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Alice", 29)
    println(serialize(person))
    *// {"age": 29, "name": "Alice"}*
}

12.2.3 어노테이션으로 직렬화 제어

@JsonExclude 처리

private fun StringBuilder.serializeObject(obj: Any) {
    (obj::class as KClass<Any>)
        .memberProperties
        .filter { it.findAnnotation<JsonExclude>() == null }  *// 제외*
        .joinToStringBuilder(this, prefix = "{", postfix = "}") { 
            serializeProperty(it, obj)
        }
}

@JsonName 처리

private fun StringBuilder.serializeProperty(
    prop: KProperty1<Any, *>, obj: Any
) {
    val jsonNameAnn = prop.findAnnotation<JsonName>()
    val propName = jsonNameAnn?.name ?: prop.name  *// 어노테이션 우선*

    serializeString(propName)
    append(": ")
    serializePropertyValue(prop.get(obj))
}

예시

data class Person(
    @JsonName("alias") val firstName: String,
    @JsonExclude val age: Int? = null
)

fun main() {
    val person = Person("Alice")
    println(serialize(person))
    *// {"alias": "Alice"}  // age는 제외, firstName은 alias로!*
}

문제

import java.util.Date
import java.time.LocalDateTime

data class Event(
    val name: String,
    val date: Date,                    // Date를 어떻게 JSON으로?
    val createdAt: LocalDateTime,      // LocalDateTime은?
    val tags: Set<String>              // Set을 Array로 바꾸고 싶은데?
)

// 기본 직렬화 결과
// {
//   "name": "생일파티",
//   "date": "Mon Mar 15 14:30:00 KST 2024",  // 읽기 어려운 형태
//   "createdAt": "2024-03-15T14:30:45.123",  // 형식이 다름
//   "tags": "[음악, 파티, 친구들]"           // 문자열로 변환됨
// }

// 원하는 결과
// {
//   "name": "생일파티", 
//   "date": "2024-03-15",              // 깔끔한 날짜
//   "createdAt": 1710482445123,        // 타임스탬프
//   "tags": ["음악", "파티", "친구들"]   // 실제 배열
// }

@CustomSerializer 처리

fun KProperty<*>.getSerializer(): ValueSerializer<Any?>? {
    val customSerializerAnn = findAnnotation<CustomSerializer>()
        ?: return null

    val serializerClass = customSerializerAnn.serializerClass

    *// 싱글턴 객체인지 확인*
    val valueSerializer = serializerClass.objectInstance
        ?: serializerClass.createInstance()  *// 일반 클래스면 인스턴스 생성*

    @Suppress("UNCHECKED_CAST")
    return valueSerializer as ValueSerializer<Any?>
}

ValueSerializer 인터페이스

커스텀 직렬화의 기본 구조

interface ValueSerializer<T> {
    fun toJsonValue(value: T): Any?      *// 객체 → JSON 값*
    fun fromJsonValue(jsonValue: Any?): T *// JSON 값 → 객체 (역직렬화용)*
}

날짜 직렬화 예시

import java.util.Date
import java.text.SimpleDateFormat

*// Date를 "yyyy-MM-dd" 형식으로 변환*
object DateSerializer : ValueSerializer<Date> {
    private val dateFormat = SimpleDateFormat("yyyy-MM-dd")

    override fun toJsonValue(value: Date): String {
        return dateFormat.format(value)
    }

    override fun fromJsonValue(jsonValue: Any?): Date {
        return dateFormat.parse(jsonValue as String)
    }
}

*// 테스트*
fun main() {
    val date = Date()
    val serializer = DateSerializer

    val jsonValue = serializer.toJsonValue(date)
    println("JSON: $jsonValue")  *// JSON: 2024-03-15*

    val backToDate = serializer.fromJsonValue(jsonValue)
    println("Date: $backToDate")  *// Date: Fri Mar 15 00:00:00 KST 2024*
}
@Target(AnnotationTarget.PROPERTY)
annotation class CustomSerializer(
    val serializerClass: KClass<out ValueSerializer<*>>
)

복잡한 타입 분석:

  • KClass<out ValueSerializer<*>>

  • KClass: 클래스 참조 타입

  • out: 공변성 (ValueSerializer의 하위 타입 허용)

  • ValueSerializer<*>: 어떤 타입의 ValueSerializer든 OK

      data class Event(
          val name: String,
          @CustomSerializer(DateSerializer::class) val date: Date,
          @CustomSerializer(TimestampSerializer::class) val createdAt: LocalDateTime
      )

12.2.4 JSON 파싱과 역직렬화

deserialize 함수

inline fun <reified T: Any> deserialize(json: String): T {
    *// reified로 실행 시점에 타입 정보 접근 가능!*
}

3단계 역직렬화 과정

*// 1단계: 렉서 (문자열 → 토큰)
// {"name": "Alice"} → ["{", "name", ":", "Alice", "}"]
// 2단계: 파서 (토큰 → 구조화된 데이터)*
interface JsonObject {
    fun setSimpleProperty(propertyName: String, value: Any?)
    fun createObject(propertyName: String): JsonObject
    fun createArray(propertyName: String): JsonObject
}

*// 3단계: 역직렬화기 (구조화된 데이터 → 객체)*

Seed 패턴 - 객체를 점진적으로 만들기

interface Seed : JsonObject {
    fun spawn(): Any?  *// 최종 객체 생성*
    fun createCompositeProperty(propertyName: String, isList: Boolean): JsonObject
}

12.2.5 객체 생성하기 - callBy 활용

생성자 호출하기

call의 단점

  • 모든 인자를 순서대로 전달해야 함

callBy의 장점

  • 파라미터와 값을 맵으로 연결
  • 모든 인자 제공
interface KCallable<out R> {
    fun call(vararg args: Any?): R      *// 기본값 지원 안함*
    fun callBy(args: Map<KParameter, Any?>): R  *// 기본값 지원!*
}

실제 사용

ObjectSeed - 객체 조립소 : 최종 조립 버튼 (spawn)

ClassInfo - 조립 설명서 : 최종 조립 실행 (callBy)

ensureAllParametersPresent - 부품 검사원

class ObjectSeed<out T: Any>(
    targetClass: KClass<T>,
    override val classInfoCache: ClassInfoCache
) : Seed {
    private val classInfo: ClassInfo<T> = classInfoCache[targetClass]
    private val valueArguments = mutableMapOf<KParameter, Any?>()
    private val seedArguments = mutableMapOf<KParameter, Seed>()

    *// 최종 인자 맵 (Seed들을 spawn으로 실제 객체로 변환)*
    private val arguments: Map<KParameter, Any?>
        get() = valueArguments + 
                seedArguments.mapValues { it.value.spawn() }

    override fun spawn(): T = 
        classInfo.createInstance(arguments)  *// callBy 사용!*
}

타입 안전한 객체 생성

class ClassInfo<T : Any>(cls: KClass<T>) {
    private val constructor = cls.primaryConstructor!!

    fun createInstance(arguments: Map<KParameter, Any?>): T {
        ensureAllParametersPresent(arguments)
        return constructor.callBy(arguments)  *// 기본값 자동 처리!*
    }

    private fun ensureAllParametersPresent(arguments: Map<KParameter, Any?>) {
        for (param in constructor.parameters) {
            if (arguments[param] == null &&
                !param.isOptional &&           *// 기본값 있는지*
                !param.type.isMarkedNullable   *// 널 허용인지*
            ) {
                throw JKidException("Missing parameter ${param.name}")
            }
        }
    }
}

리플렉션 API 계층구조

KAnnotatedElement  ← 모든 선언이 어노테이션을 가질 수 있음
    ↑
KCallable  ← 호출 가능한 모든 것
    ↑
├─ KFunction  ← 함수들
└─ KProperty  ← 프로퍼티들
    └─ KMutableProperty  ← 변경 가능한 프로퍼티

언제 리플렉션을 사용할까?

  • "어떤 타입이든 처리해야 할 때" - 프레임워크, 라이브러리
  • "객체와 데이터 간 변환" - JSON, XML, 데이터베이스
  • "자동으로 객체 생성" - 의존성 주입, 팩토리
  • "런타임에 코드 분석" - 테스트, 디버깅
  • "설정을 코드에 연결" - 어노테이션 기반 설정

'language > Kotilin In Action' 카테고리의 다른 글

7주차(14,15)  (1) 2025.09.01
6주차 (13장)  (2) 2025.09.01
4주차(9~10장)  (1) 2025.09.01
3주차 (6~8장)  (1) 2025.09.01
2주차 (4~5장)  (1) 2025.09.01