1. 빌더 패턴이란?
생성과 관련된 디자인 패턴으로, 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법
빌더 패턴은 GoF 디자인 패턴 중 생성 패턴에 해당하고 주로 복잡한 단계를 거쳐야 생성되는 개체의 구현을 서브 클래스에게 넘겨줄 때 많이 사용된다.
빌더의 생성자나 메소드에서 유효성 확인을 할 수 있고, 많은 매개변수들을 혼합해서 확인해야 하는경우, build 메소드에서 호출하는 생성자에서 확인할 수 있다.
또, 실패하면 예외를 발생시켜 어떤 매개변수가 잘못되었는지 확인도 할 수 있다.
1-1. 정의(Definition)
- GoF 디자인 패턴 중 생성 패턴에 해당한다.
- 빌더 패턴은 복잡한 객체를 생성하는 클래스와 표현하는 클래스를 분리하여, 동일한 절차에서도 서로 다른 표현을 생성하는 방법을 제공한다.
- 생성해야하는 객체가 Optional한 속성을 많이 가질 때 더 좋다.
1-2. 해결하고자 하는 문제
1. 클라이언트 프로그램에서 팩토리 클래스를 호출할 때 Optional한 인자가 많아지면, 타입과 순서에 대한 관리가 어려워져 에러 발생 확률이 높아진다.
2. 경우에 따라 필요 없는 파라미터들에 대해서 팩토리 클래스에 일일이 NULL 값을 넘겨줘야한다.
3. 생성해야 하는 sub class가 무거워지고 복잡해짐에 따라 팩토리 클래스 또한 복잡해진다.
빌더 패턴은 이러한 문제들을 해결하기 위해 별도의 Builder 클래스를 만들어 필수 값에 대해서는 생성자를 통해, 선택적인 값들에 대해서는 메소드를 통해 step-by-step으로 값을 입력받은 후에 build() 메소드를 통해 최종적으로 하나의 인스턴스를 return하는 방식이다.
2 빌더 패턴(Builder Pattern)을 사용해야 하는 이유
생성자와 수정자로 구현된 다음과 같은 Member 클래스를 바탕으로 왜 생성자나 수정자보다 빌더를 써야하는지 이해해보도록 하자.
@NoArgsConstructor
@AllArgsConstructor
public class Member {
private String name;
private String userId;
private String password;
private int count;
private String grade;
private String regNo;
}
빌더 패턴(Builder Pattern)의 장점
- 필요한 데이터만 설정할 수 있음
- 유연성을 확보할 수 있음
- 가독성을 높일 수 있음
- 변경 가능성을 최소화할 수 있음
- 사용하지 않는 값에 null / 0 / false 값을 일일히 주지 않아도 괜찮다.
1) 필요한 데이터만 설정할 수 있음
예를 들어 Member 객체를 생성해야 하는데 grade라는 파라미터가 필요 없는 상황이라고 가정하자.
생성자나 정적 메소드를 이용하는 경우라면 우리는 grade에 더미 값을 넣어주거나 grade가 없는 생성자를 새로 만들어주어야 한다.
// 1. 더미값을 넣어주는 방법
Member member = new Member("홍길동","test","test1234",0,"","860824-1655068")
@NoArgsConstructor
@AllArgsConstructor
public class Member {
private String name;
private String userId;
private String password;
private int count;
private String grade;
private String regNo;
// 2. 생성자 또는 정적 메소드를 추가하는 방법
public Member (String name, String userId, String password, int count, String regNo){
this.name = name;
this.userId = userId;
this.password = password;
this.count = count;
this.regNo = regNo;
}
//정적 메소드를 이용해 추가하는 방법
public static Member newMember(String name, String userId, String password, int count, String regNo){
return new Member(name, userId, password, 0 , "860824-1655068");
}
}
이러한 작업이 한 두번이면 번거로워도 작업을 해줄 수 있다. 하지만 결국 요구사항은 계속 변하게 되어있고, 반복적인 변경을 필요로 하면서 시간 낭비로 이어지게 된다. 하지만 빌더를 이용하면 동적으로 이를 처리할 수 있다.
Member member = Member.builder()
.name("홍길동")
.userId("test")
.password("test1234")
.count(0)
.regNo("860824-1655068")
.build();
그리고 이렇게 필요한 데이터만 설정할 수 있는 빌더의 장점은 생성자 또는 정적 메소드와 비교하여 테스트용 객체를 생성할 때 용이하게 해주고, 불필요한 코드의 양을 줄이는 등의 이점을 안겨준다.
2)유연성을 확보하고, 가독성을 높일수 있다.
Member 클래스에 새로운 나이(age)와, 사용한 금액(countMoney)을 추가해야한다고 하면 다음과 같이 생성자로 인스턴스화 해야할 것이다. 그리고 Member 객체를 생성하는 코드가 100개 있다면 새롭게 추가되는 변수 때문에 기존의 코드를 수정해주거나 생성자를 따로 추가하는 등의 불필요한 조치를 해주어야 할 것이다.
하지만 빌더 패턴를 기반으로 코드가 작성되어 있다면 기존의 코드는 수정할 필요가 없다. 왜냐하면 빌더 패턴은 유연하게 객체의 값을 설정할 수 있도록 도와주기 때문이다.
//이전
Member member = new Member("홍길동","test","test1234",0,"","860824-1655068")
//변경
Member member = new Member("홍길동","test","test1234",0,"","860824-1655068", 29, 1040510)
갑작스렇게 많아진 매개변수에, 어떤 값을 써야하는지 모르게되고 결국, 추가될수록 코드를 보기 점점 힘들어질 것이다. 최악의 경우 매개변수를 잘못 입력하여 오류가 발생 할 수도 있다.
Member member = Member.builder()
.name("홍길동")
.userId("test")
.password("test1234")
.count(0)
.regNo("860824-1655068")
.countMoney(1040510)
.age(29)
.build();
위와 같은 코드를 보면 어떤 데이터가 Member에 추가되어야하는지, 명시적으로 표현하고 있어 직관적으로 보기 쉽고 또한 순서가 변경되어도 잘 작동된다.
또한, 동적으로 빌더를 통해 잘못된 데이터가 들어간 경우(검증에 실패한경우) illegalargumentexception 을 통해 에러메세지로 어떠한 매개변수가 잘못되었는 지 확인할 수 있다.
3) 변경 가능성을 최소화할 수 있다.
많은 개발자들이 수정자 패턴(Setter)를 흔히 사용한다. 하지만 Setter를 구현한다는 것은 불필요하게 변경 가능성을 열어두는 것이다. 이는 유지보수 시에 값이 할당된 지점을 찾기 힘들게 만들며 불필요한 코드 리딩 등을 유발한다. 만약 값을 할당하는 시점이 객체의 생성뿐이라면 객체에 잘못된 값이 들어왔을 때 그 지점을 찾기 쉬우므로 유지보수성이 훨씬 높아질 것이다. 그렇기 때문에 클래스 변수는 변경 가능성을 최소화하는 것이 좋다.
변경 가능성을 최소화하는 가장 좋은 방법은 변수를 final로 선언함으로써 불변성을 확보하는 것이다.
위의 Member 클래스를 다음과 같이 수정할 수 있다.
@Builder
@RequiredArgsConstructor
public class Member {
private final String name;
private final String userId;
private final String password;
private final int count;
private final String grade;
private final String regNo;
}
하지만 경우에 따라서 클래스 변수에 final을 붙일 수 없는 경우가 있을 수 있다. 이러한 경우라면 final이 없어도 Setter를 구현하지 않음으로써 동일한 효과를 얻을 수 있다. 중요한 것은 변경 가능성을 열어두지 않는 것인데, final로 강제할 수 있다면 가장 바람직하지만 final을 붙일 수 없는 경우라면 Setter를 넣어주지 않으면 된다.
3. 빌더 패턴을 더 잘 사용하려면..
1. 불필요한 @Setter / @Data 의 사용은 NO!
변경 가능성을 최소화 하는 경우에는 Setter를 구현하지 않음으로서 final로 선언한 것과 같은 불변성을 확보할 수 있다. (의존관계의 변경이 필요한 상황이 없는경우, 변경의 가능성을 배제하고 불변성을 보장하자)
2. Class 레벨이 아닌 생성자 레벨에 적용하자
@Builder를 Class에 적용하면, 모든 맴버 필드에 대해 매개변수를 만드는 기본 생성자를 만들기때문에 만약 어떤 변수가 데이터베이스 PK 생성전략에 의존한다면, 그 데이터는 변경되어서는 안되기때문이다. Class 레벨에서는 제한하기가 어렵다.
3. 초기값이 필요할 때에는 Builder.Default를 이용하자
Builder는 Wrapper 타입을 사용하여 값을 설정하지 않으면 자동으로 다음과 같은 값을 가진다.
Wrapper / Object : null
int : 0
boolean : false
만약에 기본값을 설정하고싶다면, @Builder.Default를 이용하여 기본값을 설정해 줄 수 있는데, 다음과 같이 설정하면, 기본값이 변경된다. (@Builder Lombok)
@Builder.Default
private String grade = "VIP";
4. 빌더 패턴이 필요 없는 경우
객체를 생성하는 대부분의 경우에는 빌더 패턴을 적용하는 것이 좋다. 물론 예외적인 케이스가 있을 수 있는데, 대표적으로 크게 다음의 2가지 상황에서는 빌더를 구현할 필요가 없다.
- 객체의 생성을 라이브러리로 위임하는 경우
- 변수의 개수가 2개 이하이며, 변경 가능성이 없는 경우
예를 들어 엔티티(Entity) 객체나 도메인(Domain) 객체로부터 DTO를 생성하는 경우라면 직접 빌더를 만들고 하는 작업이 번거로우므로 MapStruct나 Model Mapper와 같은 라이브러리를 통해 생성을 위임할 수 있다. 또한 변수가 늘어날 가능성이 거의 없으며, 변수의 개수가 2개 이하인 경우에는 정적 팩토리 메소드를 사용하는 것이 더 좋을 수도 있다. 빌더의 남용은 오히려 코드를 비대하게 만들 수 있으므로 변수의 개수와 변경 가능성 등을 중점적으로 보고 빌더 패턴을 적용할지 판단하면 된다.
연관된 글 :
참고:
[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)
[Java] 빌더 패턴(Builder Pattern)을 사용해야 하는 이유
[Spring] 빌더 패턴(Bulider Pattern)에 대해 알아보자
[Spring] DTO와 Mapper, Mapper(Map Struct) 사용 방법 - gradle
'개발 > Spring' 카테고리의 다른 글
[Spring] 스웨거 (Swagger) v3 설정하기 (0) | 2023.04.30 |
---|---|
[Spring] Swagger 어노테이션 (0) | 2023.04.28 |
[Spring] 스프링 프로젝트 구조 (DTO, Entity and Mapper) (0) | 2023.04.28 |
[Spring] 스웨거 (Swagger) 설정하기 (0) | 2023.04.27 |
[Spring] h2 DB 연결하고 JPA 사용하기 (0) | 2023.04.27 |