Developing Myself Everyday

객체를 정의하고 생성하는 방법을 지정할 때 사용하는 가장 기본적인 방법은 기본 생성자를 사용하는 것입니다.

class User(var name: Stirng, var surname: String)
val user = User("Marcin", "Moskala")

 

 

기본 생성자로 객체를 만들 때는 객체의 초기 상태를 나타내는 아규먼트를 전달합니다. 데이터 모델 객체부터 살펴봅니다.

data class Student(
    val name: String,
    val surname: String,
    val age: Int,
)

 

프로퍼티는 기본 생성자로 초기화되어도, 디폴트 값을 기반으로 초기화되어도, 어떻게든 초기화만 되면 큰 문제가 없습니다.

 

일반적으로 기본 생성자가 좋은 방식인 이유를 이해하려면, 일단 생성자와 관련된 자바 패턴들을 이해하는 것이 좋습니다.

 

 

점층적 생성자 패턴(Telescoping constructor pattern)

점층적 생성자 패턴은 `여러 가지 종류의 생성자를 사용하는` 간단한 패턴입니다. 

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)
}

 

이 코드는 좋은 코드가 아닙니다. 코틀린에서는 일반적으로 이 대신에 디폴트 아규먼트를 사용합니다.

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

 

디폴트 아규먼트는 다양한 기능을 제공합니다. 아래와 같이 초기화할 수도 있습니다.

val villagePizza = Pizza("L", olives = 3)

 

디폴트 아규먼트는 점층적 생성자 패턴에 비해 아래와 같은 장점이 있습니다.

  • 파라미터의 값을 원하는대로 지정할 수 있습니다.
  • 아규먼트를 원하는 순대로 지정할 수 있습니다.
  • 명시적으로 이름을 붙여서 아규먼트를 지정하므로 의미가 훨씬 명확합니다.

 

빌더 패턴

자바에서는 위 2개의 방식을 사용할 수 없기에 빌더 패턴을 사용합니다. 빌더 패턴을 사용하면 아래와 같은 장점이 있습니다

  • 파라미터의 값을 원하는대로 지정할 수 있습니다.
  • 디폴트 값을 지정할 수 있습니다.

코틀린에서 빌더 패턴은 아래와 같이 만들어 볼 수 있습니다.

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 olives: Int = 0
        private var bacon: Int = 0

        fun setCheese(value: Int): Builder = apply { cheese = value }
        fun setOlives(value: Int): Builder = apply { cheese = value }

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

 

빌더 패턴을 사용하는 것보다 디폴트 아규먼트를 사용하는 것이 더 좋은 이유를 간단하게 정리하면 아래와 같습니다.

  • 더 짧습니다.
  • 더 명확합니다,
  • 더 사용하기 쉽습니다.

 

다만 무조건 빌더 패턴대신 기본 생성자를 사용해야 한다는 것은 아닙니다. 

 

아래의 예시를 보면 다이얼로그의 각 속성을 순차적으로 설정하고 있습니다. 여러 메서드들을 체이닝 해서 읽기 편해졌습니다. 만약 기본 생성자를 사용한다면, 모든 값을 한 번에 전달해야 해서 복잡해질 수 있습니다.

val dialog = AlertDialog.Builder(context)
    .setMassage(R.string.Fire_missiale)
    .setPositiveButton(R.string.fire, { d, id ->
        //미사일 발사
    })
    .setNegativeButton(R.string.cancel, { d, id ->
        //취소
    })
    .create()

 

 

아래의 예시에서는 여러 개의 경로를 추가하는 과정을 자연스럽게 표현합니다. 

val router = Router.Builder()
    .addRoute(path = "/home", ::showHome)
    .addRoute(path = "/user", ::showUser)
    .build()

이렇게 여러 개의 Route를 순차적으로 추가하는 방식을 통해 값의 의미를 논리적으로 묶어서 설정하는 효과를 줍니다.

 

아래의 예시를 보면 좀 더 명확하게 이해가 됩니다. 이 방식은 개발자가 `routes`라는 리스트를 직접 만들어서 넣어야 하며, 이 객체가 여러 개의 Route 정보를 갖고 있다는 걸 신경 써야 합니다.

val router = Router(
    routes = listOf(
        Route("/home", ::showHome),
        Route("/user", ::showUser)
    )
)

반면에 빌더 패턴을 사용한 경우에는 Router 객체를 만들면서 여러 개의 Route를 자연스럽게 추가하는 흐름이 생기고, 각 값이 논리적인 그룹으로 묶이는 효과를 가지게 됩니다.

 

일반적으로 위와 같은 코드는 아래와 같이 DSL 빌더를 사용합니다.

val dialog = context.alret(R.string.fire_missiles) {
    positveButton(R.stirng.fire) {

    }
    negativeButton() {

    }
}

val route = router {
    "/home" directsTo ::showHome
    "/users" directsTo ::showUsers
}

 

 

고전적인 필더 패턴의 장점으로는 팩토리로 사용할 수 있다는 것입니다.

fun Context.makeDefaultDialogBuilder = AlertDialog.Builder(this)
    .setIcon(R.drawable.ic_Dialog)
    .setTitle(R.string.dialog_title)
    .setOnCancelListenr{it.cancel}

 

 

팩토리 메서드를 기본 생성자처럼 사용하게 만들려면, 커링을 활용해야 합니다. 다만 코틀린은 직접 커링을 지원하지 않기에 객체 설정을 데이터 클래스로 만들고, 이를 copy로 복제한 뒤, 필요한 설정들을 일부 수정해서 사용하는 형태로 만듭니다.

data class Product(val type: String, val name: String)

fun defaultProduct() = Product(type = "Electronic", name = "Default")
val myProduct = defaultProduct().copy(name = "Smartphone")

 

 

아래와 같이 고차 함수와 람다로 커링을 구현할 수는 있습니다. 이를 통해 팩토리 함수를 기본 생성자처럼 사용 가능하게 만들 수 있습니다.

data class Product(val type: String, val name: String) {
    companion object {
        fun create(type: String): (String) -> Product = { name ->
            Product(type, name)
        }
    }
}

// 사용 예시
val createElectronic = Product.create("Electronic") // (String) -> Product 반환
val myProduct = createElectronic("Smartphone") // Product("Electronic", "Smartphone")

println(myProduct) // Product(type=Electronic, name=Smartphone)

 

 

사실 이런 코드들은 실무에서 보기 어렵습니다. 예를 들어 기본 대화상자를 정의하는 경우, 함수를 사용해서 만들고, 모든 사용자 정의 요소를 옵션 아규먼트로 전달하는 방법을 사용하는 것이 좋습니다. 그래서 빌더 패턴의 장점도 빌더 패턴을 사용할 이유가 되지 못합니다.

 

결론적으로는 빌더 패턴은 아래와 같은 경우에만 사용합니다.

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



profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!