본문 바로가기
Engineering/SW Design

[디자인 패턴] 생성패턴 - 빌더

by 쿨쥰 2023. 5. 25.

생성패턴 - 빌더


이전 글 : 생성패턴 - 추상 팩토리

2023.05.24 - [Engineering/SW Design] - [디자인 패턴] 생성패턴 - 추상 팩토리

 

[디자인 패턴] 생성패턴 - 추상 팩토리

생성패턴 - 추상 팩토리 이전 글 : 생성패턴 - 팩토리 메서드 2023.05.22 - [Engineering/SW Design] - [디자인 패턴] 생성패턴 - 팩토리 메서드 [디자인 패턴] 생성패턴 - 팩토리 메서드 생성패턴 - 팩토리 메

skidrow6122.tistory.com

 

 

요약

복잡한 객체들을 단계별로 생성할 수 있도록 한다.

같은 제작 코드를 사용하여 객체의 다양한 유형들과 표현을 찍어 낼 수 있는 생성패턴이다.

즉, 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법이라고도 할 수 있다.

  • 복잡한 객체를 생성하는 클래스와, 표현하는 클래스를 분리
  • 동일한 절차에서도 서로 다른 표현을 생성하는 방법을 제공
  • 생성해야하는 객체가 optional 한 속성을 많이 가질 때 유리하다

 

키워드 : 단계별 / 생성 클래스와 표현 클래스의 분리 / 다수의 optional 속성 / builder / Concrete builder / Director

 

 

문제

많은 필드와 중첩된 객체들을 힘들게 단계별로 초기화 해야하는 복잡한 객체인 House 를 생각해보자.

집을 지으려면 바닥을 만들고, 벽을 몇개쌓고, 창문도 뚫고 지붕도 얹혀야 한다. 여기에 정원도 만들고 다른 부가시설들도 계속 들어온다고 가정해보자.

이때 쉽게 생각할 수 있는 접근법으로는 점층적 생성자 패턴이 있다.

이는 옵셔널 매개변수가 0개인 생성자 부터, 모든 옵셔널 매개변수를 받는 생성자까지 계속해서 점층적으로 늘려가는 방식이다.

이는 물론 일반적인 개발시 많이 사용되는 방식이지만 속성의 수가 너무 많아지면, 클래스를 호출하는 클라이언트단 코드 가독성이 떨어지며 코딩 휴먼에러의 발생확률도 높아지기 마련이다.

점층적 생성자 패턴의 단점 : 모든 매개변수가 항상 필수는 아니라는 것

보통 대부분의 속성은 사용되지 않기 때문에 생성자 호출부의 코드는 매우 못생겨 질 것이다.

예를 들어 수영장은 극소수의 집에만 있을 것이므로, 수영장 관련 속성은 모든 호출부에서 사용되지 않고 무조건 null 처리 될 것이다.

 

 

해결책

빌더 패턴은 자신의 클래스에서 객체 생성 코드를 추출하여 builders 라는 별도의 객체들로 이동하도록 한다.

이는 복잡한 객체들을 일련의 단계 순 (buildWalls, buildDoor..) 으로 생성할 수 있도록 정리하며, 객체를 생성하고 싶으면 위 단계들을 builder 객체에 실행하면 된다.

여기서 가장 중요한 메리트는 모든 단계를 호출할 필요가 없으므로, 만들고 싶은 객체의 특정 설정을 제작하는데 필요한 단계들만 적절히 골라서 호출하면 된다는 점이다.

빌더는 제품이 생성되는 동안 다른 객체들이 제품에 접근하는 것을 허용하지 않는다

[디렉터 클래스]

House를 생성하는 데 필요한 빌더 단계들에 대한 일련의 호출을 디렉터 라는 별도의 클래스로 추출해둘 수 도 있다.

디렉터 클래스는 제작 단계들을 실행하는 순서를 정의하는 반면 빌더는 이러한 단계들에 대한 구현을 제공한다.

물론 디렉터 클래스를 포함하는 것이 필수사항은 아니지만, 디렉터 클래스가 별도로 없다면 클라이언트 코드에서 생성 단계들을 직접 특정 순서에 맞추어 매번 다른 패턴에 맞게 호출해줘야 하는 코드 결합도가 생겨버릴 수 도 있다.

따라서 디렉터 클래스의 배치는 아키텍쳐 적으로 아래의 더 좋은 메리트를 제공한다.

  • 다양한 생성 루틴들을 디렉터 클래스에 몰아서 배치하여 프로그렘 전체에서 재사용 할 수 있는 좋은 장소가 됨
  • 클라이언트 코드에서 제품 생성의 세부 정보를 완전히 숨김
    • 클라이언트는 단지 빌더를 디렉터와 연관시키고 생성을 시행한 후 빌더로 부터 결과를 얻기만 하면 끝

 

 

구조

 

 

예제 클래스 다이어그램

 

 

Hands on

  • 디렉터 클래스의 구현까지는 생략하고, 간단히 kotlin 의 data class 를 활용하여 빌더 패턴으로 객체를 생성 해 본다.
