ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 33장 - 생성자 대신 팩토리 함수를 사용하라
    Kotlin/Effective Kotlin 요약 2023. 3. 19. 00:01

    클라이언트가 클래스의 인스턴스를 만들게 하는 다양한 방법을 알아보겠습니다.

    가장 일반적인 방법 - 생성자

    class MyLinkedList<T>{
    	val head: T,
    	val tail: MyLinkedList<T>?
    }
    
    val list = MyLinkedList(1, MyLinkedList(2, null))

     

    하지만 굉장히 다양한 생성 패턴이 존재하며, 별도의 함수를 통해 생성할 수 있습니다.

     

    팩토리 함수

    fun <T> myLinkedListof(
    	vararg elements: T
    ): MyLinkedList<T>? {
    	if(elements.isEmpty()) return null
    	val head = elements.first()
    	val elementsTail = elements.copyOfRange(1, elements.size)
    	val tail = myLinkedListof(*elementsTail)
    	return MyLinkedList(head, tail)
    }
    
    val list = myLinkedListOf(1, 2)

     

    다음과 같은 장점이 생기게 됩니다.

    • 이름을 붙일 수 있다.
    • 원하는 형태의 타입을 리턴할 수 있다
    • 새로운 객체를 생성하는 대신 싱글턴 패턴을 활용하여 객체를 하나만 생성하게 강제할 수 있다.
    • 아직 존재하지 않는 객체를 리턴할 수도 있다.
    • 가시성을 원하는 대로 제어할 수 있다.
    • 함수를 인라인으로 만들 수 있고, 파라미터를 reified로 만들 수 있다. (reified는 제너릭을 사용할 때 런타임 시 타입 정보를 알 수 있게 합니다)
    • 생성자로 만들기 복잡한 객체를 만들어낼 수 있다.
    • 원하는 때에 생성자를 호출할 수 있다

     

    물론 팩토리 함수 내부에서는 생성자를 사용해야 합니다.

    자바에서는 팩토리 패턴을 구현할 때 생성자를 private으로 만들지만, 코틀린에서는 그렇게 하는 경우가 거의 없습니다.

     

    팩토리 함수는 기본 생성자가 아닌 추가적인 생성자(secondary constructor)와 경쟁관계입니다.

     

    또한 팩토리 함수는 다른 종류의 팩토리 함수와 경쟁관계에 있습니다.

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

    Companion 객체 팩토리 함수

    팩토리 함수를 정의하는 가장 일반적인 방법으로 companion 객체를 사용하는 것입니다.

    자바 개발자라면 이 코드가 정적 팩토리 함수와 같다는 것을 쉽게 알 수 있습니다.

    C++과 같은 프로그래밍 언어에서는 이를 이름을 가진 생성자라고 부릅니다.

     

    보통 다음과 같은 이름들이 많이 사용됩니다.

    • from
      • 파라미터를 하나 받고, 같은 타입의 인스턴스 하나를 리턴하는 타입 변환 함수
    • of
      • 파라미터를 여러 개 받고, 이를 통합해서 인스턴스로 만들어 주는 함수
    • instance
      • 싱글턴 인스턴스 하나를 리턴하는 함수
    • newInstance
      • 싱글턴이 적용되지 않아 호출할 때마다 새로운 인스턴스를 만들어서 리턴하는 함수
    data class UserFactory private constructor(val name: String) { //private 생성자
        companion object {
            //이메일로 닉네임을 뽑아 User 를 생성
            fun newSubscribingUser(email:String) = UserFactory(email.substringBefore("@"))
            //id 로 User 를 생성
            fun newFacebookUser(id:Int) = UserFactory("${id}")
        }
    }
    
    fun main() {
        println(UserFactory.newSubscribingUser("bababrll@naver.com"))
        println(UserFactory.newFacebookUser(1))
    }
    //결과
    UserFactory(name=bababrll)
    UserFactory(name=1)

     

    companion 객체는 더 많은 기능을 가지고 있지만 커뮤니티에서 잘 활용되지 않습니다.

    예를 들어 companion 객체는 인터페이스를 구현할 수 있으며, 클래스를 상속받을 수 있습니다.

    코루틴 라이브러리를 살펴보면, 거의 모든 코루틴 콘텍스트의 companion 객체가 CoroutineContext.Key 인터페이스를 구현하고 있습니다.

     

     

    확장 팩토리 함수

    이미 companion 객체가 존재하며, 이를 직접 수정할 수 없다면 확장 함수를 활용할 수 있습니다.

     

    다음과 같은 Tool 인터페이스를 교체할 수 없다고 가정해 보겠습니다.

    interface UserFactory{
        companion object{
        }
    }

     

    확장함수를 정의하고 다음과 같이 호출하여 사용할 수 있습니다.

    data class UserTest(
        val name: String,
    )
    
    fun UserFactory.Companion.newSubscribingUser(email: String) : UserTest{
       return UserTest(email.substringBefore("@"))
    }
    
    fun UserFactory.Companion.newFacebookUser(id: Int) : UserTest{
        return UserTest("$id")
    }
    
    fun main() {
        println(UserFactory.newSubscribingUser("bababrll@naver.com"))
        println(UserFactory.newFacebookUser(1))
    }

    이를 활용하여 외부 라이브러리를 확장할 수 있습니다.

    하지만 companion 객체를 확장하려면 적어도 비어있는 companion 객체가 필요합니다.

     

    톱레벨 팩토리 함수

    Top Level Function이란 함수를 특정 클래스에 속하지 않도록 최상위로 선언하는 것을 의미합니다.

    static 키워드를 가진 Util 함수들을 떠올리면 좋습니다.

    톱레벨 함수의 흔한 예로 listOf, setOf, mapOf가 있습니다.

     

    public 톱레벨 함수는 모든 곳에서 사용할 수 있으므로, IDE가 제공하는 팁을 복잡하게 만드는 단점이 있습니다.

    또한 네이밍을 클래스 메서드 이름처럼 짓는다면, 다양한 혼란이 일어날 수 있기 때문에 이름을 신중하게 지어야 합니다.

    fun newSubscribingUser(email: String): UserTest{
        return UserTest(email.substringBefore("@"))
    }
    
    fun newFacebookUser(id: Int): UserTest{
        return UserTest("$id")
    }
    
    fun main() {
        println(newSubscribingUser("bababrll@naver.com"))
        println(newFacebookUser(1))
    }

     

     

    가짜 생성자

    코틀린의 생성자는 톱레벨 함수와 같은 형태로 사용됩니다.

    class A
    val a = A()

     

    아래와 같은 톱레벨 함수는 생성자처럼 보이고 생성자처럼 작동합니다.

    하지만 팩토리 함수와 같은 장점을 갖습니다.

    public inline fun <T> List(
    	size: Int,
    	init: (index: Int) -> T
    ): List<T> = MutableList(size, init)

    이를 통해 인터페이스를 위한 생성자를 만들 수 있으며, reified 타입 아규먼트를 갖게 할 수 있습니다.

     

    가짜 생성자를 선언하기 위해 invoke 연산자를 가지는 companion 객체를 사용할 수 있지만 아이템 12의 연산자 오버로드를 할 때는 의미에 맞게 하라는 원칙에 위배되기 때문에 추천되지 않습니다.

     

    invoke 연산자는 이름이 없이 호출할 수 있도록 만들어줍니다.

    class UserFactory{
        operator fun invoke(email: String): UserTest{
            return UserTest(email.substringBefore("@"))
        }
    }
    
    data class UserTest(
        val name: String
    )
    
    fun main() {
        val userFactory = UserFactory()
        println(userFactory("bababrll@naver.com"))
    }

     

    팩토리 클래스의 메서드

    팩토리 클래스는 클래스의 상태를 가질 수 있다는 특징 때문에 팩토리 함수보다 다양한 기능을 갖습니다.

     

    다음은 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)
    }

     

    프로퍼티를 통해 nextId를 증가시키면서 학생을 생성할 수 있습니다.

    이외에도 캐싱을 활용하거나 이전에 만든 객체를 복제해서 생성하는 방식으로 객체 생성 속도를 높일 수 있습니다.

    댓글

Designed by Tistory.