빌더 패턴은 생성자를 대체하기 위한 방법이다. 생성자의 어떤 불편한 점을 보완한 것일까? 바로 멤버변수를 파라미터로 받는 부분이다. 간단한 예시를 들어본다.
1) 클래스A는 멤버변수 name,age,gender 3가지를 가지고 있다.
public class A {
public String name;
public int age;
public String gender;
}
2) 이때 클래스A 생성자를 통해 name만 값을 부여해 클래스A 객체를 만들고 싶다면?
A A객체명 = new A("넣어줄이름",0,null);
public class A {
public String name;
public int age;
public String gender;
public A(String name) { // name만 파라미터로 받는 생성자 생성
this.name=name;
}
}
3) 이렇게 멤버변수값을 넣을 때도 있고 안 넣을 때도 있다면 굉장히 불편한 상황이 생길 수 있다.
이런 상황들로 인해 처음부터 원하는 멤버변수만을 파라미터로 받아서 초기화를 시킬 수 있는 생성자를 만들었고, 이것이 바로 빌더(Builder)다.
굳이 빌더를 쓰지 않더라도 위 문제들을 해결할 수는 있다.
1. 기본 생성자를 이용해 객체를 만든다.
2. setter를 이용해 원하는 멤버변수만을 초기화한다.
이런 방법을 자바빈즈 패턴이라고도 부른다.
위에서 제시한 3가지 상황에 대입해 본다.
문제점 :
1. 초기화할 멤버변수가 많다면 setter를 여러번 호출해야 한다. 만약 setter 작업들이 아직 끝나지 않았는데 누군가 미완성된 객체를 호출해버린다면? 문제발생! 그러므로 스레드 안정성을 위해 추가작업이 필요하게 된다.
2. Open-Closed법칙에 위배된다. 무슨 뜻이냐면 불필요하게 확장 가능성을 열어두었다는 뜻이다. 클래스의 변수는 변경을 많이 하면 할수록 유지보수가 힘들어지게 된다. 언제 값이 바뀌었는지 같은 부분 체크가 힘들기 때문이다. 하지만 setter를 이용하면 언제나 값이 수정가능해져 오히려 유지보수를 힘들게 하는 메서드가 되버릴 수 있다.
Builder패턴을 3파트로 나누어 만들어보았다. 구성은 다음과 같다.
public class ProbonoProjectDTO {
private int probonoProjectId;
private String probonoProjectName;
private String probonoId;
private String activistId;
private String receiveId;
private String projectContent;
//Builder직접 만들어보기
//1.AllArgsConstructor만들어주기
ProbonoProjectDTO(int probonoProjectId,
String probonoProjectName,
String probonoId,
String activistId,
String receiveId,
String projectContent) {
this.probonoProjectId=probonoProjectId;
this.probonoProjectName=probonoProjectName;
this.probonoId=probonoId;
this.activistId=activistId;
this.receiveId=receiveId;
this.projectContent=projectContent;
}
//2.Builder클래스 생성,반환하는 builder메서드 만들어주기
public static ProbonoProjectDTOBuilder builder() {
return new ProbonoProjectDTOBuilder();
}
//3.Builder클래스 만들어주기
public static class ProbonoProjectDTOBuilder{
private int probonoProjectId;
private String probonoProjectName;
private String probonoId;
private String activistId;
private String receiveId;
private String projectContent;
ProbonoProjectDTOBuilder(){
}
public ProbonoProjectDTOBuilder probonoProjectId(int probonoProjectId) {
this.probonoProjectId=probonoProjectId;
return this;
}
public ProbonoProjectDTOBuilder probonoProjectName(String probonoProjectName) {
this.probonoProjectName=probonoProjectName;
return this;
}
public ProbonoProjectDTOBuilder probonoId(String probonoId) {
this.probonoId=probonoId;
return this;
}
public ProbonoProjectDTOBuilder activistId(String activistId) {
this.activistId=activistId;
return this;
}
public ProbonoProjectDTOBuilder receiveId(String receiveId) {
this.receiveId=receiveId;
return this;
}
public ProbonoProjectDTOBuilder projectContent(String projectContent) {
this.projectContent=projectContent;
return this;
}
public ProbonoProjectDTO build() {
return new ProbonoProjectDTO(this.probonoProjectId
, this.probonoProjectName
, this.probonoId
, this.activistId
, this.receiveId
, this.projectContent);
}
public String toString() {
return "Builder구성 : "
+ "DTO명 : ProbonoProjectDTO"
+ " 프로젝트명 : " + this.probonoProjectName
+ " 프로젝트ID : " +this.probonoId
+ " 기부자 : " +this.activistId
+ " 수혜자 : " +this.receiveId
+ " 내용 : " +this.projectContent;
}
}
}
전체적인 흐름을 보면 클래스ProbonoProjectDTO와 같은 형식의 멤버변수를 가진 클래스ProbonoProjectDTOBuilder를 만들어 이 클래스의 객체를 만들고 멤버변수를 초기화해 다시 클래스ProbonoProjectDTO 타입으로 반환해준다. 마찬가지로 위에서 언급한 3가지 상황에 대입하여 어떻게 문제를 해결했는지 알아본다.
자바에서는 롬복(lombok)을 통해 간단하게 어노테이션(@Builder)으로 빌더를 생성할 수 있다.
만약 클래스 멤버변수를 선언하고 값을 초기화해주었다면, 이는 롬복 빌더에서 적용되지 않는다. 예제를 본다.
public class Member {
private int age=999;
}
Member클래스에 멤머변수 age를 999 int값으로 초기화했다. 이 클래스에 @Builder를 사용해 빌더를 만든 후, age값을 설정하지 않고 Member객체를 빌드한다면? Member클래스에 toString을 설정해주고 출력해본다.
Member member123 = Member.builder().build();
System.out.println(member123);
// 출력화면 : Member [age=0]
age는 분명 999로 초기화했지만 0으로 출력이 된다. 왜 그럴까?
이유는 롬복 빌더의 Builder클래스 생성 방식때문일 것이다. Builder클래스를 선언할 때 Membr클래스의 멤버변수를 초기화한 것처럼 똑같이 멤버면수를 선언하고 초기화까지 한다면 이런 상황을 발생하지 않을 것이다. 하지만 Builder클래스는 단지 Member클래스의 멤버변수를 똑같이 선언만하지 초기화는 하지 않는다. 이로 인해 초기화값에 대한 내용은 롬복 빌더를 사용했을 때 적용되지 않는다.(초기화값 대신 0,null같은 더미값이 들어감)
내 생각으로는 setter를 이용한 자바빈즈 패턴과 거의 흡사하다고 생각된다. 다른점은 멤버변수를 초기화하는 메서드를 이어붙여 한번에 호출 가능하다는 점이다.
클래스명 객체명 = new 클래스명();
객체명.set멤버변수1(값1);
객체명.set멤버변수2(값2);
객체명.set멤버변수3(값3);
객체명.set멤버변수4(값4);
클래스명 객체명 = 클래스명.builder().멤버변수1메서드(값1).멤버변수2메서드(값2).멤버변수3메서드(값3).멤버변수4메서드(값4).build();
내가 이해한 빌더 요약 : 자바빈즈 패턴처럼 객체를 생성하고 setter로 멤버변수를 초기화하는 모든 과정을 클래스 안에 집어넣어 하나의 작업을 묶은 패턴