빌더 패턴
이번 주제는 생성자 패턴 중에서 제일 유명하고 가장 많이 쓰이는 패턴인 빌더 패턴입니다.
빌더 패턴에 장점으론
- 유연한 객체 생성 과정
- 생성된 객체의 불편성 유지
- 가독성 높은 코드 작성
등이 있습니다.
빌더 패턴과 많이들 이야기 되는게 점층적 생성자 패턴과 자바빈즈 패턴이 이야기 됩니다.
여기서 점층적 생성자 패턴을 설명 드리면 생성자를 오버로딩하여 파라미터 개수를 늘려가면서 객체를 초기화 하는 방식 입니다.
public class Person {
private String name;
private int age;
private String address;
private String phone;
public Person(String name, int age) {
this(name, age, "", "");
}
public Person(String name, int age, String address) {
this(name, age, address, "");
}
public Person(String name, int age, String address, String phone) {
this.name = name;
this.age = age;
this.address = address;
this.phone = phone;
}
}
점층적 생성자 패턴의 문제점으로 생성자가 많아지는 것도 있지만 같은 타입 유형을 파라미터로 여러 개 선언 하는 경우에 발생 합니다. 바로 아래 코드와 같은 경우 입니다.
public class Person {
private String name;
private int age;
private String address;
private String phone;
public Person(String name, String address) {
this(name, 0, address, "");
}
// 위 name, address 때문에 초기화 불가능.
public Person(String name, String phone) {
this(name, 0, "", phone);
}
public Person(String name, int age, String address, String phone) {
this.name = name;
this.age = age;
this.address = address;
this.phone = phone;
}
}
String 타입인 name, address의 초기화 생성자가 있다는 가정 하에 String 타입인 name, phone 속성을 초기화 하려고 하지만 name, address 때문에 초기화가 불가능 합니다. 즉, 초기화 하려는 속성들의 타입이 중복될 여지가 있어 초기화 하는 과정이 힘들다는 문제점이 있습니다.
물론 타입들이 전부다 객체라면 문제가 되진 않겠지만 위와 같이 기본 자료형인 경우에는 충분히 발생될 수 있는 문제점입니다.
그리고 자바빈즈 패턴은 Setter 메서드를 활용하기 떄문에
- 객체 일관성 문제
- 객체 불변성 문제
- 코드 가독성 문제
등이 있습니다. 이에 대한 예제는 생략하도록 하겠습니다.
그래서 점층적 생성자 패턴의 장점과 자바빈즈 패턴의 장점만 취한 패턴이 바로 빌더 패턴 입니다.
예제
이번 예제도 기존 게시물의 연장으로 하겠습니다. 게시물의 순서는
순으로 읽어주시면 되겠습니다. 해당 순서의 게시물에 상세 설명이 있기 때문에 상세 설명을 생략하도록 하겠습니다.
이번 게시물의 예제로는 카카오톡 가입 시 쿠폰을 발급하는 예제 입니다.
검증 프로세스
회원가입 검증 프로세스의 클래스 다이어그램 입니다.
검증 인터페이스
/**
* <pre>
* 회원가입 예외 검증 프로세스 인터페이스
* </pre>
*/
public interface MemberJoinValidateBuilderProcess {
/**
* <pre>
* 회원가입 예외 검증 프로세스 구현 메서드
* </pre>
*/
void validate();
}
기본 회원가입 검증 구현체
/**
* <pre>
* 기본 회원가입 예외 검증 구현체
* </pre>
*/
public class MemberJoinValidateBuilderProcessImpl implements MemberJoinValidateBuilderProcess {
private final MemberJoinEntity memberJoinEntity;
public MemberJoinValidateBuilderProcessImpl(MemberJoinEntity memberJoinEntity) {
this.memberJoinEntity = memberJoinEntity;
}
/**
* <pre>
* {@link MemberJoinValidateBuilderProcess#validate()}
* </pre>
*/
@Override
public void validate() {
MemberJoinEntity findMemberJoinEntity = MemberJoinFactory.getInstance(MemberJoinReadImpl.class)
.findMember(this.memberJoinEntity.getMemberId());
if(!"".equals(findMemberJoinEntity.getMemberId())) {
throw new IllegalStateException("존재하는 회원 입니다.");
}
}
}
카카오톡 회원가입 검증 구현체
/**
* <pre>
* 카카오톡 회원가입 예외 검증 구현체
* </pre>
*/
public class KakaoMemberJoinValidateBuilderProcessImpl implements MemberJoinValidateBuilderProcess {
private final KakaoMemberJoinEntity kakaoMemberJoinEntity;
public KakaoMemberJoinValidateBuilderProcessImpl(KakaoMemberJoinEntity kakaoMemberJoinEntity) {
this.kakaoMemberJoinEntity = kakaoMemberJoinEntity;
}
/**
* <pre>
* {@link MemberJoinValidateBuilderProcess#validate()}
* </pre>
*/
@Override
public void validate() {
KakaoMemberJoinEntity findKakaoMemberJoinEntity = MemberJoinFactory.getInstance(KakaoMemberJoinReadImpl.class)
.findKakaoMember(this.kakaoMemberJoinEntity.getKakaoAccountEmail());
if(findKakaoMemberJoinEntity.getId() != 0L) {
throw new IllegalStateException("존재하는 회원 입니다.");
}
}
}
회원가입 프로세스
회원가입 프로세스의 클래스 다이어그램 입니다.
회원가입 인터페이스
/**
* <pre>
* 회원가입 관련 인터페이스
* </pre>
*/
public interface MemberJoinBuilderProcess {
/**
* <pre>
* 회원가입에 대한 프로세스 로직을 작성하는 메서드
* </pre>
*/
void join();
}
기본 회원가입 구현체
/**
* <pre>
* 기본 회원가입 처리 프로세스 클래스
* </pre>
*/
public class MemberJoinBuilderProcessImpl implements MemberJoinBuilderProcess {
private final MemberJoinEntity memberJoinEntity;
public MemberJoinBuilderProcessImpl(MemberJoinEntity memberJoinEntity) {
this.memberJoinEntity = memberJoinEntity;
}
/**
* {@link MemberJoinBuilderProcess#join()}
*/
@Override
public void join() {
MemberJoinFactory.getInstance(MemberJoinWriteImpl.class)
.join(memberJoinEntity);
}
}
카카오톡 회원가입 구현체
/**
* <pre>
* 카카오톡 회원가입 처리 프로세스 클래스
* </pre>
*/
public class KakaoMemberJoinBuilderProcessImpl implements MemberJoinBuilderProcess {
private final KakaoMemberJoinEntity kakaoMemberJoinEntity;
public KakaoMemberJoinBuilderProcessImpl(KakaoMemberJoinEntity kakaoMemberJoinEntity) {
this.kakaoMemberJoinEntity = kakaoMemberJoinEntity;
}
/**
* {@link MemberJoinBuilderProcess#join()}
* 카카오톡 회원가입인 경우엔 응답 받은 카카오톡 전문에서 특정 데이터만 추출하여
* 기본 회원가입 처리도 동시 처리 진행한다.
*/
@Override
public void join() {
KakaoMemberJoinWriteImpl kakaoMemberJoinWriteImpl = MemberJoinFactory.getInstance(KakaoMemberJoinWriteImpl.class);
MemberJoinEntity memberJoinEntity = new MemberJoinEntity(this.kakaoMemberJoinEntity.getKakaoAccountEmail()
, UUID.randomUUID().toString().replace("-", "").substring(0, 10)
, MemberJoinEntity.MemberJoinType.KAKAO);
kakaoMemberJoinWriteImpl.kakaoJoin(this.kakaoMemberJoinEntity);
kakaoMemberJoinWriteImpl.join(memberJoinEntity);
}
}
쿠폰 프로세스
위에 언급한대로 일반 회원가입 시에는 쿠폰 미지급이기 때문에 구현체는 없습니다.
쿠폰 인터페이스
/**
* <pre>
* 회원가입 시 이벤트 프로세스 인터페이스
* </pre>
*/
public interface MemberJoinEventBuilderProcess {
/**
* <pre>
* 이벤트 프로세스를 구현하는 메서드
* </pre>
*/
void event();
}
카카오톡 쿠폰 구현체
/**
* <pre>
* 카카오톡 회원가입 시 이벤트 프로세스 구현체
* </pre>
*/
public class KakaoMemberJoinEventBuilderProcessImpl implements MemberJoinEventBuilderProcess {
private final EventCouponEntity eventCouponEntity;
public KakaoMemberJoinEventBuilderProcessImpl(EventCouponEntity eventCouponEntity) {
this.eventCouponEntity = eventCouponEntity;
}
/**
* <pre>
* {@link MemberJoinEventBuilderProcess#event()}
* 카카오톡으로 회원가입 하는 경우 쿠폰 발급
* </pre>
*/
@Override
public void event() {
EventCouponEntity findEventCouponEntity = MemberJoinEventFactory.getInstance(MemberJoinEventReadImpl.class)
.findMemberJoinEvent(this.eventCouponEntity.getId(), this.eventCouponEntity.getEventCode());
if("".equals(findEventCouponEntity.getEventCode())) {
MemberJoinEventFactory.getInstance(MemberJoinEventWriteImpl.class).saveMemberJoinEvent(this.eventCouponEntity);
}
}
}
빌더 패턴 적용 클래스
자 대망의 빌더 패턴 적용 클래스입니다. 라곤 했지만 사실 정형화된 구조라서 크게 볼건 없습니다.
/**
* <pre>
* 회원가입 프로세스 빌더 패턴 클래스
* </pre>
*/
public class MemberJoinBuilder {
private final MemberJoinBuilderProcess memberJoinBuilderProcess;
private final MemberJoinValidateBuilderProcess memberJoinValidateBuilderProcess;
private final MemberJoinEventBuilderProcess memberJoinEventBuilderProcess;
MemberJoinBuilder(MemberJoinBuilder.Builder builder) {
this.memberJoinBuilderProcess = builder.memberJoinBuilderProcess;
this.memberJoinValidateBuilderProcess = builder.memberJoinValidateBuilderProcess;
this.memberJoinEventBuilderProcess = builder.memberJoinEventBuilderProcess;
}
public MemberJoinBuilderProcess getMemberJoinBuilderProcess() {
return memberJoinBuilderProcess;
}
public MemberJoinValidateBuilderProcess getMemberJoinValidateBuilderProcess() {
return memberJoinValidateBuilderProcess;
}
public MemberJoinEventBuilderProcess getMemberJoinEventBuilderProcess() {
return memberJoinEventBuilderProcess;
}
/**
* <pre>
* 회원가입과 관련된 프로세스 구현체를 담당하는 빌더 패턴 클래스
* </pre>
*/
public static class Builder {
// 이벤트 객체 미 초기화 시 기본으로 사용할 객체 설정 용도.
private final static MemberJoinEventBuilderProcess DEFAULT_EVENT_CALL = () -> {};
// 필수 속성
private final MemberJoinBuilderProcess memberJoinBuilderProcess;
// 필수 속성
private final MemberJoinValidateBuilderProcess memberJoinValidateBuilderProcess;
// 옵션 속성
private MemberJoinEventBuilderProcess memberJoinEventBuilderProcess = DEFAULT_EVENT_CALL;
// 필수 속성은 생성자로 초기화 하도록 강제
public Builder(MemberJoinBuilderProcess memberJoinBuilderProcess
, MemberJoinValidateBuilderProcess memberJoinValidateBuilderProcess) {
this.memberJoinBuilderProcess = memberJoinBuilderProcess;
this.memberJoinValidateBuilderProcess = memberJoinValidateBuilderProcess;
}
// 옵션 속성은 Setter 유형 메서드를 통해 초기화. 이때 체이닝을 위해 현재 객체를 반환.
public MemberJoinBuilder.Builder memberJoinEventProcess(MemberJoinEventBuilderProcess memberJoinEventBuilderProcess) {
this.memberJoinEventBuilderProcess = memberJoinEventBuilderProcess;
return this;
}
public MemberJoinBuilder builder() {
return new MemberJoinBuilder(this);
}
}
}
빌더 패턴에서 필수로 필요한 속성은 private final
로 선언 후 객체 생성 시 생성자를 통해 초기화 하고 옵션인 속성들은 private
만 선언하고 특정 메서드 호출 시에 초기화 하도록 합니다.
서비스 클래스
빌더 패턴 클래스를 실질적으로 사용하는 클래스 입니다.
/**
* <pre>
* 빌더 패턴을 활용한 회원가입 프로세스 클래스
* </pre>
*/
@Service
public class MemberJoinBuilderServiceImpl {
/**
* <pre>
* 회원가입 프로세스 트랜잭션 처리 메서드
* </pre>
*
* @param memberJoinBuilder 회원가입 프로세스 빌더 클래스
*/
public void join(MemberJoinBuilder memberJoinBuilder) {
memberJoinBuilder.getMemberJoinValidateBuilderProcess().validate();
memberJoinBuilder.getMemberJoinBuilderProcess().join();
memberJoinBuilder.getMemberJoinEventBuilderProcess().event();
}
}
테스트 코드
그럼 실제로 정상적 잘 동작하는지 확인하기 위한 테스트 코드 입니다.
@Test
void 카카오톡_회원가입() {
KakaoMemberJoinEntity kakaoMemberJoinEntity = new KakaoMemberJoinEntity(13248627872L
, LocalDateTime.now()
, true
, false
, true
, true
, "test@kakao.com"
);
MemberJoinBuilderProcess kakaoMemberJoinBuilderProcessImpl = new KakaoMemberJoinBuilderProcessImpl(kakaoMemberJoinEntity);
MemberJoinValidateBuilderProcess kakaoMemberJoinValidateBuilderProcessImpl = new KakaoMemberJoinValidateBuilderProcessImpl(kakaoMemberJoinEntity);
MemberJoinEventBuilderProcess kakaoMemberJoinEventBuilderProcessImpl = new KakaoMemberJoinEventBuilderProcessImpl(EventCouponEntity.testEventCoupon(String.valueOf(kakaoMemberJoinEntity.getId())));
MemberJoinBuilder memberJoinBuilder = new MemberJoinBuilder.Builder(kakaoMemberJoinBuilderProcessImpl
, kakaoMemberJoinValidateBuilderProcessImpl)
.memberJoinEventProcess(kakaoMemberJoinEventBuilderProcessImpl)
.builder();
this.memberJoinBuilderServiceImpl.join(memberJoinBuilder);
}
위 코드를 실행하면 아래와 같이 정상적으로 회원가입 후 쿠폰 발급까지 정상적으로 이뤄진걸 확인 할 수 있습니다.
c.d.c.r.m.w.t.KakaoMemberJoinWriteImpl : kakaoJoin : [{"id":13248627872,"connectedAt":[2023,4,5,13,57,26,205544600],"kakaoAccountHasEmail":true,"kakaoAccountEmailNeedsAgreement":false,"kakaoAccountIsEmailValid":true,"kakaoAccountIsEmailVerified":true,"kakaoAccountEmail":"test@kakao.com"}]
c.d.c.r.m.w.test.MemberJoinWriteImpl : defaultJoin : [{"memberId":"test@kakao.com","password":"6cc4e051c1","memberJoinType":"KAKAO"}]
c.d.c.r.e.w.t.MemberJoinEventWriteImpl : Save Event Coupon : [{"id":"13248627872","eventCode":"TEST","eventStartDate":[2023,4,5,13,57,26,208247300],"eventEndDate":[2023,4,5,13,57,26,208247300]}]
그러면 일반 회원가입 같은 경우에는 쿠폰 미지급이기 때문에 MemberJoinEventBuilderProcess
속성을 초기화 시키지 않습니다.
@Test
void 일반_회원가입() {
MemberJoinEntity memberJoinEntity = new MemberJoinEntity(UUID.randomUUID().toString().replace("-", "").substring(0, 5)
, UUID.randomUUID().toString().replace("-", "").substring(0, 10)
, MemberJoinEntity.MemberJoinType.DEFAULT);
MemberJoinBuilderProcess memberJoinBuilderProcessImpl = new MemberJoinBuilderProcessImpl(memberJoinEntity);
MemberJoinValidateBuilderProcess memberJoinValidateBuilderProcessImpl = new MemberJoinValidateBuilderProcessImpl(memberJoinEntity);
MemberJoinBuilder memberJoinBuilder = new MemberJoinBuilder.Builder(memberJoinBuilderProcessImpl
, memberJoinValidateBuilderProcessImpl)
.builder();
this.memberJoinBuilderServiceImpl.join(memberJoinBuilder);
}
위 코드를 실행하면 아래와 같이 쿠폰 발급이 안되는것을 확인 할 수 있습니다.
c.d.c.r.m.w.test.MemberJoinWriteImpl : defaultJoin : [{"memberId":"410c2","password":"813769d501","memberJoinType":"DEFAULT"}]
마치며
생성 패턴 중에서 빌더 패턴은 극악(?)의 유연성을 제공하는 아주 훌륭한 패턴이라고 생각합니다. 이런 극악의 유연성을 제공과 동시에 단점으론 응집도를 높히는데에는 취약한 편이기도 합니다.
위 예제에 가입, 검증, 쿠폰 발급 세 가지의 프로세스들을 보면 각각의 프로세스들은 훌륭하게 나눴으나 응집도를 높히긴 위해선 생성자를 활용한 방법 외에는 뾰족한 수가 없습니다. 생성자로 응집도를 관리하게 되면 결국 상단에 점층적 생성자 패턴을 활용 해야 하는데 그러면 너무 많은 생성자로 인해 응집도 관리가 어려운 상황이 생깁니다. 그리고 위 상황에서 처럼 일반 가입은 쿠폰 미지급이라는 정책이 있음에도 개발자가 실수로 이벤트 객체를 설정하면 정책을 위반하는 프로세스가 될 위험성도 있습니다.
그리고 각 응집도에 맞는 프로세스 흐름을 구현해야 하는데 이 부분도 마찬가지로 다른 메서드에서 직접 구현해야 하는 경우도 있습니다.(물론 이 부분은 의도에 따라 다르긴 합니다.)
개인적인 견해로는 객체 생성의 유연성 보단 응집도를 높혀야 하는 경우엔 추상 관련 패턴을 사용하고 응집도 보단 유연성을 요구하는 경우엔 빌더 패턴을 사용하는게 어떨까 합니다.
마지막으로 해당 게시글의 예제는 빌더패턴 예제에서 확인하실 수 있습니다.