[Kotlin] 팩토리 함수

hxeyexn·2024년 3월 3일

[Kotlin]

목록 보기
6/6

지난 포스팅에서 간략히 정리했던 팩토리 함수에 대해 자세히 알아보자!

클라이언트가 클래스의 인스턴스를 만들게 하는 가장 일반적인 방법은 무엇일까?
바로 기본 생성자를 사용하는 방법이다.

class Crew {
	val nickname: String
    
    constructor(email: String) { // 부 생성자
    	nickname = email.substringBefore('@')
    }
    
    constructor(crewId: Int) { // 부 생성자
    	nickname = getName(crewId)
    }
}

클라이언트는 기본 생성자(주 생성자, 부 생성자)를 사용해 클래스의 인스턴스를 만들 수 있다.


🤔 위 로직을 좀 더 유연하게 표현할 방법이 있을까?

클래스의 인스턴스를 생성하는 팩토리 함수를 사용하면 위 로직을 아래와 같이 좀 더 유연하게 표현할 수 있다.

class Crew private constructor(val nickname: String) {
	companion object {
      fun createAndroidCrew(email: String) =
          Crew(email.substringBefore('@'))

      fun createFrontCrew() =
          Crew(getName(crewId))
    }
}

그렇다면 팩토리 함수란 무엇일까?

팩토리 함수란?

생성자의 역할을 대신 해 주는 함수

팩토리 함수

개발자가 구성한 Static Method를 통해 간접적으로 생성자를 호출하는 객체를 생성한다.

Kotlin은 static 키워드가 없다. 따라서 동반 객체를 사용해 팩토리 함수를 정의하는 것이 가장 일반적인 방법이다.

자바의 정적 팩토리 메서드 패턴과 굉장히 유사하다.

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

이런 제안을 하는 이유는 다음에 소개할 팩토리 함수의 장점 때문이라고 한다.


팩토리 함수의 장점

생성자와 다르게, 함수에 이름을 붙일 수 있다.

ArrayList(3) vs ArrayList.withSize(3)

ArrayList(3)은 3의 의미를 전혀 알 수가 없다. ArrayList.withSize(3)와 같이 함수에 이름을 붙여 3의 의미를 이해하기 쉽게 만들어준다.
또한 동일한 파라미터 타입을 갖는 생성자의 충돌도 줄일 수 있다.


함수가 원하는 형태의 타입을 리턴할 수 있다.

팩토리 함수는 생성자와 다르게 함수가 원하는 형태의 타입을 리턴할 수 있다. 즉, 다른 객체를 생성할 때 사용할 수 있다는 것이다.

인터페이스 뒤에 실제 객체의 구현을 숨길 때도 유용하다.
이는 listOf에서도 확인할 수 있다. listOf는 List 인터페이스를 반환한다. 실제 어떤 객체를 리턴하는 지는 플랫폼에 따라 다르다.


생성자와 달리 호출될 때마다 새 객체를 만들 필요가 없다.

함수를 사용해서 객체를 생성하면 싱글턴 패턴처럼 객체를 하나만 생성하게 강제하거나, 최적화를 위해 캐싱 메커니즘을 사용할 수도 있다.

아래 LottoNumber 클래스는 LottoNumber 인스턴스를 매번 새롭게 생성하는 대신, 한 번 생성된 인스턴스를 재사용하고 있다.

class LottoNumber private constructor(private val number: Int) {
    companion object {
        private const val MINIMUM_NUMBER = 1
        private const val MAXIMUM_NUMBER = 45
        private val NUMBERS: MutableMap<Int, LottoNumber> = mutableMapOf()

        fun from(number: Int): LottoNumber {
        	if (!value.contains(number)) {
            	NUMBERS[number] = LottoNumber(number)
            }
            
            return NUMBERS[number] ?: throw IllegalArgumentException()
        }
    }
}

또한 객체를 만들 수 없는 경우, null을 리턴하게 만들 수도 있다.

팩토리 함수는 인라인으로 만들 수 있으며 , 그 파라미터들을 reified로 만들 수 있다.

inline fun <reified T> createInstance(): T? {
    return try {
        T::class.java.newInstance()
    } catch (e: Exception) {
        null
    }
}

reified는 인라인 함수에서 타입 파라미터를 실체화하기 위한 키워드이다. 이는 타입 파라미터를 런타임에 접근 가능하게 만들어 준다.


이외에도 아래와 같은 장점이 있다.

  • 아직 존재하지 않는 객체를 리턴할 수도 있다

  • 객체 외부에 팩토리 함수를 만들면, 그 가시성을 원하는 대로 제어할 수 있다.

  • 생성자로 만들기 복잡한 객체도 만들어 낼 수 있다.


팩토리 함수 네이밍

공식 문서를 보면 아래와 같은 글을 볼 수 있다.

