Developing Myself Everyday
article thumbnail

제네릭의 정의


제네릭이란 자료형의 타입을 일반화하는 것을 의미합니다. 이는 내부에서 타입을 지정하는 것이 아니라, 외부에서 사용 시점에 자료형을 지정하게끔 일반화시켜 두는 방법입니다.

 

제네릭을 사용하면 함수나 클래스 등을 일반화시켜 재사용하기 쉽게 만들 수 있습니다.

 

제네릭은 object 같은 싱글톤 패턴을 사용하는 상황에서는 사용할 수 없습니다. 오직 하나의 객체만 생성하기에 굳이 일반화할 필요가 없기 때문입니다.

 

 

타입 매개변수(Type Parameter)

클래스나 함수의 자료형을 임의의 문자로 지정해서 컴파일 다임에 자료형 점검을 할 때 사용합니다.

 

타입 매개변수는 영어 대문자로 표시하며 아래와 같이 사용될 수 있습니다. 다만 이에 대한 규칙이 존재하는 것은 아닙니다. 그렇기에 유형에 더 구체적인 이름을 사용해도 됩니다.

T 일반적으로 사용되는 타입 매개변수
E 요소(Element)의 타입을 나타내는 매개변수. 주로 컬렉션(Collection)에서 사용
K 맵(Map)에서 키(Key)의 타입을 나타내는 매개변수
V 맵(Map)에서 값(Value)의 타입을 나타내는 매개변수
N 숫자(Number) 타입을 나타내는 매개변수
R 반환(Return) 타입을 나타내는 매개변수
S, U, V 등 여러 개의 타입 매개변수가 필요한 경우, 추가적으로 사용될 수 있는 일반적인 알파벳

 

 

타입 인자(Type Argument)

객체를 생성하거나 함수를 호출할 때 실제 자료형을 지정해서 일반화된 타입에 실제 타입을 지정하는 데 사용됩니다. 다만 대부분은 명시 타입 추론이 가능하기 때문에 자동으로 타입 인자를 유추합니다.

 

추상 클래스인터페이스는 객체를 생성하기 못하기에 클래스가 이를 상속할 때, 타입 인자를 위임호출 처리해야 합니다.

 

 

 

 

제네릭 함수


제네릭 함수는 fun 예약어함수명 사이에 꺽쇠괄호<>를 사용해서 타입 매개변수와 타입 인자에 사용될 자료형을 문자로 정의합니다.

 

 

 

위의 그림과 같이 제네릭 데이터 타입을 지정하면 아래와 같이 사용할 수 있습니다. 제네릭 함수를 호출할 때에도 타입 인자를 지정해야 하지만, 타입 추론이 가능하기 때문에 생략할 수 있습니다.

fun main() {
    val stringList: List<String> = listOf("Hello", "World")
    printList(stringList)

    val integerList: List<Int> = listOf(10, 20)
    printList(integerList)
}

fun <T> printList(list: List<T>) {
    for (item in list) {
        println(item)
    }
}

 

 

입력과 반환 자료형이 다를 경우

제네릭 함수를 정의하다 보면 입력 자료형과 반환 자료형이 다를 경우일 때가 있습니다. 이때 반환 자료형의 타입 매개변수는 주로 R로 표시합니다.

fun <T, R> combineValues(a: T, b: T, op: (T, T) -> R): R {
    return op(a, b)
}

fun main() {
    val value = combineValues(24, 26) { a, b -> "결과는 ${(a + b)}" }
    println(value) // 결과는 50
}

이 때 a와 b의 자료형은 동일해야 합니다. 

 

 

타입 매개변수 타입을 특정 자료형 제한

제네릭 함수의 타입 매개변수의 타입을 특정 자료형으로 제한할 수 있습니다. 다만 특정 자료형이 final type일 경우에는 자료형이 미리 결정되어 버려 제네릭을 사용할 이유가 없어져 버리게 됩니다. 

 

