반응형
빌더(Builder) 패턴이란?
- 빌더(Builder) 패턴은 객체를 생성하는 과정을 단순화하기 위한 디자인 패턴
- Builder에 인스턴스 생성방법을 스탭 별로 만들어 놓은 인스턴스를 정의하고, 최종적으로 인스턴스를 받아올 수 있는 getXXX(); 라는 메서드를 구현하는 구현체를 만들어 객체를 생성하는 방법
빌더 패턴을 사용하는 이유
- 필요한 데이터만 설정 가능하다
- 유연성을 확보할 수 있다
- 가독성을 높인다
- 변경 가능성을 최소화할 수 있다
빌더패턴을 사용하는 이유와 구현 코드를 알아보자
예제 코드
예행 계획을 만드는 여행계획 클래스가 있다
public class 여행계획 {
private String title; //여행 이름
private int nights; // 몇박
private int days;// 몇일
private LocalDate startDate; // 여행 시작일
private String whereToStay; // 숙소
private List<DetailPlan> plans; // 구체적인 계획
public 여행계획() {
}
public 여행계획(String title, int nights, int days, LocalDate startDate, String whereToStay, List<DetailPlan> plans) {
this.title = title;
this.nights = nights;
this.days = days;
this.startDate = startDate;
this.whereToStay = whereToStay;
this.plans = plans;
}
// getter, setter 생략
여행을 계획 인스턴스를 만들 때 이렇게 만들 수 있다
public static void main(String[] args) {
여행계획 일본여행 = new 여행계획();
일본여행.setTitle("일본 여행");
일본여행.setNights(2);
일본여행.setDays(3);
일본여행.setStartDate(LocalDate.of(2023, 05, 16));
일본여행.setWhereToStay("호텔");
일본여행.addPlan(0, "체크인");
일본여행.addPlan(0, "식사");
일본여행.addPlan(1, "인형뽑기");
일본여행.addPlan(1, "맛집 탐방");
}
그런데 짧은 국내 여행을 간다 생각하면 어떤 식으로 만들 수 있을까?
여행계획 제주도여행 = new 여행계획();
제주도여행.setTitle("제주도");
제주도여행.setStartDate(LocalDate.of(2023, 7, 15));
이러한 코드는 “일본 여행” 처럼 장황하거나, “제주도 여행” 처럼 객체가 불완전하고 days와 nights 컬럼이 잘못 만들어질 가능성이 존재한다.
그렇다고 생성자로 계획을 모두 만들어야 한다면 생성자가 너무 많아지고, 사용하는 측에서 어떤 생성자를 써서 인스턴스를 생성해야 하는지 쉽게 알기 어렵다.
// 숙소가 없는 여행일수도, 당일로 다녀오는 여행일수도
public 여행계획(String title, int nights, int days, LocalDate startDate, String whereToStay, List<DetailPlan> plans) {
this.title = title;
this.nights = nights;
this.days = days;
this.startDate = startDate;
this.whereToStay = whereToStay;
this.plans = plans;
}
public 여행계획(String title, int nights, int days, LocalDate startDate, String whereToStay) {
this.title = title;
this.nights = nights;
this.days = days;
this.startDate = startDate;
this.whereToStay = whereToStay;
}
public 여행계획(String title, int nights, int days, LocalDate startDate) {
this.title = title;
this.nights = nights;
this.days = days;
this.startDate = startDate;
}
public 여행계획(String title, int nights, int days) {
this.title = title;
this.nights = nights;
this.days = days;
}
public 여행계획(String title, LocalDate startDate, String whereToStay, List<DetailPlan> plans) {
this.title = title;
this.startDate = startDate;
this.whereToStay = whereToStay;
this.plans = plans;
}
... 생략
이제 빌더 패턴을 정의하려면, 먼저 최종적으로 여행 계획을 만드는 인터페이스를 정의한다.
public interface 여행계획Builder {
여행계획Builder nightsAndDays(int nights, int days);
여행계획Builder title(String title);
여행계획Builder startDate(LocalDate localDate);
여행계획Builder whereToStay(String whereToStay);
여행계획Builder addPlan(int day, String plan);
여행계획 getPlan();
}
위의 코드에서 가장 중요한 점은
- 리턴타입이 인터페이스인 여행계획Builder 인 메서드를 호출하고 나면 여행계획Builder 에서 제공하는 또 다른 메서드를 사용해야 한다는 것
- 같이 생성되어야 하는 메서드를 nightsAndDays(int nights, int days); 와 같이 만들 수 있다는 점
- 이러한 형태는 getPlan(); 메서드로 여행계획을 최종적으로 생성할때까지 계속해서 여행 계획을 생성할 수 있고
- 최종적으로 생성하는 getPlan(); 메서드 안에서 데이터를 검증할 수 있다는 점
이제 여행계획Builder 인터페이스를 구현하는 구현체를 살펴보자
public class 여행계획_작성Builder implements 여행계획Builder {
private String title;
private int nights;
private int days;
private LocalDate startDate;
private String whereToStay;
private List<DetailPlan> plans;
@Override
public 여행계획Builder nightsAndDays(int nights, int days) {
this.nights = nights;
this.days = days;
return this;
}
@Override
public 여행계획Builder title(String title) {
this.title = title;
return this;
}
@Override
public 여행계획Builder startDate(LocalDate startDate) {
this.startDate = startDate;
return this;
}
@Override
public 여행계획Builder whereToStay(String whereToStay) {
this.whereToStay = whereToStay;
return this;
}
@Override
public 여행계획Builder addPlan(int day, String plan) {
if (this.plans == null) {
this.plans = new ArrayList<>();
}
this.plans.add(new DetailPlan(day, plan));
return this;
}
@Override
public 여행계획 getPlan() {
return new 여행계획(title, nights, days, startDate, whereToStay, plans);
}
}
기존 여행 생성 코드에 빌더 패턴을 적용해보면
public static void main(String[] args) {
여행계획 일본여행 = new 여행계획_작성Builder()
.addPlan(0,"체크인")
.addPlan(0, "식사")
.addPlan(1, "인형뽑기")
.addPlan(1, "맛집 탐방")
.title("일본 여행")
.nightsAndDays(2,3)
.startDate(LocalDate.of(2020, 12, 9))
.whereToStay("호텔")
.getPlan();
여행계획 제주도여행 = new 여행계획_작성Builder()
.startDate(LocalDate.of(2023, 07, 15))
.title("제주도 여행")
.getPlan();
}
생성자는 만들 때 변수의 순서를 신경 써야 하지만 빌더 패턴은 많은 생성자를 하나하나 만들 필요도 없고 순서가 바뀌어도 상관없는걸 볼 수 있다.
이러한 코드가 중복으로 많이 사용되거나 재사용하고 싶다면 Director를 구현하면 된다
public class 여행Director {
private 여행계획Builder 여행계획Builder;
public 여행Director(여행계획Builder 여행계획Builder) {
this.여행계획Builder = 여행계획Builder;
}
public 여행계획 일본여행() {
return 여행계획Builder
.addPlan(0,"체크인")
.addPlan(0, "식사")
.addPlan(1, "인형뽑기")
.addPlan(1, "맛집 탐방")
.title("일본 여행")
.nightsAndDays(2,3)
.startDate(LocalDate.of(2020, 12, 9))
.whereToStay("호텔")
.getPlan();
}
public 여행계획 제주도여행() {
return 여행계획Builder.startDate(LocalDate.of(2023, 07, 15))
.title("제주도 여행")
.getPlan();
}
}
---- 사용코드
public static void main(String[] args) {
여행Director 패키지 = new 여행Director(new 여행계획_작성Builder());
여행계획 일본여행 = 패키지.일본여행();
여행계획 제주도여행 = 패키지.제주도여행();
}
장점 정리
- 빌더패턴은 사용자에게 순서를 강제할 수도 있고, 필드별 검증 로직을 분리해서 작성할 수 있다.
- 기존 생성자에서 검증하려면 생성자 코드가 너무 복잡해지고 사용하기 어렵다는 단점을 해결했다.
- getxxx(); 메서드를 반드시 호출해야만 인스턴스를 가져올 수 있기 때문에 불완전한 객체를 사용하지 못하게 방지 할 수 있다.
단점 정리
- 빌더를 만들어야 한다. 그런데 이러한 단점은 lombok의 @Builder 어노테이션을 사용하는 방법이 있다.
- 구조가 단순한 생성자나 setter 방식보다 복잡하다