전공/Design Pattern

Design Pattern - Builder Pattern

ProgYun. 2023. 10. 9. 18:58

QueryDSL에서의 빌더패턴의 예시

Member findMember = queryFactory.select(m)  
        .from(m)  
        .where(m.username.eq("member1"))  
        .fetchOne();

쿼리 DSL에서 비슷한 패턴으로 JPQL을 편리하게 이용하는 과정을 목도했는데 Method Chaining이 다소 생소하긴 했는데 이번에 빌더패턴을 공부하게 되면서 좀 더 깊이있는 사고를 하게 된 것 같다.

(정확히 말하면 DSL은 메서드체이닝 방식을 이용한거고,
빌더패턴도 메서드 체이닝을 이용해서 구현한 전략이긴 하다.)

Builder Pattern

동일한 프로세스를 거쳐서 다양한 구성의 인스턴스를 만드는 방법으로 복잡한 객체를 만드는 프로세스를 독립적으로 분리할 수 있다는 장점이 있다.

Pasted image 20231009175731.png

이번에 진행한 실습을 클래스 다이어그램으로 간단하게 나타내보았다

TourDirector로 반복되는 호출에 관한 데이터 스택을 따로 숨길 수 있고
여러개의 DefaultTourBuilder의 구현을 통해 다양한 구성의 인스턴스를 만들 수 있다

여기서는 TourPlan에서 여러가지 경우의 수에 따라 다른 DefaultTourBuilder를 생성함으로써 여러 생성자를 만들어야하는 경우 생성자의 길이가 늘어나거나 검증 로직을 별도로 추가할 수 없는(너무 복잡해짐 + SRP 위반) 한계를 마주하는데 빌더패턴으로써 이 한계점을 돌파할 수 있다.

왜 TourPlanBuilder 인터페이스의 메서드 반환형은 TourPlanBuilder인가?

Method Chaining을 통해 다음과 같은 방식으로 객체 인스턴스 필드에 값을 연속적으로, 순서대로 지정하고 특정 메서드의 호출로 완성된 객체 인스턴스를 반환받을 수 있기 때문이다.

이러한 패턴을 이용하여, 우리는 어떤 필드를 검증하거나 특정 순서대로 객체 인스턴스의 필드에 값을 배정하는 등 부가적인 기능을 담을 수 있다.

Pasted image 20231009181407.png
Pasted image 20231009183159.png
GPT에게 Product와 Builder의 객체간 관계가 포함관계에 해당하는지 궁금해져서 물어봤더니 아니라고 한다.

Pasted image 20231009183349.png
단순히 객체를 담는 필드가 클래스 내에 있다고 해서 강한 결합관계에 있다고 가정하면 큰일날 성 싶다.

빌더 패턴의 장단점

장점

  • 메서드 체이닝 방식을 사용하고 있어서 복잡한 객체를 순차적으로 만들 수 있다.
  • 복잡한 객체를 만드는 구체적인 과정을 Director 객체를 통해 추상화하여 클라이언트로부터 숨길 수 있다.
  • 동일한 프로세스를 통해 각기 다르게 구성된 객체를 만들 수 있다.
    • 세부적인 플랜의 추가는 어떤 빌더를 사용하느냐에 따라 다르게 생성할 수 있다.
    • 과정은 비슷한데 다른 인스턴스가 나오는게 가능한것에 집중하자
  • 불완전한 객체를 사용하지 못하도록 방지할 수 있다.
    • 별도의 검증로직을 메서드에 추가하여 getInstance()로 만든 인스턴스를 반환할때 별도의 검증 로직을 추가할 수 있다
    • 이는 검증 로직을 생성자에 별도로 결합할 수 없기 때문에(너무 복잡해짐) 오히려 코드의 복잡성을 떨어트리는데는 도움이 될 수 있다.

단점

  • 원하는 객체를 만들려면 빌더 클래스를 제작해주어야 한다.
  • 클래스를 여러개 더 만들어야 하기 때문에 클래스 계층구조가 원래보다 더 복잡해질 수 있다.

빌더 패턴이 쓰이는 곳

자바 8 Stream Builder API

여기서 사실 제너릭에 관련된 개념이 조금 헷갈리는게 있었다

https://chat.openai.com/share/71c8f076-9b84-4347-a74d-feed479997f5

이걸 보면 어떤 대화가 오고 갔는지 대충 볼 수 있는데, 하나만 끌어와보자면,

Pasted image 20231009183922.png

??? : 런타임에 타입 체크 자동으로 된다면서요

List<String> arrList = new ArrayList<>();

이건 되는데

Stream<String> names = Stream.<>builder.add("hello").build();

이건 왜 자동으로 inference(유추)가 안되는 거임? 왼쪽에 줬잖아요라고 질문했다.

아직도 잘 이해가 안가서 메모해놓는다…

Pasted image 20231009185549.png
하고 답을 찾았다 객체를 만드는 과정에서 메서드 체이닝이 일어나는데 메서드 체이닝 과정 전에 Type이 확정되는 것이 아니기 때문에 에러가 발생할 수 있다고 한다. 따라서 Explicit하게 별도로 타입을 명시해주어야한다고 하네요!


그 외에도 LombokBuilder, 스프링의 UriComponentsBuilder, MockMvcWebClientBuilder 등 다양한 빌더 패턴을 이용한 편의기능이 사용되고 있다.

UriComponents를 통해 UriComponentsBuilder로 부터 URI를 받아오는 URI를 문자열로 만드는것보다 안전하다 공백, 인코딩을 URI에 쓰기 안전한 형태로 반환해준다.