아래와 같이 T의 자료형을 Number와 그 하위 자료형으로 제한할 수 있습니다.

fun <T: Number, R> combineValues(a: T, b: T, op: (T, T) -> R): R {
    return op(a, b)
}

 

 

만약 두 가지 이상의 상한 타입을 정해야 한다면 where 키워드를 사용해 상한 타입을 정해줄 수 있습니다.

fun <T> combineValues(a: T, b: T, op: (T, T) -> T): T where T: Number, T: Comparable<T> {
    return op(a, b)
}

 

 

 

 

제네릭 클래스


클래스도 속성과 메서드의 자료형을 일반화하면 다양한 클래스로 객체를 생성하는 효과를 가질 수 있습니다. 이러한 클래스를 제네릭 클래스라고 합니다.

 

클래스를 제네릭으로 만들기 위한 형식은 함수를 제네릭으로 만들 때와는 다릅니다. 아래와 같이 클래스 이름 오른쪽에 일반 유형 매개변수를 추가하고 클래스에 속성에 일반 유형 매개변수를 지정할 수 있습니다. 이렇게 지정된 속성의 타입은 클래스를 인스턴스화할 때 데이터 유형이 지정됩니다.

 

 

 

아래와 같이 제네릭 클래스를 만들어 보겠습니다. 아래의 예시에서는 일반 유형 매개변수를 T로 받고 answer의 타입을 T로 지정해주고 있습니다.

class Question<T>(
    val questionText: String,
    val answer: T,
    val difficulty: String
)

 

 

클래스의 객체를 생성할 때, 데이터 유형을 지정해 주면 하나의 클래스로 3가지의 answer 타입을 갖는 코드를 만들 수 있습니다.

fun main() {
    val question1 = Question<String>("Quoth the raven ___", "nevermore", "medium")
    val question2 = Question<Boolean>("The sky is green. True or false", false, "easy")
    val question3 = Question<Int>("How many days are there between full moons?", 28, "hard")
}

 

 

타입 매개변수는 기본 널러블 자료형이다.

제네릭 클래스의 타입 매개변수는 기본적으로 널러블한 자료형입니다. 그렇기 때문에 아래와 같이 널러블을 처리해도 문제가 없습니다.

fun main() {
    val question = Question<String?>("Quoth the raven ___", "nevermore", "medium")
 }

 

 

클래스의 객체를 제네릭 클래스의 타입으로 지정

다른 클래스의 객체를 제네릭 클래스의 타입 매개변수로 지정할 수 있습니다.

 

아래의 예시는 이넘 클래스와 데이터 클래스, 제네릭 클래스를 정의한 코드입니다.

// 이넘 클래스 정의
enum class Color {
    RED, GREEN, BLUE
}

// 데이터 클래스 정의
data class BoxData<T>(
    val color: T,
    val size: Int
)

// 제네릭 클래스 정의
class Box<T>(val item: T) {
    override fun toString(): String {
        return "$item"
    }
}

 

 

데이터 클래스의 타입 매개변수로 이넘 클래스를 넣고 다시 이 데이터 클래스를 Box 클래스의 타입 매개변수로 넣습니다. 그럼 아래와 같이 사용할 수 있습니다.

val redBoxData = BoxData<Color>(RED, 10)
val greenBoxData = BoxData<Color>(GREEN, 25)

val redBox = Box<BoxData<Color>>(redBoxData)
val greenBox = Box<BoxData<Color>>(greenBoxData)

println("Red Color: $redBox") // Red Color: BoxData(color=RED, size=10)
println("Green Color: $greenBox") // Green Color: BoxData(color=GREEN, size=25)

 

 

 

 

제네릭 인터페이스


인터페이스도 일반화해서 제네릭 인터페이스로 지정할 수 있습니다. 제네릭 인터페이스로 지정하는 방법은 클래스를 제네릭으로 지정할 때와 동일합니다.

 

 

제네릭 인터페이스를 상속한 제네릭 클래스

일반 인터페이스처럼 제네릭 인터페이스도 상속할 수 있습니다. 

