생성자처럼 생겼지만 실제로 생성자는 아니고 그냥 함수
Kotlin 과 Java 에는 생성자라는 개념이 있지만 사실 일반 함수와 다를게 없다. 유일한 차이점이라면, 일반적으로 함수의 이름은 소문자로 시작하고 생성자의 이름은 대문자로 시작하는 것 정도이다. (클래스 이름이 보통 대문자로 시작하다보니 그렇게 된 것 같다.) 코틀린에선 아래와 같은 생성자(처럼 생긴 함수)들이 있다.
List(4) { "${it}번" } // [0번, 1번, 2번, 3번]
얼핏 보면 List()
는 생성자 같지만 List 는 인터페이스 이기 때문에 생성자가 있을 수 없다. List()
는 함수이며 내부 구현은 아래와 같다.
public inline fun <T> List(
size: Int,
init: (index: Int) -> T
): List<T> = MutableList(size, init)
생성자처럼 생겼고 생성자처럼 행동하지만 실제론 팩토리 함수인 위 같은 함수들을 fake constructor(가짜 생성자) 라고 부른다. fake constructor 의 다른 대표적인 예시로 코루틴을 사용할 때 자주 쓰이는 Job()
과CoroutineScope()
도 있다.
fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)
fun CoroutineScope(context: CoroutineContext): CoroutineScope=
ContextScope(
if (context[Job] != null) context else context + Job()
)
Job 과 CoroutineScope 도 물론 인터페이스다. 하지만 새로운 Job 을 생성하거나 CoroutineScope 를 따로 생성해야할 때는 위 함수를 주로 사용한다. 그래서인지 Job 이나 CoroutineScope 는 List 와는 다르게 인터페이스인 것 조차 인지하지 못하는 경우도 있다. 코틀린 코루틴 개발자들이 Job()
, CoroutineScope()
함수를 작성하면서 의도했던 결과가 아닐까?
굳이 가짜 생성자를?
위 같은 가짜 생성자를 사용하는 이유는 여러가지가 있겠지만 제일 중요한건 가독성 향상과 추상화로 보인다. 사실 생성자를 사용하지않고 객체를 생성하는 방법에는 가짜 생성자 말고도 팩토리 함수라는 더 대중적인 형태가 있다. 자바에서부터 흔히 정적 팩토리 함수라는 이름으로 사용되어 온 패턴이고 코틀린에서는 아래 코드처럼 주로 Companion 객체로 팩토리 함수를 구현한다.
class Car private constructor(val name: String) {
companion object {
fun create(name: String): Car {
return Car(name)
}
}
}
팩토리 함수는 그냥 생성자를 이용하는 것 보다 이점이 많다.
- 생성자에 이름을 붙일 수 있다.
- 원하는 형태의 타입을 반환할 수 있다.
- 인터페이스 뒤에 실제 객체의 구현을 숨길 수 있다. (추상화)
- 생성자로는 구현하기 힘든 복잡한 로직을 구현할 수 있다.
이렇게 보니 굳이 가짜 생성자보다는 팩토리 메소드를 이용하는게 훨씬 유용해보이기도 하지만 꼭 그렇지는 않다.
특히 추상화관점에서 위에서 소개했던 Job()
과 CoroutineScope()
함수를 다시 살펴보자. 코틀린팀의 코루틴 개발자들이 위 두 함수를 팩토리 함수가 아니라 굳이 가짜 생성자 형태로 작성한 이유는 무엇이었을까? 두 함수가 가짜 생성자가 아니라 팩토리 함수 형태로 작성되었다고 생각해보자. 아마 Job.of()
혹은 Job.create()
같은 형태가 됐지 싶은데 이 형태로 기존 코드에서 사용하면 아마 아래처럼 될 것 같다.
// factory method
suspend fun main(): Unit = coroutineScope {
launch(Job.create(parentJob)) {
// some logic
}
launch(Job.create()) {
// other logic
}
}
// fake constructor
suspend fun main(): Unit = coroutineScope {
launch(Job(parentJob)) {
// some logic
}
launch(Job()) {
// other logic
}
}
가짜 생성자는 추상화와 직관성 측면에서 팩토리 함수보다 유용하다. 코루틴를 사용하는 입장의 개발자는 Job 과 CoroutineScope 가 인터페이스인 걸 굳이 알 필요는 없다. 일반 생성자처럼 Job 을 생성하고 CoroutineContext 에 집어넣어서 사용하면 된다. Job.create()
처럼 팩토리 함수로 사용해도 무관하나 이는 사용하는 개발자에게 있어서 미세하지만 인지적인 부하를 줄 수 있다. 심지어 Job 이나 CoroutineScope 를 생성하는 코드는 굉장히 자주 사용되는데, 빈도가 높아질수록 이 부하는 늘어날 것이다.
가짜 생성자는 겉으로 봤을 때는 생성자와 동일한 형태를 유지함으로써 추상화의 단계를 한층 더 높여주고 직관성 또한 높임으로서 사용자의 인지적인 부하를 줄여줄 수 있다. 그래서 만약 이와 비슷한 목적을 갖게된다면 팩토리 함수 말고도 가짜 생성자라는 선택지가 있으니 이를 활용해보는 것도 좋겠다.