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*
제네릭스의 장점
- 타입 안전성: 컴파일 시점에 타입 체크
- 재사용성: 하나의 코드로 여러 타입 지원
- 성능: 캐스팅 불필요
주요 문법
*// 클래스*
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*
왜 인라인에서만 가능할까?
- 인라인 함수: 호출 지점에 코드가 복사됨
- 타입 정보 보존: 복사할 때 구체적인 타입으로 치환
- 결과: 마치 직접
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 *// 클래스 참조*
)
사용 가이드
- 간단한 마커:
annotation class MyMarker - 값 하나:
annotation class MyValue(val value: String) - 클래스 참조:
val clazz: KClass<out SomeInterface> - 제네릭 클래스:
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든 OKdata 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 |