생성자 대신 팩토리 함수를 사용하라
- 팩토리 함수: 생성자 역할을 대신 해주는 함수
- 팩토리 함수의 장점
- 생성자와 다르게, 함수에 이름을 붙일 수 있다.
- 생성자와 다르게, 함수가 원하는 형태의 타입을 리턴할 수 있다. 또한 인터페이스 뒤에 실제 객체의 구현을 숨길 수 있다.
- 생성자와 다르게, 호출될 때마다 새 객체를 만들 필요가 없다.
- 싱글턴 패턴, 캐싱 메커니즘
- 팩토리 함수는 아직 존재하지 않는 객체를 리턴할 수도 있다.
- 객체 외부에 팩토리 함수를 만들면, 그 가시성을 원하는 대로 제어할 수 있다.
- 팩토리 함수는 인라인으로 만들 수 있으며, 그 파라미터들을 reified로 만들 수 있다.
- 팩토리 함수는 생성자로 만들기 복잡한 객체도 만들어 낼 수 있다.
- 팩토리 함수를 사용하면, 즉시 기본 생성자를 호출할 필요가 없어진다.
- 상속 관계에서 슈퍼클래스의 생성자가 필요한 경우
- 슈퍼클래스도 팩토리 함수를 만들어서 사용해야된다.
Companion 객체 팩토리 함수
- companion 객체를 사용하는 방법
class MyLinkedList<T>(
val head: T,
val tail: MyLinkedList<T>?
) {
companion object {
fun <T> of(vararg elements: T): MyLinkedList<T> {
/* */
}
}
}
- 이름을 가진 생성자라고 부른다.
- 이름 규칙
from
: 파라미터를 하나 받고, 같은 타입의 인스턴스 하나를 리턴of
: 파라미터를 여러 개 받고, 이를 통합해서 인스턴스를 만들어주는 함수valueOf
:from
또는of
와 비슷한 기능을 하면서도, 의미를 좀 더 쉽게 읽을 수 있게 이름을 붙인 함수instance
또는getInstance
: 싱글턴으로 인스턴스 하나를 리턴createInstance
또는newInstance
: 싱글턴이 적용되지 않아서, 함수를 호출할 때 마다 새로운 인스턴스를 만들어서 리턴한다.getType
:getInstance
처럼 동작하지만, 팩토리 함수가 다른 클래스에 있다.newType
:newInstnace
처럼 동작하지만, 팩토리 함수가 달느 클래스에 있을 때 사용한다.
- companion 객체는 인터페이스를 구현할 수 있다.
- companion 객체 팩토리는 값을 가질 수 있다.
- 캐싱이나, 테스트를 위한 가짜 객체 생성을 할 수 있다.
확장 팩토리 함수
- 다른 파일에 함수를 만들어야 하는 경우 사용할 수 있다.
- 팩토리 메서드를 확장하려면 적어도 비어있는 컴패니언 객체가 필요하다.
fun Tool.Companion.createBigTool() : BigTool {
// ...
}
interface Tool {
companion object
}
톱레벨 팩토리 함수
- 객체를 만드는 흔한 방법 중 하나
listOf
,setOf
,mapOf
등
- public 톱레벨 함수는 모든 곳에서 사용할 수 있으므로, IDE가 제공하는 팁을 복잡하게 만드는 단점이 있다.
- 톱레벨 함수의 이름을 클래스 메서드 이름처럼 만들면, 다양한 혼란을 일으킬 수 있다.
가짜 생성자
- 코틀린의 생성자는 톱레벨 함수와 같은 형태로 사용된다.
- 따라서 다음과 같이 톱레벨 함수처럼 사용될 수 있다.
class A
val reference: () -> A = ::A
- 톱레벨 함수를 대문자로 시작하게 만들어, 생성자 처럼 보이고 작동하게 만들 수 있다.
- 팩토리 함수와 같은 장점을 가지지만 이 함수가 톱레벨 함수인지 모를 수 있다는 특징이 있다.
- 가짜 생성자를 만드는 이유
- 인터페이스를 위한 생성자를 만들고 싶을 때
- reified 타입 아규먼트를 갖게 하고 싶을 때
- 가짜 생성자는 진짜 생성자처럼 동작해야 한다.
- 캐싱, nullable 타입 리턴, 서브 클래스 리턴 등의 기능을 포함해서 객체를 만들고 싶다면, 팩토리 함수를 사용하는 것이 좋다.
팩토리 클래스의 메서드
- 일부 팩토리 클래스 관련 패턴은 코틀린에서 적합하지 않다.
- 점층적 생성자 패턴, 빌더 패턴 등
- 팩토리 클래스는 클래스의 상태를 가질 수 있다는 특징 때문에 팩토리 함수보다 다양한 기능을 갖는다.
기본 생성자에 이름 있는 옵션 아규먼트를 사용하라
- 기본 생성자가 좋은 방식인 이유를 이해하려면, 일단 생성자와 관련된 자바 패턴들을 이해하는 것이 좋다.
- 점층적 생성자 패턴
- 빌더 패턴
점층적 생성자 패턴
- 점층적 생성자 패턴: 여러 가지 종류의 생성자를 사용해서 프로퍼티의 기본값을 정의하는 패턴.
class Pizza {
val size: String,
val cheese: Int,
val olives: Int,
val bacon: Int
constructor(size: String, cheese: Int, olives: Int, bacon: Int) {
this.size = size
this.cheese = cheese
this.olives = olives
this.bacon = bacon
}
constructor(size: String, cheese: Int, olives: Int): this(size, cheese, olives, 0)
constructor(size: String, cheese: Int): this(size, cheese, 0)
constructor(size: String): this(size, 0)
}
- 이름있는 디폴트 아규먼트가 점층적 생성자 패턴보다 좋은 이유
- 파라미터들의 값을 원하는 대로 지정할 수 있다.
- 아규먼트를 원하는 순서로 지정할 수 있다.
- 명시적으로 이름을 붙여서 아규먼트를 지정하므로 의미가 훨씬 명확하다.
빌더 패턴
- 빌더 패턴을 사용하는 것보다 이름 있는 파라미터를 사용하는 것이 좋은 이유
- 디폴트 아규먼트가 있는 생성자 또는 팩토리 메서드가 빌더 패턴보다 구현하기 쉽다.
- 객체가 어떻게 생성되는지 확인하고 싶을 때, 빌더 패턴은 여러 메서드들을 확인해야 한다.
- 기본 생성자는 기본적으로 언어에 내장된 개념이라 이해하기 쉽다.
- 코틀린의 함수 파라미터는 항상 immutable하다. 하지만 대부분의 빌더 패턴에서 프로퍼티는 mutable이다.
- 코틀린에서 빌더 패턴이 더 유리한 경우
- 값의 의미를 묶어서 지정할 때
- 팩토리 메서드로 사용할 수 있다. 팩토리 메서드를 기본 생성자처럼 사용하게 만들려면, 커링을 활용해야 하지만 코틀린은 커링을 지원하지 않는다.
- 값의 의미를 묶어서 지정할 때
팩토리 메서드를 기본 생성자처럼 사용하게 만든다는 것이 무슨 의미일까?
- 일반적으로 빌더 패턴이 필요한 경우 DSL 빌더를 사용한다.
- DSL 빌더를 활용하는 패턴이 전통적인 빌더 패턴보다 훨씬 유연하고 명확하다.
- 코틀린에서 빌더 패턴이 더 유리한 경우는 실무에서 거의 보기 힘든 형태의 코드다.
복잡한 객체를 생성하기 위한 DSL을 정의하라
- DSL은 복잡한 객체, 계층 구조를 갖고 있는 객체들을 정의할 때 굉장히 유용하다.
- 코틀린 DSL은 type-safe이므로, 여러 가지 유용한 힌트를 활용할 수 있다.
사용자 정의 DSL 만들기
- 함수 타입을 만드는 기본적인 방법
- 람다 표현식
- 익명 함수
- 함수 레퍼런스
- 확장 함수의 경우 ‘리시버를 가진 함수 타입’을 가진다.
val myPlus: Int.(Int)->Int = fun Int.(other: Int) = this + other
- 리시버를 가진 람다 표현식을 사용하면, 스코프 내부에
this
키워드가 확장 리시버를 참조하게 된다.
val myPlus: Int.(Int)->Int = { this + it }
- 리시버를 가진 익명 확장 함수와 람다 표현식 호출 방법
- 일반 객체처럼
invoke
메서드 사용:myPlust.invoke(1, 2)
- 확장 함수가 아닌 함수처럼 사용:
myPlus(1, 2)
- 일반적인 확장 함수처럼 사용:
1.myPlus(2)
- 일반 객체처럼
언제 사용해야 할까?
- DSL은 여러 종류의 정보를 표현할 수 있지만, 사용자 입장에서는 이 정보가 어떻게 활용되는지 명확하지는 않다.
- DSL의 복잡한 사용법은 익숙하지 않은 사람에게 혼란을 줄 수 있다.
- DSL은 다음과 같은 것을 표현하는 경우에 유용하다.
- 복잡한 자료 구조
- 계층적인 구조
- 거대한 양의 데이터