interface Box<T> {
    val color: T
    override fun toString(): String
}

class BoxImpl<T>(override val color: T): Box<T> {
    override fun toString(): String {
        return color.toString()
    }
}

fun main() {
    val redBox = BoxImpl("RED")
    val blueBox = BoxImpl("BLUE")

    println("Red Color: $redBox") // Red Color: RED
    println("Blue Color: $blueBox") // Blue Color: BLUE
}

 

 

 

 

변성


코틀린의 제네릭 특성 중 변성은 List<String> List<Any> 같이 기저 타입이 같고 타입 인자기 다른 여러 타입이 서로 어떤 관계가 있는지 설명하는 개념입니다.

 

변성에는 무변성, 공변성, 반공변성, 세 종류가 있습니다.

  • 무변성(invariance): 타입 매개변수는 단순한 문자 표시라서 상속관계를 추론하지 못한다. 그래서 타입 인자에서 들어온 자료형으로만 처리한다.
  • 공변성(covariance): 타입 매개변수는 상속관계를 확인해서 추론한다. 이때는 예약어 out을 타입 매개변수 앞에 지정한다. 이는 상한 자료형을 지정해서 그 하위 자료형의 객체를 모두 처리한다는 뜻이다.
  • 반공변성(contravariance): 타입 매개변수는 상속관계를 공변성의 역방향을 추론한다. 이때는 타입 매개변수 앞에 in을 붙인다. 타입 인자로 하위 자료형을 지정하면 상속관계에 맞춰 상위 자료형을 처리한다.

 

 

변성을 생산자와 소비자 개념에서 이해

생산자와 소비자 개념에서 변성을 이해할 수 있습니다. 

 

out으로 지정한 공변성은 데이터를 변경 없이 반환 자료형 등에 사용합니다. 타입 매개변수를 메서드의 반환값으로 처리하는 연산만 제공하고, 입력값으로 사용할 수 없습니다. 그렇기에 생산자입니다.

 

in으로 지정한 반공변성은 데이터를 변경할 수 있고, 타입 매개변수를 메서드의 입력으로 처리합니다. 그렇기에 소비자입니다.

 

생산자와 소비자에 해당하지 않는 경우에는 무변성으로 처리합니다.

 

또한 in과 out은 해당 위치에서만 사용할 수 있다는 것을 의미합니다.

 

아래와 같은 코드에서 `t: T`는 in 위치에 속하고 리턴 타입인 `T`는 out 위치에 속합니다.

interface Transformer <T> {
  fun transform(t: T): T
}

 

 

일반적인 자료형 처리

일반적으로 변수를 정의할 때 자료형을 지정하는 것은, 해당 변수를 해당 자료형의 상위 자료형에도 할당할 수 있다고 말하는 것과 같습니다.

 

아래의 코드에서 Number의 상위 자료형은 Any입니다. 그렇기에 Any 자료형에 Number 변수를 할당할 수 있습니다. 반면에 Number의 하위 자료형 중에 하나가 Int입니다. 그렇기에 하위 자료형에는 상위 자료형의 변수를 할당할 수 없습니다.

val num : Number = 100
val any : Any = num
val int : Int = num // 에러 발생

 

 

무변성 자료형 처리

무변성으로 정의된 클래스는 모든 타입 매개변수를 대체해서 처리합니다. 이때에는 상속관계에 대한 정보가 없어서 해당 자료형일 경우에만 에러 없이 처리합니다.

 

일반적으로 무변성으로 정의한다고 했을 때에는 문제가 없습니다.

open class Animal {
  fun feed() { ... }
}

class Herd<T: Animal> { // 무변성으로 지정
  val size : Int get() = { ... }
  operator fun get(i: Int): T { ... }
}

