개요
builder 패턴은 간단히 요약하자면 객체 생성시,
- 클래스의 속성 값이 많은 경우 생성자를 활용하면 코드가 너무 길어져 가독성을 해친다는 단점
- setter 함수를 활용하면 어디서 객체 속성 값이 변경되었는지 추적하기 어렵다는 단점
위 두 단점을 해결할 수 있는 장점을 갖는 객체 생성 패턴이다.
이런 유용한 builder 패턴을 Lombok의 @Builder
어노테이션으로 간단하게 사용할 수 있다.
클래스에 붙여서 사용할 수 있고, 생성자에 붙여서도 사용할 수 있다.
예제 클래스
public class User {
private String name;
private String email;
private int age;
// 이 속성은 직접 값을 지정하지 않는다.
// 즉, 객체 생성시 해당 속성의 값을 지정할 수 없다.
private boolean isAdult;
}
직접 Builder 패턴 구현하기
@Builder
어노테이션을 활용하기 전에 직접 builder 패턴을 구현해보았다.
구현 코드
public class User {
private String name;
private String email;
private int age;
private boolean isAdult;
private User(Builder builder) {
this.name = builder.name;
this.email = builder.email;
this.age = builder.age;
}
public static class Builder {
private final String name; // 필수 속성은 반드시 final을 선언
private final int age; // 필수 속성은 반드시 final을 선언
private String email = ''; // 선택 속성은 기본 값을 설정
public Builder(String name, int age) {
this.name = name;
this.age = age;
}
public Builder email(String email) {
this.email = email;
return this;
}
public User build() {
return new User(this);
}
}
public static class Builder
내부 클래스를 정의한다.- 필수 속성은
final
을 추가하고, 선택 속성은 기본값을 설정한다. Builder
내부 클래스의 생성자에는 필수 속성을 파라미터로 정의한다.- 선택 속성은 하나씩 메소드로 정의한다.
- 클래스 객체를 최종 생성하는
build()
메소드를 정의한다. - 클래스는
Builder
클래스를 받아서 객체를 생성하는private
생성자를 만든다.
사용 예제
// ERROR: 필수 값인 age가 설정되지 않았으므로 사용할 수 없다.
new User.Builder("a").build();
// 필수 값인 name과 age가 설정되고, 선택적으로 email 값도 설정한다.
new User.Builder("a", 10).build();
new User.Builder("a", 10).email("a@a.com").build();
클래스에 @Builder
붙이기
구현 코드
@Builder
public class User {
private String name;
private String email;
private int age;
private boolean isAdult;
}
정말 간단하게 클래스에 @Builder
어노테이션을 붙여주면 된다.
컴파일 후 생성된 클래스 코드
public class User {
private String name;
private String email;
private int age;
private boolean isAdult;
User(final String name, final String email, final int age, final boolean isAdult) {
this.name = name;
this.email = email;
this.age = age;
this.isAdult = isAdult;
}
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
private String name;
private String email;
private int age;
private boolean isAdult;
UserBuilder() {}
public UserBuilder name(final String name) {
this.name = name;
return this;
}
public UserBuilder email(final String email) {
this.email = email;
return this;
}
public UserBuilder age(final int age) {
this.age = age;
return this;
}
public UserBuilder isAdult(final boolean isAdult) {
this.isAdult = isAdult;
return this;
}
public User build() {
return new User(this.name, this.email, this.age, this.isAdult);
}
public String toString() {
return "User.UserBuilder(name=" + this.name + ", email=" + this.email + ", age=" + this.age + ", isAdult=" + this.isAdult + ")");
}
}
}
- 모든 클래스 속성이 담긴
default
생성자가 만들어진다. Builder
내부 스태틱 클래스와 그 클래스를 리턴하는builder()
스태틱 메소드가 만들어진다.- 각 속성들은 속성 명을 따르는 메소드가 정의된다.
- 클래스 객체를 최종 생성하는
build()
메소드가 정의된다. - 현재
Builder
클래스의 현황을 볼 수있는toString()
메소드가 정의된다.
문제점
- 모든 클래스 속성이 생성자의 파라미터로 들어간다.
- 직접 지정하지 않는
isAdult
속성도 지정할 수 있게된다.
- 직접 지정하지 않는
- 생성자의 접근제한자가
default
가 된다.- 생성자는 다른 클래스에서 접근할 필요가 없으므로 접근 제한을 더욱 축소할 필요가 있다.
생성자에 @Builder
붙이기
클래스에 @Builder
어노테이션을 붙여 생기는 문제점을 해소하기 위해 생성자에 @Builder
를 붙이는 방식을 많이 사용한다고 한다.
구현 코드
public class User {
private String name;
private String email;
private int age;
private boolean isAdult;
@Builder
private User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
}
객체 생성 시 지정할 수 있는 속성 값만 담은 private
생성자를 만들어 @Builder
어노테이션을 달아준다.
컴파일 후 생성된 클래스 코드
public class User {
private String name;
private String email;
private int age;
private boolean isAdult;
private User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder() {
private String name;
private String email;
private int age;
UserBuilder() {}
public UserBuilder name(final String name) {
this.name = name;
return this;
}
public UserBuilder email(final String email) {
this.email = email;
return this;
}
public UserBuilder age(final int age) {
this.age = age;
return this;
}
public User build() {
return new User(this.name, this.email, this.age);
}
public String toString() {
return "User.UserBuilder(name=" + this.name + ", email=" + this.email + ", age=" + this.age + ")";
}
}
- 객체 생성 시 지정할 속성만 담은
private
생성자가 있다.isAdult
속성은Builder
클래스 내부에서 찾아볼 수 없다.
- 이하 클래스에
@Builder
를 붙였을 때와 동일하다.
사용 예제
User user = User.builder()
.name("a")
.email("a@a.com")
.age(10).build();
장점
- 클래스 생성자가
private
접근제한자를 가진다. - 객체 생성 시 직접 지정하지않는
isAdult
속성에 대한 접근을 차단할 수 있다.
한계점
직접 builder 패턴을 구현할 때 필수 속성, 선택 속성을 나누어 활용할 수 있던 구조를 찾아볼 수 없다.
@Builder.Default
어노테이션도 있는데….
warning: @Builder.Default requires @Builder or @SuperBuilder on the class for it to mean anything.
생성자에 @Builder
어노테이션을 붙인 상태로 속성 값에 @Builder.Default
어노테이션을 붙여서 사용할 수는 없는 것 같다.
생성자에 붙인 상태에서 어노테이션을 사용하는 방법을 찾지 못했다. 만약 발견한다면 추후 업데이트 예정!
만약 방법이 없고 디폴트 값 지정이 필요하다면 차라리 builder 패턴을 직접 구현하는 것이 유리할 수 있다고 생각한다.