class Car private constructor(
    val brandName: String,
    val modelName: String,
    val trimName: String,
    val plateNumber: String,
    val specialty: String
) {
    data class Builder(
        var brandName: String = "",
        var modelName: String = "",
        var trimName: String = "",
        var plateNumber: String = "",
        var feature: String = ""
    ) {
        fun getBrandName(brandName: String) = apply { this.brandName = brandName }
        fun getModelName(modelName: String) = apply { this.modelName = modelName }
        fun getTrimName(trimName: String) = apply { this.trimName = trimName }
        fun getPlateNumber(plateNumber: String) = apply { this.plateNumber = plateNumber }
        fun getFeature(feature: String) = apply { this.feature = feature }
        fun build() = Car(brandName, modelName, trimName, plateNumber, feature)
    }
}
  • Car 라는 객체를 찍어내는 빌더 클래스이다.
  • 각 매개변수의 필요 조합을 일일히 consturctor 를 활용해서 다 정의하는 것보다 훨씬 간결하게 Bulder 라는 하위 data class 로 정의 되었다.

 

fun carClient() {
    val sportCar: Car = Car.Builder()
        .getBrandName("포르쉐")
        .getModelName("911")
        .getFeature("오픈 에어링")
        .build()

    val suv: Car = Car.Builder()
        .getBrandName("현대")
        .getModelName("GV80")
        .getTrimName("380")
        .getPlateNumber("223가9999")
        .getFeature("많이 좋아 짐")
        .build()
}
  • 같은 빌더 데이터 클래스를 활용해서 각기 다른 형태의 객체를 만들었다.

 

 

[ 코틀린에서의 빌더 패턴 ]

사실, 코틀린에서는 생성자 선언 부에서 초기값을 선언 할 수도 있고, 객체 생성 시점에 호출되는 init 메서드를 잘 섞어 쓰면 잘못 된 변수에 대해서도 체크할 수 있으므로 빌더 패턴을 사용하지 않아도 무방하다.

다만 깔끔한 코드를 만들어내기 위해 아래와 같이 빌더 패턴을 잘 엮어 쓰자.

 

class MyDialog private constructor(
    private val title: String,
    private val text: String?,
    private val onAccept: (() -> Unit)?
) {
    class Builder(val title: String) {
        private var text: String? = null
        private var onAccept: (() -> Unit)? = null

        fun setText(text: String?): Builder {
            this.text = text
            return this
        }

        fun setOnAccept(onAccept: (() -> Unit)?): Builder {
            this.onAccept = onAccept
            return this
        }

        fun build() = MyDialog(title, text, onAccept)
    }
}
MyDialog.Builder("title")
            .setText("hello?")
            .setOnAccept { /**doSomethine*/ }
            .build()

 

 

핵심

  • 다수의 선택적 매개변수가 있는 생성자가 있을때 사용함
  • 객체들을 단계별로 생성하거나 생성 단계들을 연기하거나 재귀적으로 단계들을 실행 할 수 있음
  • 추상 팩토리는 관련된 객체들의 패밀리를 생성하는데 중점을 두어 제품을 즉시 반환하지만,
  • 빌더는 제품을 가져오기 전에 몇가지 추가 생성 단계를 실행할 수 있도록 함
  • 코틀린에서는 빌더 패턴까지 굳이 확장해서 클래스를 배치하지 않아도 Java에서의 문제를 해결 할 수 는 있으나, 패턴을 입혀서 클래스를 관리하면 좀 더 깔끔한 코딩을 할 수 있다.

 

 

패턴 접근 과정

  1. 사용할 수 있는 모든 제품 표현을 생성하기 위한 공통 생성 단계들을 명확하게 정의할 수 있는지 미리 확인

  2. Base Builder 인터페이스에서 작업 스텝을 선언

  3. 각 제품 표현에 대한 Concrete Builder 를 만들고 해당 생성 단계들을 구현생성 결과를 가져오는 메서드를 구현

  4. Director Class 를 만드는 것에 대해 고려
    - 같은 빌더 객체를 사용하여 제품을 제작하는 다양한 방법을 캡슐화할 수 있음

  5. Client 코드는 Builder 객체들과 Director 객체들을 모두 생성
    - 제작이 시작되기 전에 Client 는 Builder 객체를 Director에게 전달해야 함 
    - 일반적으로 Client는 Director의 클래스 생성자의 매개변수들을 통해 위 작업을 한 번만 수행.
    - 그 후 Director는 모든 추가 제작에서 builder 객체를 사용

  6. 모든 제품이 같은 인터페이스를 따르는 경우에만 Director 로부터 직접 생성 결과를 얻을 수 있음
    - 그렇지 않으면 Client는 Builder에서 결과를 가져와야 함.

 


간단히 hands on 을 통해 빌더 패턴을 구현 해 보았다.

사실 디렉터 클래스를 활용해서 더 많은 조합을 예제로 꾸며보면 더 좋았을텐데, 일단 이정도로 마무리하고 아래 블로그에서 Java 기준의 빌더 패턴을 거의 정석에 가깝게 구현한 예제를 볼 수 있으므로 정리만 해둔다.

https://dev-youngjun.tistory.com/197

댓글