fun feedAll(animals: Herd<Animal> {
  for (i in 0 until animals.size) {
    animals[i].feed()
  }
}

 

 

다만 Animal 클래스를 상속한 다른 클래스를 정의한다면 아래와 같은 경우에 T 타입 파라미터에 대해 아무 변성도 지정하지 않았기 때문에 Cat 클래스는 Animal 클래스의 하위 클래스라는 것을 컴파일러는 알지 못합니다.

class Cat: Animal() {
  fun cleanLitter() { ... }
}

fun takeCareOfCats(cats: Herd<Cat>) {
  for (i in 0 until cats.size) {
    cats[i].cleanLitter()
  }
  // feedAll(cats) // Error: inferred type is Herd<Cat>, but Herd<Animal> was expected
}

 

 

따라서 아래와 같이 out을 넣어 공변적인 클래스로 만들면 이제 feedAll을 사용할 수 있게 됩니다.

class Herd<out T : Animal> {
  ...
}

 

 

out 키워드의 역할

결과적으로 out 키워드의 역할은 하위 클래스의 자료형을 상위 클래스에 자료형에 할당할 수 있게 하는 것뿐만이 아니라, 타입 매개변수를 out 위치에서만 사용할 수 있게 한다는 것을 의미합니다.

 

 

List를 예를 들어 보겠습니다. 

public interface List<out E> : Collection<E> {
    ...
}

List 인터페이스는 읽기 전용이기 때문에 값을 추가하거나 기존 값을 변경하지 않습니다. 그렇기 때문에 List의 타입 매개변수는 공변성으로 정의되어 있습니다.

 

 

반면에 MutableList의 경우에는 T를 인자로 받고 T의 값을 반환합니다. 그렇기에 MutableList의 타입 매개변수는 무변성으로 정의되어 있습니다.

public inline fun <T> MutableList(size: Int, init: (index: Int) -> T): MutableList<T> {
    ...
}

 

 

생성자 매개변수

타입 매개변수가 out으로 지정되었다고 해도 생성자 매개변수에 사용할 수 있습니다. 생성자 매개변수는 인스턴스를 생성한 뒤 나중에 호출할 수 있는 메서드가 아니기 때문에 위험의 여지가 없습니다. 

 

그렇기에 out으로 정의되어도 생성자 파라미터 선언에 사용할 수 있습니다.

class Herd<out T: Animal>(vararg animals: T) { ... }

 

다만, val이나 var 키워드를 사용하면 게터나 세터를 정의하는 것과 같기 때문에 val은 out위치, var은 in, out 위치 모두에 해당합니다.

 

아래와 같이 leadAnimal 매개변수를 var로 정의한다면 in 위치에도 해당하기 때문에 Animal을 공변성으로 지정할 수 없습니다.

class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) { ... }

 

 

반공변성

타입 매개변수에 in을 지정해서 소비자로만 처리하는 방식이 반공변성입니다. 

 

아래와 같은 Comparable 인터페이스는 compareTo 메서드에 매개변수에 사용하기 위해 타입 매개변수를 in으로 지정했습니다.

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

 

또한 하위 클래스가 지정된 곳에 상위 클래스의 객체가 할당할 수 있습니다. 하위 클래스를 기준으로 상위 클래스를 확인할 수 있기 때문입니다.

open class Animal
class Dog : Animal()

class Container<in T>

var a: Container<Dog> = Container<Animal>()

 

 

반공변성으로 입력을 받아 갱신

반공변성으로 입력을 받아 속성을 갱신할 때에는 속성을 아래와 같이 private로 처리해야 합니다.

class foo<in T>(private var b: T) {
    fun consume(t: T) {

    }
}

 

코틀린에서 val, var으로 변수를 선언한다면 val의 경우에는 getter를 var의 경우에는 getter, setter를 자동으로 생성합니다. 이는 자동으로 클래스 내의 변수를 캡슐화합니다. 

 

private로 변수를 선언한다면 이러한 getter와 setter를 생성하지 않습니다.

 

반공변성은 변수를 소비자로만 처리하고 다른 곳에서 반환되지 않아야 하기 때문에 getter가 생성된다면 반공변성인 코드에서는 private으로 변수를 선언해야 합니다.

 

 