클래스에 대한 팩토리 함수를 선언하는 경우 클래스 자체와 동일한 이름을 지정할 수 없다.
팩토리 함수의 동작이 특별한 이유를 명확히 하기 위해 고유한 이름을 사용하는 것을 선호한다고 한다.
실제로 특별한 의미가 없는 경우에만 클래스와 동일한 이름을 사용할 수 있다.

자주 사용되는 네이밍 규칙들을 살펴보자


많이 사용 되는 네이밍 규칙

많이 사용 되는 이름으로는 from, of, valueOf, instance 등이 있다.

자주 사용했던 LocalDate의 of와 from 등도 팩토리 함수이다!!

fun test() {
    LocalDate.of(2024, 3, 4)
    LocalDate.from(ZonedDateTime.now())
}

from

  • 파라미터를 하나 받음
  • 같은 타입의 인스턴스 하나를 리턴하는 타입 변환 함수
LocalDate.from(ZonedDateTime.now())

of

  • 파라미터를 여러 개 받음
  • 이를 통합해 인스턴스를 만들어 주는 함수
LocalDate.of(2024, 3, 4)

valueOf

  • from 또는 of와 비슷한 기능을 하면서도, 의미를 조금 더 쉽게 읽을 수 있게 이름을 붙인 함수
val number = Integer.valueOf("123")

instance 또는 getInstance

  • 싱글턴으로 인스턴스 하나를 리턴하는 함수
  • 파라미터가 있을 경우, 인자를 기반으로 하는 인스턴스를 리턴
  • 일반적으로 같은 인자를 넣으면, 같은 인스턴스를 리턴하는 형태로 작동
class Crew private constructor(private val name: String) {
    companion object {
        private val instances = mutableMapOf<String, Singleton>()

        fun getInstance(name: String): Crew {
            return instances.getOrPut(name) { Crew(name) }
        }
    }
}
fun main() {
    val instance1 = Crew.getInstance("first")
    val instance2 = Crew.getInstance("second")
    val instance3 = Crew.getInstance("first")

    println(instance1) // Crew(name='first')
    println(instance2) // Crew(name='second')
    println(instance3) // Crew(name='first')

    println(instance1 === instance3)
}

createInstance, newInstance

  • getInstanc처럼 동작하지만, 싱글턴이 적용되지 않아서, 함수를 호출할 때마다 새로운 인스턴스를 만들어 리턴
class Crew(val name: String) {
    companion object {
        fun newInstance(name: String): Crew {
            return Crew(name)
        }
    }
}

fun main() {
    val instance1 = Crew.newInstance("example1")
    val instance2 = Crew.newInstance("example2")

    println(instance1) // Crew(data='example1')
    println(instance2) // Crew(data='example2')

    println(instance1 === instance2)
}

getType

  • getInstance처럼 동작하지만, 팩토리 함수가 다른 클래스에 있을 때 사용하는 이름
  • 타입은 팩토리 함수에서 리턴하는 타입
class FileStore(val filePath: String) {
    fun read() {
        println("Read: $filePath")
    }

    fun write(content: String) {
        println("Write: $filePath: $content")
    }
}

class Files {
    companion object {
        fun getFileStore(filePath: String): FileStore {
            return FileStore(filePath)
        }
    }
}

newType

  • newInstance처럼 동작하지만, 팩토리 함수가 다른 클래스에 있을 때 사용하는 이름
  • 타입은 팩토리 함수에서 리턴하는 타입

클래스를 확장해야만 한다면?

  • 동반 객체 멤버를 하위 클래스에서 오버라이드할 수 없으므로 여러 생성자를 사용해야 함

팩토리 함수 종류

팩토리 함수는 기본 생성자가 아닌 부 생성자와 경쟁 관계에 있다고 할 수 있다.

팩토리 함수 종류는 다음과 같다.

1. companion 객체 팩토리 함수

2. 확장 팩토리 함수

3. 톱레벨 팩토리 함수

4. 가짜 생성자

  • 가짜 생성자는 생성자처럼 보여야 하며, 생성자와 같은 동작을 해야 함
  • 캐싱, nullable 타입 리턴, 서브 클래스 리턴 등의 기능까지 포함해서 객체를 만들고 싶다면, 다른 팩토리 함수를 사용하는 것이 좋음

5. 팩토리 클래스의 메서드


companion 객체 팩토리 함수

추상 companion 객체 팩토리는 값을 가질 수 있다. 따라서 캐싱을 구현하거나, 테스트를 위한 가짜 객체 생성을 할 수 있다.


언제 사용하면 좋을까?

  • 생성자가 할 수 없는 한계를 맞닥뜨릴 때 팩토리 함수를 사용
  • 팩토리 함수를 사용하면 주소 값이 항상 같음, 주소 값이 같다는 건 값이 같다(동등성, 동일성 모두 보장)

📖 참고자료

Kotlin Docs - Factory functions
Kotlin in action
Effective Kotlin
정적 팩토리 메서드 패턴 (Static Factory Method)

정적 팩토리 메서드를 왜 사용하는가? 어떤 상황에 사용하는게 좋을까? (생성자와 차이)

profile
Android Developer

0개의 댓글