반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item33. 생성자 대신 팩토리 함수를 사용하라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item33. 생성자 대신 팩토리 함수를 사용하라

어리둥절범고래 2023. 12. 17. 17:18
반응형

클래스의 인스턴스를 만드는 가장 일반적인 방법은 기본 생성자(primary constructor)를 사용하는 방법입니다.

class MyLinkedList<T> (
  val head: T,
  val tail: MyLinkedList<T>?
)

val list = MyLinkedList(1, MyLinkedList(2, null))

 

디자인 패턴으로 굉장히 다양한 생성 패턴들이 만들어져 있으며, 일반적으로 이러한 생성 패턴은 객체를 생성자로 생성하지 않고 별도의 함수를 통해 생성합니다.

 

생성자의 역할을 대신 해 주는 함수를 팩토리 함수라고 부르며, 다양한 장점들이 있습니다.

 

  • 생성자와 다르게, 함수에 이름을 붙일 수 있다. 이름이 붙어있다면 훨씬 이해하기 쉽다.
  • 생성자와 다르게, 함수가 원하는 형태의 타입을 리턴할 수 있다.
  • 생성자와 다르게, 호출될 때마다 새 객체를 만들 필요가 없다. 함수를 사용해서 싱글턴 패턴처럼 객체를 하나만 생성하게 강제하거나, 캐싱 메커니즘을 사용하거나, 객체를 만들 수 없을 경우 null을 리턴하게 할 수도 있다.
  • 팩토리 함수는 아직 존재하지 않는 객체를 리턴할 수도 있다.
  • 객체 외부에 팩토리 함수를 만들면, 가시성을 제어할 수 있다.
  • 팩토리 함수를 인라인으로 만들 수 있으며, 그 파라미터들을 reified로 만들 수 있다.
  • 생성자로 만들기 복잡한 객체도 만들 수 있다.
  • 생성자는 즉시 슈퍼클래스 또는 기본 생성자를 호출해야 하지만, 팩토리 함수를 사용하면, 원하는 때에 생성자를 호출할 수 있다.

 

 

 

Companion 객체 팩토리 함수

class MyLinkedList<T>(
    val head: T,
    val tail: MyLinkedList<T>?
) {
    companion object {
        fun <T> of(vararg elements: T): MyLinkedList<T>? {
            // ...
        }
    }
}

val list = MyLinkedList.of(1, 2)

팩토리 함수를 정의하는 가장 일반적인 방법은 companion 객체를 사용하는 것이며, 자바의 정적 팩토리 함수(static factory function)와 같습니다.

 

 

코틀린에서는 이러한 접근 방법을 인터페이스에서 구현할 수 있습니다.

class MyLinkedList<T>(
    val head: T,
    val tail: MyLinkedList<T>?
): MyList<T> {
    interface MyList<T> {
        // ...
        
        companion object {
            fun <T> of(vararg elements: T): MyList<T>? {
                // ...
            }
        }   
    }
}

val list = MyList.of(1, 2)

 

또한, companion 객체는 클래스를 상속받을 수도 있습니다.

abstract class ActivityFactory {
    abstract fun getIntent(context: Context): Intent

    fun start(context: Context) {
        val intent = getIntent(context)
        context.startActivity(intent)
    }

    fun startForResult(activity: Activity, requestCode: Int) {
        val intent = getIntent(activity)
        activity.startActivityForResult(intent, requestCode)
    }
}

class MainActivity : AppCompatActivity() {
    // ...
    
    companion object: ActivityFactory() {
        override fun getIntent(context: Context): Intent {
            Intent(context, MainActivity::class.java)
        }
    }
}

 

함수 이름만 보면 무엇을 하는 함수인지 잘 모를 수 있으며, 다음과 같은 이름들이 많이 사용됩니다.

 

  • from: 파라미터를 하나 받고, 같은 타입의 인스턴스 하나를 리턴하는 타입 변환 함수를 나타낸다.
val data: Date = Date.from(instant)
  • of: 파라미터를 여러 개 받고, 이를 통합해서 인스턴스를 만들어주는 함수를 나타낸다.
val faceCards: Set<Rank> = EnumSet.of(JACK, QUEEN, KING)
  • valueOf: from 또는 of와 비슷한 기능을 하면서도, 의미를 조금 더 쉽게 읽을 수 있게 이름을 붙인 함수이다.
