Builder Pattern 빌더 패턴의 특징, 장점 (@Builder 사용이유, @Builder 예제)
entity나 Dto객체에 값을 넣어줄때, 롬복의 빌더 애노테이션(@Builder)을 사용하는 예제들이 많다.
객체를 생성하기 위해서는 생성자 패턴, 자바빈 패턴, 빌더 패턴 등을 사용할 수 있는데, 이 빌더 패턴을 쓰는 이유가 무엇일까?
우선 빌더 패턴이란 디자인패턴중 하나로, 생성과 표현의 분리다.
클래스를 설계하다보면 필수로 받야할 인자들이 있고 선택적으로 받야할 인자들이 있는데,
쉽게 말해 생성자에서 인자가 많을때 고려해 볼수있는것이 바로 이 빌더패턴이다.
왜 그런 것인지 다른 패턴들과 함께 비교해보자.
Telescoping Constructor Pattern
점층적 생성자 패턴
각 생성자를 오버로딩 해서 만드는 기초적인 방식이다.
필수적으로 값이 있어야할 멤버변수를 위해 생성자에 매개변수를 넣는다.
또한 선택적 인자를 받기위해 추가적인 생성자를 만든다.
쉽게 짤수 있다는 장점이 있지만,
인자들이 많아질수록 생성자가 많아지고
매개변수의 정보를 설명할 수 없으므로 어떤 객체에 어떤 인자가 들어갔는지 알기어렵다.
그래서 코드 수정(필드 추가 등)이 필요한 경우 수정이 복잡하다.
public class Member {
private String nickname; //필수
private String password; //필수
private Gender gender; //선택
public Member(String nickname) {
this.nickname = nickname;
}
public Member(String nickname, String password) {
this.nickname = nickname;
this.password = password;
}
public Member(String nickname, String password, Gender gender) {
this.nickname = nickname;
this.password = password;
this.gender = gender;
}
}
Member member = new Member("minji", "123", F);
매개변수의 어떤 위치에 어떤타입과 값을 넣어줘야하는지 개발자가 알고있어야하고, 가독성도 떨어진다.
Java Beans Pattern
자바 빈 패턴
가장 익숙한 getter/setter를 이용하여 객체를 생성할때 필드를 주입하는 방식이다.
public class Member {
private String nickname;
private String password;
private Gender gender;
public Member() {
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public void setPassword(String password) {
this.password = password;
}
public void setGender(Gender gender) {
this.gender = gender;
}
}
자바 빈즈패턴을 사용하면 가독성이 어느정도 해결된다. 하지만 코드량이 늘어나는 단점이 존재하고, 가장 문제가 되는점은 setter 메소드를 통해 값이 계속 변할 수 있기 때문에, 객체일관성이 깨진다는 것이다. 객체일관성이 깨진다는것은 한번 객체를 생성할때 그 객체가 변할 여지가 있다는 것이다. (이는 쓰레드간의 공유 가능한 상태가 존재한다는 것이기 때문에 immutable 객체를 생성할 수 없다.)
Member member = new Member();
member.setName("minji");
member.setPassword("123");
member.setGender("F");
이렇게 객체 생성을 하고나서, 값을 부여하고 있다.
Builder Pattern
빌더 패턴
위 둘의 단점을 모두 보완해서 나타난것이 바로 빌더패턴이다.
정보들은 자바빈즈패턴처럼 받되, 데이터 일관성을 위해 정보들을 다 받은 후에 객체를 생성한다.
빌더패턴을 적용하면 다음과 같은 장점이 있다.
- 불필요한 생성자의 제거
- 데이터의 순서에 상관없이 객체생성 가능
- 명시적 선언으로 이해하기가 쉽고 각 인자가 어떤 의미인지 알기 쉽다.(가독성)
- setter 메서드가 없으므로 변경 불가능한 객체를 만들수있다.(객체불변성)
- 한번에 객체를 생성하므로 객체 일관성이 깨지지 않는다.
- build() 함수가 null인지 체크해주므로 검증이 가능한다. (안그러면 set하지않은 객체에대해 get을 하게되는경우 nullPointerExcetpion발생 등등의 문제가 생김)
빌더패턴은 다음과 같이 만들어진다.
- Member 클래스 내부에 빌더클래스 생성.
- 각 멤버 변수별 메서드를 작성, 각 메소드는 변수에 값을 set하고 빌더객체를 리턴한다.
- build() 메서드는 필수 멤버변수의 null체크를 하고 지금까지 set된 builder를 바탕으로 Member 클래스의 생성자를 호출하고 인스턴스를 리턴.
public class Member {
private String nickname; //필수
private String password; //필수
private Gender gender; //선택
public static class Builder {
private final String nickname;
private final String password;
private Gender gender;
// 필수변수는 생성자로 값을 넣는다.
public Builder(String nickname, String password) {
this.nickname = nickname;
this.password = password;
}
// 멤버변수별 메소드: 빌더클래스의 필드값을 set하고 빌더객체를 리턴한다.
public Builder gender(Gender gender) {
this.gender = gender;
return this;
}
// 빌더메소드
public Member build() {
return new Member(this);
}
}
public Member(Builder builder) {
this.nickname = builder.nickname;
this.password = builder.password;
this.gender = builder.gender;
}
}
Member memberEntity = new Builder("minji", "1234")
.gender(Gender.F)
.build();
하지만 빌더 패턴도 역시 단점이 존재한다. 객체를 생성하려면 우선 빌더객체를 생성해야 하고, 보다시피 다른 패턴들보다 많은 코드를 요구하기 때문에 인자가 충분히 많은 상황에서 이용할 필요가 있다..
Lombok @Builder
@Builder
..는! 이제 위에서 만든 빌더클래스를 직접 만들지 않아도 롬복이 지원해주는 어노테이션 하나로 클래스를 생성할 수 있다. 클래스 또는 생성자 위에 @Builder 어노테이션을 붙여주면 빌더패턴 코드가 빌드된다. 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함된다.
import lombok.Builder;
@Builder
public class Member {
private String nickname;
private String password;
private Gender gender;
}
public static void main(String[] args) {
Member memberEntity = Member.builder()
.nickname("minji")
.password("123")
.gender(Gender.F)
.build();
}
정말 간단하고 좋은 빌더패턴!
하지만 변수의 개수가 2개 이하이거나, 변경 가능성이 없을 경우에는 필더 패턴의 장점을 누릴 수 없을테니 사용하지 않아도 될 것 같다.