본문 바로가기

프로그래밍 언어 기초/KOTLIN

[Kotlin] 코틀린의 심화 기술 - Object, Delegation, Type Aliases, Reflection

개요

오늘은 지난 포스팅에 이어 코틀린의 또 다른 특별 클래스 Object에 대해 알아보고

코틀린에서 제공하는 특수한 기능 Delegation, Type Aliases,  Reflection에 대해서 알아보도록 하겠습니다.

 


Object

코틀린의 Object 클래스를 보다 잘 이해하기 위하여 자바(Java)의 프로그래밍 디자인 패턴 하나를 살펴보도록 하겠습니다.

 

자바(Java)에서 객체(Class)는 프로그램 내에서 자신에 대한 정보를 기록할 메모리 주소를 할당받습니다.

런데 객체(Class)를 잘 만들어서 사용의 빈도가 늘어날수록 자신에 대한 정보를 기록할 메모리 주소를 많이 필요로 하게 되므로 

재사용성과 효율성은 상호 모순적인 관계에 놓이게 됩니다.

 

이에 따라 자바 개발자들은 이를 해결하는 방안을 모색하게 되었는데요.

 

잘 만든 객체가 과도하게 메모리를 사용하는 문제를 해결하기 위해

`자주 사용하는 객체는 하나의 주소를 할당받고, 이를 필요로 하는 곳에서 지속해서 가져다 쓸 방법`을 고안하게 됩니다.

즉, 자주 사용하는 객체가 하나의 메모리 주소를 할당받고 필요로 하는 곳에서 지속해서 사용하는 프로그램 설계 방법

바로 싱글턴 패턴(Singleton Pattern)입니다.

 

