코틀린은 순수 함수형 스타일로 작성할 수도 있지만 자바처럼 OOP스타일로도 작성할 수 있다. 객체를 생성하는 방법에느 어떤 것들이 있고 어떠한 장단점이 있는지 살펴보자.

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

객체를 만드는 일반적인 방법은 생성자를 사용하는 방법이지만, 팩토리 패턴을 이용한 객체를 생성할 수도 있다.

팩토리 함수(패턴)의 특징

  • 생성자와 다르게 함수에 원하는 이름을 붙일 수 있다.
  • 인터페이스의 뒤에서 실제 객체의 구현을 숨긴 원하는 타입을 반환할 수 있다.
  • 매번 새로운 객체를 반환하는게 아니라 싱글턴처럼 단일객체를 반환하거나 캐싱 메커니즘을 적용하는 것이 가능하다.
  • 아직 존재하지 않는 객체를 반환할 수도 있다.
  • 팩토리 함수는 인라인함수로 만들 수 있으며, 그 파라미터들을 reified로 만들 수 있다.
  • 생성자로 만들기 복잡한 객체를 단순하게 만들어 낼 수 있다.

팩토리 함수를 활용한 객체 생성방식

  • Companion 팩토리 함수: companion 객체를 사용하는 일반적인 방식.
  • 확장 팩토리 함수: companion 객체를 직접 수정할 수 없는 경우 확장 함수를 이용한 팩토리 함수 선언.
  • top-level 팩토리 함수: top-level에 선언하는 팩토리 함수. 예) listOf, setOf, mapOf
  • 가짜 생성자: top-level 함수처럼 보이고 생성자처럼 사용하는 함수를 가짜생성자라고 한다.
    다음과 같은 이유로 가짜생성자를 사용한다.

    – 인터페이스를 위한 생성자를 만들 때
    – reified 타입 아규먼트를 갖게 하고 싶을 때

    기본생성자를 만들 수 없는 상황 또는 생성자가 제공하지 않는 기능(reified 타입 파라미터)으로 생성자를 만들어야 하는 상황에서만 가짜 생성자를 사용하는 것이 좋다.
  • 팩토리 클래스의 함수: 팩토리 클래스는 상태를 가질 수 있다는 특징을 갖으며, 이를 활용하여 다양한 기능 및 전략을 도입할 수 있다.

기본 생성자에 있는 이름 옵션 아규먼트를 사용하기

코틀린의 기본 생성자와 자바의 생성자를 이용한 패턴의 차이를 살펴보자.

점층적 생성자 패턴

다음의 자바 스타일의 점층적 생성자 패턴을 확인해보자

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)
    constuuctor(size: String): this(size, 0)
}

이러한 코드는 코틀린에서 다음과 같이 생성자에 기본 인자값을 사용하여 코드를 단순하고 깔끔하게 만들 수 있다.

class Pizza(
    val size: String,
    val cheese: Int = 0,
    val olives: Int = 0,
    val bacon: Int = 0
)

기본 아규먼트를 갖는 생성자가 좋은 이유는 다음과 같다.

  • 파라미터들의 값을 원하는 대로 지정 가능하다.
  • 아규먼트의 이름을 사용하여 순서와 관계 없이 지정 가능하다.
  • 이름을 명시적으로 붙이므로 명확하고, 가독성이 좋다.

빌더 패턴

일반적인 빌더패턴은 다음과 같은 장점이 있다.

  • 파라미터에 이름을 붙일 수 있다
  • 파라미터를 원하는 순서대로 지정할 수 있따.
  • 기본값을 지정할 수 있다.

다음의 코틀린으로 작성된 빌더 패턴을 확인하자.

class Pizza private constructor(
    val size: String,
    val cheese: Int,
    val olives: Int,
    val bacon: Int
){
    class Builder(private val size: String) {
        private var cheese:Int = 0
        private var olive:Int = 0
        private var bacon:Int = 0
        
        fun setCheese(value: Int): Builder = apply {
            cheese = value
        }
        fun setOlives(value: Int): Builder = apply {
            olives = value
        }
        fun setBacon(value: Int): Builder = apply {
            bacon = value
        }

        fun build() = Pizza(size, cheese, olives, bacon)
    }
}

이러한 빌더 패턴도 앞에서 언급한 코틀린 기본 인자가 있는 생성자를 활용하면 간단히 대체 할 수 있다.

val pizza = Pizza(
    val size = "L",
    val cheese = 1,
    val olives = 2,
    val bacon = 3
)

빌더 패턴보다 이름 있는 파라미터를 사용하는 것이 좋은 이유는 다음과 같다.

  • 코드가 짧다
  • 명확하다
  • 사용하기 쉽다
  • 동시성 이슈가 없다.

항상 빌더 패턴 대신 기본 생성자를 사용하는게 좋다는 이야기는 아니다. addXXX 같은 빌더 메서드를 기본생성자에서 표현하는 것은 직관적이지 못하므로 그러한 코드는 DSL 빌더를 사용하는 것이 좋다..

결론적으로 코틀린에서는 빌더 패턴을 잘 사용하지는 않지만 다음과 같은 경우에 사용할 수 있다.

  • 빌더 패턴을 사용하는 다른 언어로 작성된 라이브러리를 그대로 옮길 때
  • 기본 인자와 DSL을 지원하지 않는 다른 언어에서 쉽게 사용할 수 있게 API를 설계할 때

일반적인 프로젝트에서는 기본 생성자를 사용해 객체를 만든다

복잡한 객체를 생성하기 위한 DSL을 정의하기

DSL(Domain-Specific Language)란 특정한 도메인을 적용하는데 특화된 컴퓨터 언어를 말한다. 코틀린을 활용하면 DSL을 직접 만들 수 있다. DSL의 특징은 다음과 같다.

  • 복잡한 객체를 쉽게 정의 할 수 있다.
  • 보일러플레이트 코드가 줄어든다
  • 복잡한 내용을 숨긴다
  • 의도를 명확하게 표현한다.
  • type-safe하다.

사용자 정의 DSL 만들기

커스텀 DSL을 만드려면 리시버를 사용하는 함수 타입에 대한 개념을 이해해야 한다.

리시버를 가진 함수 타입의 가장 중요한 특징은 this의 참조 대상을 변경할 수 있다는 것이다. 리시버를 가진 함수 타입은 코틀린 DSL을 구성하는 가장 기본적인 블록이다.

DSL을 정의한다는 것은 개발자의 인지적 혼란과 성능이라는 비용이 모두 발생할 수 있다. 따라서 단순한 기능까지 DSL을 사용하는 것은 닭잡는데 소잡는 칼을 쓰는 꼴이므로 다음과 같은 경우에만 DSL을 정의하자

  • 복잡한 자료 구조
  • 계층적인 구조
  • 거대한 양의 데이터
카테고리: Kotlin

2개의 댓글

최우성 · 2022년 3월 11일 4:30 오후

잘봤습니당!!

    Charlezz · 2022년 3월 22일 6:13 오후

    감사합니다!

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.