사실 이러한 특성은 공변성에서도 마찬가지입니다. 아래와 같은 코드에서 b는 val로 선언되어 있습니다.

class foo<out T>(val b: T) {
    fun consume(): T {
        return b
    }
}

val은 getter만 생성하기에 공변성의 특성과 일치합니다. 오직 생산만 하고 있습니다.

 

만약 var로 선언한다면 setter가 생기에 되어 에러가 발생하게 됩니다.

 

 

사용자 지정 변성

사용자 지정 변성은 다르게 말하면 사용 지점에 변성을 지정한다는 말입니다.

 

copyData라는 함수를 살펴보겠습니다. 이 함수는 컬렉션의 원소를 다른 컬렉션으로 복사합니다.

fun <T> copyData(
  source: MutableList<T>,
  destination: MutableList<T>
  ) {
  for (item in source) {
    destination.add(item)
  }
}

현재 source는 읽기만 하고 destination은 쓰기만 합니다.

 

 

서로 다른 리스트 타입에 대해서도 작동하게 만들기 위해서는 아래와 같이 정의할 수 있습니다.

fun <T: R, R> copyData(
  source: MutableList<T>,
  destination: MutableList<R>
  ) {
  for (item in source) {
    destination.add(item)
  }
}

 

여기서 사용자 지정 변성을 사용하면 더 간단하게 정의할 수 있습니다.

fun <T> copyData(
  source: MutableList<out T>,
  destination: MutableList<in T>
  ) {
  for (item in source) {
    destination.add(item)
  }
}

 

 

스타 프로젝션

실제로 사용하는 시점의 함수 등에서도 어떤 자료형인지 확신할 수 없을 수도 있습니다. 이런 경우에는 스타 프로젝션으로 타입을 처리해서 명확한 타입으로 변환한 후에 처리하면 됩니다.

 

JVM에서는 기본 자료형은 관리하고 있지만, 실제 제네릭으로 처리하는 자료형을 제거하기 때문에 자료형을 바로 처리하지 못할 경우 스타 프로젝션을 사용합니다.

 

 

Any와 스타 프로젝션

단, 스타 프로젝션은 타입이 정해지지 않았을 뿐이지 모든 타입을 담을 수 있는 것이 아닙니다.

 

아래의 코드를 보겠습니다. 모든 타입을 담을 수 있는 자료형은 Any 자료형입니다. 

fun main() {

    val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")
    val chars = mutableListOf('a', 'b', 'c')

    val unknownElements: MutableList<*> =
        if(Random.nextBoolean()) list else chars

    //unknownElements.add(42)
    println(unknownElements)
}

 

여기서 unknownElements는  MutableList<out Any?> 처럼 동작합니다. 

unknownElements의 원소의 타입을 모를지라도 Any? 타입으로 원소를 꺼내올 수 있지만, 반대로 add는 사용할 수 없습니다.

 

add를 사용할 수 없는 이유는 알 수 없는 타입에 구체적인 타입을 넘기면 안전하지 못하다고 컴파일러가 판단하기 때문입니다.

 

 

 

 

리플렉션


리플렉션은 실행 중에 클래스의 정보를 동적으로 검사하고 조작할 수 있도록 하는 기능입니다.

 

리플렉션에 대해 이해하기 위해서는 JVM의 작동방식에 대해 이해할 필요가 있습니다. JVM은 클래스를 로드 시 클래스 로더를 사용해 클래스에 대한 정보를 즉 MetadataMetaspace 영역에 저장합니다.

 

리플렉션은 로딩된 클래스의 Metadata를 활용하여 클래스의 정보를 동적을 조작하는 기능입니다.

 

 

최상위 속성 참조

최상위 속성도 메모리에 로딩되기에 이를 참조할 수 있습니다.

var x = 1

fun main() {
    println("${::x}") // val x: kotlin.Int
    println("${::x.get()}") // 1
    ::x.set(2)
    println("${::x.get()}") // 2
    println(::x.name) // x
}

