팩토리 함수
// 기본 생성자
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)
팩토리 함수의 장점
- 이름을 붙일 수 있다.
- ArrayList(3)보다 ArrayList.withSize(3)이 훨씬 이해하기 쉽다.
- 원하는 타입을 리턴할 수 있다.
- 예를 들어 표준 라이브러리의 listOf는 List 인터페이스를 리턴하지만, 실제론 플랫폼별로 다른 빌트인 컬렉션으로 만들어진다.
- 호출될 때마다 새 객체를 만들 필요가 없다.
- 싱글턴 패턴처럼 객체를 하나만 생성하게 강제하거나, 최적화를 위해 캐싱 메커니즘을 사용할 수 있다.
- 객체를 만들 수 없을 경우, null을 리턴하게 만들 수도 있다.
- 예를 들어 Connection.createOrNull()은 어떤 이유로 연결을 생성할 수 없을 때 null을 리턴한다.
- 아직 존재하지 않는 객체를 리턴할 수도 있다.
-
이러한 특징 때문에 어노테이션 처리를 기반으로 하는 라이브러리에서는 팩토리 함수를 많이 사용한다.
-
이를 활용하면 프로젝트를 빌드하지 않고도 앞으로 만들어질 객체를 사용하거나, 프록시를 통해 만들어지는 객체를 사용할 수 있다.
- 예를 들어 스프링에서 많이 사용하는 JPA에선 아래와 같이 UserRepository라는 인터페이스만 정의했을 뿐인데 실제로 동작한다.
@Repository
interface UserRepository : JpaRepository<User, Long> {
fun findByEmail(email: String): User?
}
- 우리가 직접 구현체를 만들지 않았는데도 스프링이 실행될 때 이 인터페이스를 구현한 객체를 만들어준다.
- 이것이 가능한 이유는
- 스프링이 컴파일 시점에 @Repository어노테이션을 처리하면서 팩토리 함수를 생성
- 이 팩토리 함수는 런타임에 실제 구현체를 동적으로 만들어서 반환하기 때문
- 가시성을 제어할 수 있다.
- 톱레벨 팩토리 함수를 같은 파일 또는 같은 모듈에서만 접근하게 만들 수 있다.
- 인라인으로 만들 수 있으며, 그 파라미터들을 reified로 만들 수 있다.
- 인라인 함수로 만들면 컴파일 후 생성되는 바이트코드에 함수 본문이 그대로 들어가 함수 호출의 오버헤드가 없어짐
- reified로 만들면 제네릭 타입의 정보를 런타임에도 사용할 수 있게 해줌
- 생성자도 만들기 복잡한 객체도 만들어 낼 수 있다.
- 생성자는 즉시 슈퍼 클래스 또는 기본 생성자를 호출해야 하지만 팩토리 함수는 원하는 때에 호출할 수 있다.
팩토리 함수의 종류
1. Companion 객체 팩토리 함수
팩토리 함수를 정의하는 가장 일반적인 방법