kyucumber
전체 글 보기

이펙티브 코틀린 아이템 33. 생성자 대신 팩토리 함수를 사용하라

클라이언트가 클래스의 인스턴스를 만들게 하는 가장 일반적인 방법은 기본 생성자를 사용하는 방법이다. 생성자가 객체를 만들 수 있는 유일한 방법은 아니며 굉장히 다양한 생성 패턴이 만들어져 있다.

생성자의 역할을 대신 해주는 함수를 팩토리 함수라고 부른다. 생성자 대신 팩토리 함수를 사용하면 아래와 같은 장점이 생긴다.

  • 생성자와 다르게 함수에 이름을 붙일 수 있다. 객체가 생성되는 방법과 아규먼트로 무엇이 필요한지 이름으로 설명할 수 있다.
  • 생성자와 다르게, 함수가 원하는 형태의 타입을 리턴할 수 있다.
  • 생성자와 다르게 호출될 때마다 새로운 객체를 만들 필요가 없다. 싱글턴 패턴처럼 객체를 하나만 생성하게 강제하거나 캐싱 메커니즘을 사용할 수도 있다.
  • 팩토리 함수는 아직 존재하지 않는 객체를 리턴할 수도 있다. 프로젝트를 빌드하지 않고도 앞으로 만들어질 객체를 사용하거나, 프록시를 통해 만들어지는 객체를 사용할 수 있다.
  • 객체 외부에 팩토리 함수를 만들면, 그 가시성을 원하는 대로 제어할 수 있다.
  • 팩토리 함수는 인라인으로 만들 수 있으며, 그 파라미터들을 reified로 만들 수 있다.
  • 팩토리 함수는 생성자로 만들기 복잡한 객체도 만들어 낼 수 있다.
  • 생성자는 슈퍼클래스 또는 기본 생성자를 호출해야 하지만 팩토리 함수를 사용하면 원하는 때에 생성자를 호출할 수 있다.

위와 같은 장점이 있다고 해서 팩토리 함수를 사용하고 기본 생성자를 사용하지 말라는 것은 아니다. 팩토리 함수 내부에서는 생성자를 사용해야 한다.

팩토리 함수는 기본 생성자가 아닌 secondary constructor와 경쟁 관계이다.

팩토리 함수를 정의하는 방법은 아래와 같다.

  • companion 객체 팩토리 함수
  • 확장 팩토리 함수
  • 톱레벨 팩토리 함수
  • 가짜 생성자
  • 팩토리 클래스의 메소드

companion 객체 팩토리 함수

팩토리 함수를 정의하는 가장 일반적인 방법은 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)

팩토리 함수의 네이밍에는 아래와 같은 이름들이 많이 사용된다.

  • from: 파라미터를 하나 받고, 같은 타입의 인스턴스 하나를 리턴하는 타입 변환 함수

    val date: 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 객체는 더 많은 기능을 가진다. 인터페이스를 구현하거나 클래스를 상속받을 수 있다.

예를 들어 코틀린의 코루틴 내부에서는 코루틴 컨텍스트의 companion 객체들이 CoroutineContext.Key의 인터페이스를 구현하고 있다.

public interface ContinuationInterceptor : CoroutineContext.Element { /** * The key that defines *the* context interceptor. */ companion object Key : CoroutineContext.Key<ContinuationInterceptor> }

확장 팩토리 함수

이미 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)라고 부른다.

생성자 대신 가짜 생성자를 만드는 이유는 다음과 같다.

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

가짜 생성자를 선언하기 위해 invoke 연산자를 갖는 companion 객체를 사용할 수 있다.

class Tree<T> { companion object { operator fun <T> invoke(size: Int, generator: (Int) -> T): Tree <T> } }

이는 거의 사용되지 않으며 아이템 12. 연산자 오버로드를 할 때는 의미에 맞게 하라는 원칙에 위배되기 때문에 추천하지 않는다.

팩토리 클래스의 메소드

팩토리 클래스와 관련된 추상 팩토리, 프로토타입 등의 수많은 생성 패턴이 있다.

이런 패턴 중 일부는 코틀린에서는 적합하지 않다. 점층적 생성자 패턴과 빌더 패턴은 코틀린에서는 의미가 없다.

아래는 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) }

팩토리 클래스는 프로퍼티를 가질 수 있으며 이를 활용해 다양하게 최적화하거나 기능을 가질 수 있다.

정리

코틀린은 팩토리 함수를 만들 수 있는 다양한 방법들을 제공하며 각각의 방법들은 여러 특징을 갖고 있다.

객체를 생성할때는 이런 특징을 잘 파악하고 사용해야 한다.

Reference

  • 이펙티브 코틀린 - 프로그래밍 인사이트, 마르친 모스칼라 지음, 윤인성 옮김

개인적인 기록을 위해 작성된 글이라 잘못된 내용이 있을 수 있습니다.

오류가 있다면 댓글을 남겨주세요.