안드로이드 개발자 노트
[이펙티브 코틀린] Item33. 생성자 대신 팩토리 함수를 사용하라 본문
클래스의 인스턴스를 만드는 가장 일반적인 방법은 기본 생성자(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 객체를 사용하는 것이며, 이 방식은 자바 정적 팩토리 메서드 패턴과 유사하여 안전하고 익숙하다.
'Kotlin > 이펙티브 코틀린' 카테고리의 다른 글
[이펙티브 코틀린] Item34. 기본 생성자에 이름 있는 옵션 아규먼트를 사용하라 (0) | 2023.12.24 |
---|---|
[이펙티브 코틀린] Item32. 추상화 규약을 지켜라 (0) | 2023.11.26 |
[이펙티브 코틀린] Item31. 문서로 규약을 정의하라 (0) | 2023.11.26 |