val prime: BigInteger = BigInteger.valueOf(Integer.MAX_VALUE)
  • instance 또는 getInstance: 싱글턴으로 인스턴스 하나를 리턴하는 함수이다.
val luke: StackWalker = StackWalker.getInstance(options)
  • createInstance 또는 newInstance: 싱글턴이 적용되지 않은 getInstance로, 호출할 때마다 새로운 인스턴스를 리턴한다.
val newArray = Array.newInstance(classObject, arrayLen)
  • getType: getInstance처럼 동작하지만, 팩토리 함수가 다른 클래스에 있을 때 사용한다(타입은 팩토리 함수에서 리턴하는 타입).
val fs: FileStore = Files.getFileStore(path)
  • newType: newInstance처럼 동작하집만, 팩토리 함수가 다른 클래스에 있을 때 사용한다(타입은 팩토리 함수에서 리턴하는 타입).
val br: BufferedReader = Files.newBufferedReader(path)

 

 

 

 

확장 팩토리 함수

 

companion 객체를 직접 수정할 수는 없고, companion 객체의 함수처럼 사용할 수 있는 팩토리 함수를 만들어야 할 경우에는 확장 함수를 활용하면 됩니다.

// 교체할 수 없는 Tool 인터페이스
interface Tool {
    companion object { /*...*/ }
}

// 확장 함수 정의
fun Tool.Companion.createBigTool( /*...*/ ): BigTool {
    // ...
}

// 호출
Tool.createBigTool()

 

 

 

 

톱레벨 팩토리 함수

 

대표적인 예로 listOf, setOf, mapOf가 있습니다.

예를 들어 안드로이드에서는 액티비티를 시작하기 위해서, 인텐트(intent)를 만드는 함수를 정의해서 사용합니다.

class MainActivity: Activity {
    companion object {
        fun getIntent(context: Context) =
            Intent(context, MainActivity::class.java)
        }
    }
}

 

 

 

 

가짜 생성자

 

코틀린의 생성자는 톱레벨 함수와 같은 형태로 사용되며, 다음과 같이 톱레벨 함수처럼 참조될 수 있습니다.

class A
val a = A()

val reference: ()->A = ::A

일반적인 관점에서 대문자로 시작하면 생성자, 소문자로 시작하면 함수인 것으로 구분합니다.

예를 들어 List와 MutableList는 인터페이스지만, 생성자처럼 사용할 수 있습니다.

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

public inline fun <T> MutableList(size: Int, init: (index: Int) -> T): MutableList<T> {
    val list = ArrayList<T>(size)
    repeat(size) { index -> list.add(init(index)) }
    return list
}

이러한 톱레벨 함수는 생성자처럼 보이고, 생성자처럼 작동합니다.

이것을 가짜 생성자(fake constructor)라고 부르며, 진짜 생성자 대신 가짜 생성자를 만드는 이유는 다음과 같습니다.

 

  • 인터페이스를 위한 생성자를 만들고 싶을 때
  • reified 타입 아규먼트를 갖게 하고 싶을 때

 

 

 

 

팩토리 클래스의 메서드

팩토리 클래스는 상태를 가질 수 있다는 특징 때문에 팩토리 함수보다 다양한 기능을 갖습니다.

이를 활용하면 다양한 종류로 최적화하고, 다양한 기능을 도입할 수 있습니다.

예를 들어 캐싱을 활용하거나, 이전에 만든 객체를 복제해서 객체를 생성하는 방법으로 객체 생성 속도를 높일 수 있습니다.

data class Student(
    val id: Int
    val name: String, 
    val surname: String
)

class StudentsFactory {
    var nextId = 0
    fun next(name: String, surname: String) = Student(nextId++, name, surname)
}

val factory = StudentsFactory()
val s1 = factory.next("Marcin", "Moskala")
println(s1) // Student(id=0, name="Marcin", surname="Moskala")
val s2 = factory.next("Igor", "Wojda")
println(s2) // Student(id=1, name="Igor", surname="Wojda")

 

 

 

 


정리

 

  • 코틀린은 팩토리 함수를 만들 수 있는 다양한 방법들을 제공한다.
  • 객체를 생성할 때는 이러한 방법들의 여러 특징들을 잘 파악하고 신중하게 사용해야 한다.
  • 팩토리 함수를 정의하는 가장 일반적인 방법은 companion 객체를 사용하는 것이며, 이 방식은 자바 정적 팩토리 메서드 패턴과 유사하여 안전하고 익숙하다.
반응형