외부 API를 연동하기 위해 RestClinet를 사용하던 중 아래와 같은 코드를 마주했다. 여기서 builder()가 이 글의 주제다. 결론부터 말하자면 builder 패턴은 때론 anti pattern 이 될 수 있다. 특히 POJO에선 anti pattern 으로 사용하게 될 확률이 높다. 이에 대해 Spring Framework Docs에서도 사용하고 있는데 좋은거 아니야? 라고 반문할 수 있다.
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
.baseUrl("https://example.com")
.defaultUriVariables(Map.of("variable", "foo"))
.defaultHeader("My-Header", "Foo")
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build();
Java
복사
When Builder is anti-pattern
builder 패턴이 왜 문제가 될 수 있는지를 살펴보면 해당 질문에 대한 해답을 어느 정도 얻어갈 수 있다.
아래 아티클의 내용 중 핵심을 뽑아보자면 아래와 같다.
1.
builder를 사용할 땐 npe가 터지지 않도록 모든 필드들에 default 값을 설정해라.
2.
builder는 특정 상황에서 유용하다. 예를 들어 환경 설정을 다룰 때엔, 개발자가 필요에 따라 특정 설정만을 명시적으로 변경할 수 있게 하기 위해 builder가 유용할 수 있다. (POJO 는 오히려 적합X)
Wrong Example
사실 나는 프로젝트를 할 때 builder를 애용했었다. 예전에 내가 느꼈던 builder의 장점은 필드와 생성자가 많아졌을 때 사용하는 쪽에서 어떤 필드를 어느 순서로 집어넣어야 하는지 정해, 생성자를 잘못 사용하는 실수를 줄일 수 있단 점이었다.
실제로 new Member("이름",…"전화번호", "주소")와 같이 사용할 때, 필드가 없어졌다 추가됐다를 반복하다가 new Member("이름", …"주소", "전화번호")과 같이 사용해 두 필드의 순서를 바꿔 생성하는 실수를 한적이 있었다. 이는 컴파일 타임은 커녕, 런타임에도 잡히지 않아 나아아중에 해당 데이터를 사용할 때 겨우 알아냈던 경험이 있다.
이때 프로젝트 전반적으로 Member.builder().name("이름").phone("전화번호").address("주소")와 같이 builder 패턴을 도입해 개발자의 실수를 줄이는 데 도움을 받았었는데, 지금와서 다시 생각해보면 위 케이스의 경우 builder 패턴이나, 생성자나 어차피 런타임에 잡히지 않는단 건 동일한 데 굳이..? 싶다. 인텔리제이가 친절하게 생성자 매개변수에 대한 미리보기 기능도 지원하기에 builder만이 주는 엄청난 메리트는 없었지 않나 싶다.
더 나아가 애초에 사용하는 쪽에서 어떤 필드를 어느 순서로 집어넣을지 정한다는게 객체지향관점에서 모순적이다. 순서가 뒤죽박죽이 되어 오히려 관리포인트가 늘어나는 단점이 따를 수 있다.
Conclusion
오히려 위의 Member 케이스의 경우, 객체의 각 필드에 대한 검증이 필요했던 시점 같다. 앞으론 만일 설계한 도메인 객체에 builder가 쓰고 싶다면, 객체 검증이 부족하진 않은지, 객체의 역할분담이 제대로 되었는지를 한 번 살펴봐야겠다. 참고로 글 초반부에 등장한 Docs의 예시는 환경 설정을 다루는 경우이므로 이 글에서 언급하는 anti pattern이 아니다.