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

woga·2023년 9월 9일
0

코틀린 공부

목록 보기
36/54
post-thumbnail

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

예를 들어 다음 코드의 톱레벨 함수는 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 객체 팩토리 함수
  • 확장 팩토리 함수
  • 톱레벨 팩토리 함수
  • 가짜 생성자
  • 팩토리 클래스의 메소드

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

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

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

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

가짜 생성자를 선언하기 위해 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)
}

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

정리

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

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

profile
와니와니와니와니 당근당근

0개의 댓글