public class AppUtil {
    private static AppUtil INSTANCE;
    private AppUtil(){}
    public static AppUtil getInstance(){
        if(INSTANCE == null){
            INSTANCE = new AppUtil();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {

     AppUtil util1 = AppUtil.getInstance();
     AppUtil util2 = AppUtil.getInstance();

     System.out.println(util1 == util2); // true
     System.out.println(util1); // AppUtil@15db9742
     System.out.println(util2); // AppUtil@15db9742

    }
}

 

자바(Java)에서 싱글턴 패턴은 위처럼 설계할 수 있고 그 과정을 정리하자면 아래와 같습니다.

  • ① 클래스의 생성자(constructor)를 접근 제한자 private를 통해 외부에서 사용하지 못하도록 제한한다.
  • ② 클래스 내부에 자기 자신을 담을 정적(static) 변수를 만든다.
  • ③ ②의 과정에 더불어 자기 자신을 담은 정적 변수를 반환할 정적(static) 메서드를 만든다.

자바 이후 개발된 코틀린에서는 아예 싱글턴(Singleton) 패턴이 적용된 된 객체, Object를 지원합니다.

코틀린의 Object는 자바와 비교하여 반복되는 보일러 플레이트(boilerplate) 코드를 획기적으로 줄였습니다.

코틀린에서 Object는 아래와 같이 사용합니다.

 

object MyObject {
    fun sayHello(str:String){
        println(str);
    }
}

 

또한, 실제로 이 Object가 싱글턴 패턴이 적용되었는지 확인해볼 수도 있습니다.

안드로이드 스튜디오와 같은 IDE에서는 컴파일된 코틀린 바이트 코드를 

자바(Java) 코드로 역변환하여 볼 수 있는 기능을 지원합니다.

이러한 기능을 활용하여 자바로 변환된 코틀린 코드를 확인해보면 아래와 같습니다.

public final class MyObject {
   @NotNull
   public static final MyObject INSTANCE;

   public final void sayHello(@NotNull String str) {
      Intrinsics.checkNotNullParameter(str, "str");
      System.out.println(str);
   }

   private MyObject() {
   }

   static {
      MyObject var0 = new MyObject();
      INSTANCE = var0;
   }
}

 


위임, Delegation

코틀린에서 클래스는 다른 클래스를 상속(Inheritance)받을 수 있습니다.

그러나, 어떤 부모 클래스를 여러 자식 클래스에서 상속받아야 하는 상황이라면 

자식 클래스의 수만큼 상속과 관련된 코드를 반복하여 작성해야 합니다.

만약 여러 개의 자식 클래스가 같은 로직을 통해 같은 기능을 지원하는 상황이라면

이 작업은 상당히 비효율적이고 지루한 반복 작업이 될 수 있습니다.

 

코틀린에서는 이에 사용할 수 있는 Delegation이라는 기술을 지원합니다.


Delegation은 부모 클래스를 상속한 자식 클래스 (또는 구현체 (xxxImpl))가 있을 때 

Delegation을 통해 이미 구현된 기능을 재사용할 수 있습니다.

 

interface Base {
    fun print()
}

class BaseImpl(val x:Int) : Base {
    override fun print() {
        println(x)
    }
}

class Derived(b:Base) : Base by b

fun main() {
    val b = BaseImpl(10);
    b.print();
    val d = Derived(b)
    d.print();
}

 

또한, Delegation은 특정한 기능 `만` 재설계하여 개발자가 자유롭게 커스텀할 수도 있습니다.

interface Base {
    fun print()
    fun addNumber(x:Int, y:Int):Int
}

class BaseImpl(val x:Int) : Base {
    override fun print() {
        println(x)
    }
    override fun addNumber(x:Int, y:Int):Int{
        return x+y+1;
    }
}

class Derived(b:Base) : Base by b {
    override fun addNumber(x:Int, y:Int):Int {
        return x+y+2;
    }
}

fun main() {
    val b = BaseImpl(10);
    val d = Derived(b)

    val result1 = b.addNumber(1,2); // 4
    val result2 = d.addNumber(1,2); // 5
    println(result1); // 4
    println(result2); // 5

}

 

위 코드에서 메서드 addNumber는 BaseImpl에서 이미 재정의(override)되었으나,

Derived 클래스에서 Delegation을 통해 새롭게 로직을 구성했습니다.

따라서, 각각의 인스턴스를 통해 계산한 값 result1과 result2는 서로 다른 로직에 의해 다른 값을 출력합니다.

 


Type aliases

코틀린에서는 Int, String과 같은 비교적 짧은 길이의 자료형도 있지만

개발자가 코드를 작성하기에 따라 긴 코드를 갖는 자료형도 존재합니다.

 

이름이 긴 자료형은 대부분 클래스의 이름인 경우가 많은데, 컬렉션 자료형도 이에 포함될 수 있습니다.

만약 너무 긴 자료형을 보다 축약된 형태로 사용하고자 할 때는 아래와 같이 Type aliases를 사용할 수 있습니다.

 

typealias NodeSet = Set<Network.Node>

typealias FileTable<K> = MutableMap<K, MutableList<File>>


또한, 코틀린에서는 함수도 Type Aliases로 사용할 수 있습니다.

코틀린 함수의 Type Aliases는 아래와 같이 작성합니다.

 

typealias MyHandler = (Int, String, Any) -> Unit

typealias Predicate<T> = (T) -> Boolean

 

만약 클래스 내부에 있는 중첩 클래스 등을 타입으로 사용하는 경우, Type aliases를 통해 더 간략하게 표현할 수도 있습니다.

 

class A {
    inner class Inner
}
class B {
    inner class Inner
}

typealias AInner = A.Inner
typealias BInner = B.Inner


코틀린에서 다양한 클래스와 함수에 대한 Type aliases가 가능한 이유는 

Type aliases가 새로운 유형을 만드는 것이 아니기 때문입니다.

 

typealias Predicate<T> = (T) -> Boolean

fun foo(p: Predicate<Int>) = p(42)

fun main() {
    val f: (Int) -> Boolean = { it > 0 }
    println(foo(f)) // prints "true"

    val p: Predicate<Int> = { it > 0 }
    println(listOf(1, -2).filter(p)) // prints "[1]"
}



위 코드에서 타입 Predicate는 대표적인 함수형 인터페이스의 한 종류입니다.

Predicate는 Type Alias와 제네릭을 통해 모든 타입에 대응할 수 있도록 설계하였습니다.
이에 따라 Predicate를 사용하는 곳에서는 <>를 통해 제네릭에 짝지을 자료형을 명시해주어야 하며, 

이 시점에 비로소 제네릭 T의 타입이 확정됩니다.

코틀린 컴파일러는 Type aliases로 설계된 Predicate 타입을 (Int) -> Boolean으로 인식합니다.
따라서, 개발자는 의미상의 구분과 가독성을 위해 타입을 명시해두었으나 

컴파일러는 이를 번역하여 클래스나 메서드 사이에서 전달할 수 있는 타입으로 바꾸어 줍니다.

 

따라서, Type Alias를 통해 얼마든지 다양한 자료형에 대해 정리하거나 새롭게 정의 및 확장할 수 있는 것입니다.


Reflection

Reflection은 프로그램이 자신의 구조를 검사하고 조작할 수 있는 기능을 제공하는 프로그래밍 언어의 기능입니다. 

Kotlin도 Reflection을 지원하여 실행 중에 클래스, 메서드, 프로퍼티 등을 검사하고 조작할 수 있습니다. 

Reflection은 일반적으로 런타임에 다음과 같은 작업을 수행하는 데 사용됩니다:

 

  1. 클래스의 정보 가져오기: 클래스의 이름, 생성자, 메서드, 프로퍼티 등의 정보를 가져올 수 있습니다
  2. 객체 생성 및 메서드 호출: 클래스의 인스턴스를 생성하고 메서드를 호출할 수 있습니다.
  3. 프로퍼티 값 설정 및 가져오기: 객체의 프로퍼티 값을 설정하고 가져올 수 있습니다.

Kotlin에서 Reflection을 사용하려면 kotlin-reflect 모듈을 의존성으로 추가해야 합니다. 

Reflection을 사용하여 클래스 정보를 검사하고 조작하는 간단한 예제를 살펴보겠습니다.

 

import kotlin.reflect.full.*

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

fun main() {
    val person = Person("Alice", 30)
    
    // 클래스 정보 가져오기
    val clazz = person::class
    println("Class name: ${clazz.simpleName}")
    
    // 프로퍼티 정보 가져오기
    clazz.declaredMemberProperties.forEach { prop ->
        println("Property: ${prop.name}, Value: ${prop.get(person)}")
    }
    
    // 메서드 정보 가져오기
    clazz.declaredMemberFunctions.forEach { func ->
        println("Function: ${func.name}")
    }
}


이 코드에서는 Person 클래스의 인스턴스를 생성하고, 해당 클래스의 이름, 프로퍼티 정보 및 메서드 정보를 출력합니다.

Reflection을 사용하면 실행 중에 클래스의 구조를 동적으로 검사하고 활용할 수 있습니다.

Reflection은 런타임 비용이 비싸므로 필요한 경우에만 사용해야 하며,

컴파일 타임에 미리 알고 있는 정보를 활용하는 것이 바람직합니다.


참고 자료

https://kotlinlang.org/docs/object-declarations.html

 

Object expressions and declarations | Kotlin

 

kotlinlang.org

https://kotlinlang.org/docs/delegation.html

 

Delegation | Kotlin

 

kotlinlang.org

https://kotlinlang.org/docs/type-aliases.html

 

Type aliases | Kotlin

 

kotlinlang.org

https://kotlinlang.org/docs/reflection.html

 

Reflection | Kotlin

 

kotlinlang.org