속성 참조를 사용해 속성을 사용할 수 있으며 get과 set을 사용할 수도 있습니다.

 

 

함수와 클래스 생성자 참조

함수와 클래스를 참조할 때에도 참조연산자 다음에 함수명과 클래스명을 넣는 것으로 참조합니다.

fun add(x: Int, y: Int): Int = x + y

class Hello(val comment: String)

fun main() {
    val addF = ::add
    println(addF(1, 2)) // 3

    val helloC = (::Hello)("안녕하세요")
    println(helloC.comment) // 안녕하세요
}

 

함수와 클래스 참조를 변수에 할당할 수 있고 함수는 인자를 전달하면 실행되고 클래스는 인자를 전달하면 객체가 생성됩니다.

 

 

클래스, 클래스의 속성과 메서드, Companion Object 참조

클래스를 참조할 때, 클래스 자체를 참조하는 방법과, 클래스의 속성, 메서드, Companion Object의 속성과 메서드를 참조하는 방식이 각각 다릅니다.

 

  • 클래스 참조는 클래스명 + :: + class 예약어를 사용합니다.
  • 객체에 대한 속성은 객체 + :: + 속성명을 사용합니다.
  • Companion Object의 속성은 (클래스명) + :: + 속성명, 메서드명을 사용합니다.

 

class Example {
    val a = 100
    fun getFull(): Int = a

    companion object {
        const val CONST = 200
        fun getCom() = "컴패니언 메서드 실행"
    }
}

fun main() {
    println("${Example::class}")
    println("${Example::a}")
    println((Example::getFull)(Example()))
    println("${(Example)::CONST.get()}")
    println(((Example)::getCom)())
}

 

 

KClass

코틀린의 리플렉션 API를 사용해 MyClass::class와 같은 방식을 사용하면 KClass 인터페이스를 얻을 수 있게 됩니다.

class Example {
    val a = 100
}

fun main() {
    val KClass: KClass<Example> = Example::class
}

KClass는 리플렉션을 통해 얻을 수 있는 레퍼런스 객체입니다. 이 객체로 클래스의 정보를 얻거나 속성, 메서드에 접근할 수 있습니다.

 

Retrofit의 create 메서드를 사용할 때, 우리가 사용할 인터페이스의 객체를 리플렉션으로 넘겨주게 됩니다.

 

 

다만 여기서 create는 자바의 Class가 필요하기에 코틀린 리플렉션으로 얻은 KClass를 .java로 자바 클래스 타입으로 바꿔줘야 합니다.

 

 

Reified

Reified 키워드는 인라인 함수에서 사용되며 런타임에 제네릭의 타입 정보를 알고 싶을 때 사용합니다.

 

제네릭의 개념은 코드를 컴파일할때 컴파일러가 T가 어떤 타입인지 알게 하는 것을 의미합니다. 이 말은 런타임에는 T가 어떤 타입이 될 수 있는지 모른다는 말이 됩니다. 런타임에는 오직 컴파일 할 때 T로 정해진 객체를 알고 있게 될 뿐입니다.

 

이때 Reified를 사용하면 런타임에도 제네릭 매개변수의 타입 정보를 알 수 있게 됩니다. 이를 사용하는 방법은 제네릭의 앞에 `reified` 키워드를 써주면 됩니다.

inline fun <reified T> function(argument: T)

 

 

일반적으로 제네릭을 사용했을 때 리플렉션을 사용한 예시를 보겠습니다.

fun <T> printGenerics(value: T) {
    when (value::class) {  // compile error!
        String::class.java -> {
            println("String : $value")
        }
        Int::class.java -> {
            println("Integer : $value")
        }
    }
}

위와 같은 경우에는 런타임에 타입 정보를 알 수 없기 때문에 에러가 발생합니다.

 

 

아래와 같이 reified 키워드를 추가하면 이젠 런타임에도 타입 정보를 알 수 있습니다.

