생성자 대신 팩토리 함수를 사용하라

  • 팩토리 함수: 생성자 역할을 대신 해주는 함수
  • 팩토리 함수의 장점
    • 생성자와 다르게, 함수에 이름을 붙일 수 있다.
    • 생성자와 다르게, 함수가 원하는 형태의 타입을 리턴할 수 있다. 또한 인터페이스 뒤에 실제 객체의 구현을 숨길 수 있다.
    • 생성자와 다르게, 호출될 때마다 새 객체를 만들 필요가 없다.
      • 싱글턴 패턴, 캐싱 메커니즘
    • 팩토리 함수는 아직 존재하지 않는 객체를 리턴할 수도 있다.
    • 객체 외부에 팩토리 함수를 만들면, 그 가시성을 원하는 대로 제어할 수 있다.
    • 팩토리 함수는 인라인으로 만들 수 있으며, 그 파라미터들을 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은 다음과 같은 것을 표현하는 경우에 유용하다.
    • 복잡한 자료 구조
    • 계층적인 구조
    • 거대한 양의 데이터