클라이언트가 클래스의 인스턴스를 만들게 하는 가장 일반적인 방법은 기본 생성자를 사용하는 방법이다. 생성자가 객체를 만들 수 있는 유일한 방법은 아니며 굉장히 다양한 생성 패턴이 만들어져 있다.
예를 들어 다음 코드의 톱레벨 함수는 MyLinkedList
클래스의 인스턴스를 만들어서 제공한다고 해보자.
fun <T> mLinkedListOf(
vararg elements: T
): MyLinkedList<T>? {
if(elements.isEmpty()) return null
val head = elements.first()
val elementsTail = elements.copyOfRange(1, elements.size)
val tail = mLinkedListOf(*elementsTail)
return MyLinkedList(head, tail)
}
// use
val list = myLinkedListOf(1,2)
이렇게 생성자의 역할을 대신 해주는 함수를 팩토리 함수라고 부른다. 생성자 대신 팩토리 함수를 사용하면 아래와 같은 장점이 생긴다.
생성자와 다르게 함수에 이름을 붙일 수 있다.
원하는 형태의 타입을 리턴할 수 있다. 인터페이스 뒤에 실제 객체의 구현을 숨길 때 유용하게 사용할 수 있다. ex) listOf()
호출될 때마다 새로운 객체를 만들 필요가 없다. 싱클턴 패턴처럼 객체를 하나만 생성하게 강제가능하거나 최적화릘 위해 캐싱 메커니즘을 사용할 수도 있다. 혹은 null로 반환도 가능 ex) Connection.createOrNull()
아직 존재하지 않는 객체를 리턴할 수 있다. 어노테이션 처리를 기반으로 하는 라이브러리에서는 팩토리 함수를 많이 사용한다. 앞으로 만들어질 객체를 사용하거나,프록시를 통해 만들어지는 객체를 사용할 수 있기 때문.
객체 외부에 팩토리 함수를 만들면, 그 가시성을 원하는대로 제어할 수 있다.
팩토리 함수는 인라인으로 만들 수 잇으며, 파라미터들을 reified로 만들수 있다
팩토리 함수는 생성자로 만들기 복잡한 객체도 만들어 낼 수 있다.
생성자는 즉시 슈퍼클래스 또는 기본 생성자를 호출해야 하지만, 팩토리 함수를 사용하면 원하는 때에 호출할 수 있다.
팩토리 함수는 강력한 객체 생성 방법이다. 물론 기본 생성자 또는 팩토리 함수 중에 하나를 사용해야 한다고 이해할 수 있는데, 기본 생성자를 사용하지 말라는 말은 아니다.
일반적인 자바로 팩토리 패턴을 구현할 때는 생성자를 private으로 만들지만, 코틀린에서는 그렇게 하는 경우가 거의 없다
아래는 팩토리 함수에는 어떤 것들이 있는지다.
팩토리 함수를 정의하는 가장 일반적인 방법은 companion 객체를 사용하는 것이다.
class MyLinkedList<T>(
val head: T,
val tail: MyLinkedList<T>
) {
compaion object {
fun <T> of(vararg elements: T): MyLinkedList<T> {
// ...
}
}
}
이러한 접근 방법은 인터페이스에도 구현할 수 있다.
class MyLinkedList<T>(
val head: T,
val tail: MyLinkedList<T>
) {
// ...
}
interface MyList {
// ...
compaion object {
fun <T> of(vararg elements: T): MyyhList<T> {
// ...
}
}
}
val list = MyList.of(1, 2)
val date: Date = Date.from(instant)
val faceCards: Set<Rank> = EnumSet.of(JACK, QUEEN, KING)
val prime: BigInteger = BigInteger.valueOf(Integer.MAX_VALUE)
val luke: StackWalker = StackWalker.getInstance(options)
val newArray = Array.newInstance(classObject, arrayLen)
val fs: FileStore = Files.getFileStore(path)
val br: BufferedReader = Files.newBufferedReader(path)
경험이 없는 코틀린 개발자들은 companion 객체 멤버를 단순한 정적 멤버처럼 다루는 경우가 많다.
하지만 companion 객체는 더 많은 기능을 가진다. 인터페이스를 구현하거나 클래스를 상속받을 수 있다.
abstract class ActivityFactory {
abstract fun getIntent(context: Cotnext) : 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() {
overide fun getIntent(context: Context) Intent =
Intent(context, MainActivity::class.java
}
}
//사용
val intent = MainActivity.getIntent(context)
MainActivity.start(context)
MainAcitivity.startForResult(activity, requestCode)
또한 compainon 객체 팩토리는 값을 가질수 있어서 캐싱을 구현하거나, 가짜 객체를 생성할 수 있다.
이미 companion 객체가 존재할 때, companion 객체를 직접 수정할 수는 없고 다른 파일에 함수를 만들어야 한다면 확장 함수를 활용하면 된다.
fun Tool.Companion.createBigTool( /*...*/ ) : BigTool {
// ...
}
// 사용
Tool.createBigTool()
객체를 만드는 흔한 방법 중 하나로 listOf, setOf, mapOf 등과 같은 톱레벨 팩토리 함수를 이용하는 방법이 있다.
listOf(1, 2, 3)
public 톱레벨 함수는 모든 곳에서 사용할 수 있으므로 IDE의 팁을 복잡하게 만드는 단점이 있다. 따라서 톱레벨 함수를 만들 때는 이름을 신중하게 잘 지정해야 한다.
코틀린의 생성자는 톱레벨 함수와 같은 형태로 사용된다.
class A
val a = A()
아래와 같은 톱레벨 함수는 생성자 처럼 보이고 생성자 처럼 작동한다. 하지만 팩토리 함수와 같은 모든 장점을 갖는다.
public inline fun <T> List(
size: Int,
init: (index: Int) -> T
): List<T> = MutableList(size, init)
많은 개발자들이 이를 톱레벨 함수인지 잘 모르며, 이를 가짜 생성자(Fake Constructor)라고 부른다.
생성자 대신 가짜 생성자를 만드는 이유는 다음과 같다.
가짜 생성자를 선언하기 위해 invoke 연산자를 갖는 companion 객체를 사용할 수 있다.
class Tree<T> {
companion object {
operator fun <T> invoke(size: Int, generator: (Int) -> T): Tree <T>
}
}
하지만 이는 거의 사용되지 않으며 아이템 12. 연산자 오버로드를 할 때는 의미에 맞게 하라는 원칙에 위배되기 때문에 추천하지 않는다.
Tree.invoke(10) { "$it" }
invoke는 호출한다는 의미. 그래서 객체 생성과 의미가 다릅니다. 이런 식으로 연산자를 오버로드하면 원래 의미와 차이가 발생하고, 이런 방식은 톱레벨 함수로 만드는 코드보다 훨씬 복잡하다.
ex) 리플렉션: 생성자(val f:()->Tree = ::Tree
), 가짜 생성자(val f:()->Tree = ::Tree
), invoke 함수(val f:()->Tree = Tree.Companion::invoke
)의 복잡성까지 확인 가능
따라서 가짜 생성자는 톱레벨 함수를 사용하는 것이 좋다.
또한, 기본 생성자를 만들 수 없는 상황 또는 생성자가 제공하지 않는 기능(reified 타입 파라미터 등)으로 생성자를 만들어야 하는 상황에만 가짜 생성자를 사용하자.
팩토리 클래스와 관련된 추상 팩토리, 프로토타입 등의 수많은 생성 패턴이 있다.
이런 패턴 중 일부는 코틀린에서는 적합하지 않다. 점층적 생성자 패턴과 빌더 패턴은 코틀린에서는 의미가 없다.
아래는 nextId를 갖는 학생을 생성하는 팩토리 클래스이다.
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)
}
팩토리 클래스는 프로퍼티를 가질 수 있으며 이를 활용해 다양하게 최적화하거나 기능을 가질 수 있다.
코틀린은 팩토리 함수를 만들 수 있는 다양한 방법들을 제공하며 각각의 방법들은 여러 특징을 갖고 있다.
객체를 생성할때는 이런 특징을 잘 파악하고 사용해야 한다.