inline fun <reified T> printGenerics(value: T) {
    when (T::class) {
        String::class -> {
            println("String : $value")
        }
        Int::class -> {
            println("Int : $value")
        }
    }
}

 

 

인자 없이 리플렉션 한 클래스의 객체 생성

위에서 클래스 참조에 인자를 넘기면 객체가 생성된다고 말했습니다. 매개변수가 존재하지 않는 클래스의 객체를 생성하려면 `createinstance` 메서드를 사용해야 합니다.

class Example {
    val a = 100
    fun getComment(): String = "안녕하세요"
}

fun main() {
    val kClass: KClass<out Example> = Example::class
    val instance = kClass.createInstance()
    println(instance.getComment()) // 안녕하세요
}

 

 

클래스의 매개변수에 초기값이 있는 경우에는 2가지 방법을 다 사용할 수 있습니다.

class Example(val comment: String = "안녕하세요")

fun main() {

    val kClass: KClass<out Example> = Example::class
    val instance = kClass.createInstance()
    println(instance.comment) // 안녕하세요
    
    println((::Example)("반갑습니다").comment) // 반갑습니다
}

 

 

 

 

함수 인터페이스 확인


위에서 Class를 참조하면 KClass 인터페이스를 얻을 수 있다고 했습니다. 

 

함수의 경우에는 KFunctionN의 인터페이스를 얻게 됩니다. 여기서 N은 매개변수의 숫자를 나타내고 자료형의 마지막은 Return 자료형을 나타냅니다.

import kotlin.reflect.KFunction2

fun add(x: Int, y: Int): Int = x + y

fun main() {
    val addF : KFunction2<Int, Int, Int> = ::add
}

 

 

반환값과 매개변수가 없는 경우에는 Unit을 가지게 됩니다.

fun print() = println("실행")

fun main() {
    val printF : Function<Unit> = ::print
}

 

 

함수를 매개변수나 속성으로 전달

이러한 리플렉션 함수 인터페이스를 사용해서 매개변수나 속성으로 함수를 전달할 수 있습니다.

import kotlin.reflect.KFunction2

fun add(x: Int, y: Int): Int = x + y
fun subtract(x: Int, y: Int): Int = x - y

fun performOperation(x: Int, y: Int, operation: KFunction2<Int, Int, Int>): Int {
    return operation(x, y)
}

fun main() {
    val x = 10
    val y = 5

    val result1 = performOperation(x, y, ::add)
    val result2 = performOperation(x, y, ::subtract)

    println("더하기: $result1") // 더하기: 15
    println("빼기: $result2") // 빼기: 5
}

 

 

 

 

애노테이션(annotation)


애노테이션의 사전적 의미는 주석입니다. 우리가 보통 통상적으로 말하는 주석은 프로그램을 설명할 때 사용하는 것을 말합니다.

// 코드 설명

다만 이때 사용되는 주석은 `comments`를 말합니다.

 

 

애노테이션은 @Annotation 예약어를 사용해 해당 클래스나 함수에 정보를 제공합니다.

 

@Annotation 예약어를 붙이면 지정하는 곳에 객체가 만들어집니다. 애노테이션은 코틀린 실행에서는 아무런 영향을 주지 않으며 개발 툴이나 프레임워크에서 특별한 정보로 사용하는 경우에 영향을 미칩니다.

 

 

커스텀 애노테이션

annotation class로 클래스를 정의하면 커스텀 애노테이션을 정의할 수 있습니다.

annotation class MyCustomAnnotation(val description: String)

class Example {
    @MyCustomAnnotation("이 함수는 커스텀 애노테이션을 사용한 예시입니다.")
    fun myAnnotatedFunction() {
        // 함수 내용
    }
}

 

 

이렇게 정의한 애노테이션은 리플렉션으로 확인할 수 있습니다.

fun main() {
    val example = Example()
    val method = example::class.java.getDeclaredMethod("myAnnotatedFunction")
    val annotation = method.getAnnotation(MyCustomAnnotation::class.java)

    if (annotation != null) {
        println("어노테이션 설명: ${annotation.description}")
    } else {
        println("어노테이션이 없습니다.")
    }
}
// 출력: 어노테이션 설명: 이 함수는 커스텀 어노테이션을 사용한 예시입니다.

위의 예시에서는 `getDeclaredMethod`로 정의된 메서드를 가져온 다음, `getAnnotation`으로 정의된 애노테이션을 가져왔습니다.

 

 

코틀린 애노테이션

코틀린 내부적으로 정의된 애노테이션을 사용할 수도 있습니다. 가장 흔히 사용되는 애노테이션은 @Deprecated 애노테이션입니다. 

 

이 애노테이션을 사용하면 특정 함수나 클래스가 더 이상 지원하지 않아 사용하지 못할 경우에 경고를 표시해 줍니다.

Deprecated("이 클래스는 더 이상 사용되지 않습니다. 대신 NewClass를 사용하세요.")
class DeprecatedClass {
   ...
}

 

 

메타 애노테이션

메타 애노테이션은 쉽게 말하면 애노테이션에 붙이는 애노테이션입니다. 이는 사용할 애노테이션을 정의하는 데 사용됩니다.

 

많이 사용되는 메타 애노테이션은 아래와 같습니다.

  • @Target: 애노테이션을 추가할 곳을 제한
  • @Retention: 소스 코드나 런타임 등까지 애노테이션 정보에 대한 유지 여부를 제한

 

@Retention(AnnotationRetention.RUNTIME) // 런타임까지 어노테이션 정보 유지
@Target(AnnotationTarget.FUNCTION) // 함수에 어노테이션을 사용할 수 있도록 함
annotation class MyCustomAnnotation(val description: String)

 

 

 

 

자바 애노테이션


자바 애노테이션 - 패키지

패키지를 파일로 지정해 이름을 제공할 수 있습니다.

@file:JvmName("dahl.moom")
package dahl.moon

fun foo() = "패키지 정의"

 

이를 다른 패키지에서 사용할 수 있습니다.

import dahl.moon.*

println(foo())

 

 

자바 애노테이션 - 이름 충돌

함수를 정의할 때 동일한 이름을 가졌더라도 자료형에 따라 여러 가지의 함수를 만들 수 있습니다. 이를 함수 오버로딩이라고 합니다. 

 

여기서 주의할 점은 자료형이 리스트 등의 원소를 관리하는 자료형일 때입니다.

public inline fun <T> List(size: Int, init: (index: Int) -> T): List<T> = MutableList(size, init)

list는 제네릭으로 타입 매개변수가 설정되었기 때문에 JVM에서는 런타임에 세부 자료형을 알지 못하게 됩니다. 그렇기 때문에 동일한 이름의 함수에서 리스트로 지정할 경우 동일한 자료형으로 인식해서 함수를 구분할 수 없게 됩니다.

 

그래서 동일한 파일에서 원소를 관리하는 자료형을 사용할 때에는 @JvmName 애노테이션을 사용해 각각의 함수 이름을 지정해줘야 합니다. (하나만 지정해 줘도 되긴 합니다.)

@JvmName("fooString")
fun foo(a: List<String>) {
    println(a)
}

@JvmName("fooInt")
fun foo(b: List<Int>) {
    println(b)
}

fun main() {
    foo(listOf("a", "b", "c"))
    foo(listOf(1, 2, 3))
}

 

 

자바 애노테이션 - 정적 처리

코틀린에서는 정적 영역은 없지만, JVM으로 처리할 때 정적으로 변환됩니다. 이를 애노테이션을 사용해서 명확하게 지정할 수 있습니다.

class Bar {
    companion object {
        @JvmStatic
        var name: String = "bar"
    }
}

 

 

 

 

 

Reference

 

Kotlin Generics — 변성

코틀린의 제네릭 특징 중 변성에 대해 알아봅니다.

medium.com

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!