생성자에 선택적 매개변수가 많다면, 실수를 유발하기 쉽습니다. 이는 정적 팩터리도 마찬가지입니다.
다음은 선택적 매개변수가 많은 예시 클래스입니다.
public class Person {
private final long id; // 필수
private final String name; // 필수
private final int age; // 필수
private final String job; // 선택
private final String address; // 선택
private final int weight; // 선택
id, name, age는 필수 요소이며 job, address, weight는 선택 요소입니다.
이럴 때 점층적 생성자 패턴을 사용할 수 있습니다. 다음 예시 코드와 함께 점층적 생성자 패턴을 설명하겠습니다.
public Person(long id, String name, int age) {
this(id, name, age, null);
}
public Person(long id, String name, int age, String job) {
this(id, name, age, job, null);
}
public Person(long id, String name, int age, String job, String address) {
this(id, name, age, job, address, 0);
}
public Person(long id, String name, int age, String job, String address, int weight) {
this.id = id;
this.name = name;
this.age = age;
this.job = job;
this.address = address;
this.weight = weight;
}
단점
public Person(long id, String name, int age, String job, String address) {
this(id, name, age, address, job, 0);
}두 번째 방법으로 자바 빈즈 패턴을 사용하는 방법이 있습니다. 역시 코드로 설명하겠습니다.
public class Person {
private long id; // 필수
private String name; // 필수
private int age; // 필수
private String job; // 선택
private String address; // 선택
private int weight; // 선택
public Person() {
}
public void setId(long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setJob(String job) {
this.job = job;
}
public void setAddress(String address) {
this.address = address;
}
public void setWeight(int weight) {
this.weight = weight;
}
}
단점
Person person = new Person();
person.setId(1);
person.setName("PERSON");
person.setAddress("BUSAN");
person.setAge(20);
person.setJob("STUDENT");
person.setWeight(100);불변에 대해서는 item 17에서 더 자세히 다루도록 하겠습니다.
점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성을 겸비한 패턴입니다.
public class Person {
private final long id; // 필수
private final String name; // 필수
private final int age; // 필수
private final String job; // 선택
private final String address; // 선택
private final int weight; // 선택
public static class Builder {
private final long id; // 필수
private final String name; // 필수
private final int age; // 필수
private String job = "STUDENT"; // 선택
private String address = "KOREA"; // 선택
private int weight = 0; // 선택
public Builder(long id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public Builder job(String job) {
this.job = job;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder weight(int weight) {
this.weight = weight;
return this;
}
public Person build() {
return new Person(this);
}
}
public Person(Builder builder) {
this.id = builder.id;
this.name = builder.name;
this.age = builder.age;
this.job = builder.job;
this.address = builder.address;
this.weight = builder.weight;
}
}
Person person = new Builder(1, "SON", 20)
.address("BUSAN")
.weight(30)
.build();
빌더 패턴을 사용하여 객체를 생성한 모습입니다. job 메서드를 호출하지 않았는데, 콘솔에 출력해보면

default로 설정해 둔 STUDENT 가 출력됨을 확인할 수 있습니다.
단점
객체를 만들기 전에 빌더를 만들어야 하는 수고가 있습니다. 따라서 매개변수가 3개 이하라면, 굳이 빌더 패턴을 사용하지 않아도 될 수 있습니다.
하지만 api의 확장 가능성을 항상 염두해 둔다면 시작부터 빌더 패턴을 사용하는 것도 나쁘지 않다고 합니다.
@Component
public class CommentTestHelper {
@Autowired
CommentRepository commentRepository;
@Autowired
MemberTestHelper memberTestHelper;
@Autowired
PostTestHelper postTestHelper;
public Comment generate() {
return this.builder().build();
}
public CommentBuilder builder() {
return new CommentBuilder();
}
public final class CommentBuilder {
private Member member;
private Post post;
private Comment parent;
private String content;
private String ipAddress;
private CommentBuilder() {
}
public CommentBuilder member(Member member) {
this.member = member;
return this;
}
public CommentBuilder post(Post post) {
this.post = post;
return this;
}
public CommentBuilder parent(Comment parent) {
this.parent = parent;
return this;
}
public CommentBuilder content(String content) {
this.content = content;
return this;
}
public CommentBuilder ipAddress(String ipAddress) {
this.ipAddress = ipAddress;
return this;
}
public Comment build() {
return commentRepository.save(Comment.builder()
.member(member != null ? member : memberTestHelper.generate())
.post(post != null ? post : postTestHelper.generate())
.parent(parent)
.content(content != null ? content : "댓글내용")
.ipAddress(ipAddress != null ? ipAddress : "0.0.0.0")
.build());
}
}
}
위 코드는 백엔드 프로젝트의 테스트 코드에서 도입한 빌더 패턴입니다. 테스트 코드를 짤 때, 객체 생성을 편하게 하도록 하기 위해서 작성되었습니다. 메서드 단위로 뜯어서 설명해보겠습니다.
public CommentBuilder builder() {
return new CommentBuilder();
}
CommentTestHelper.builder() 메서드는 CommentBuilder 생성자를 호출하고 있습니다. 빌더를 호출할 때 가장 먼저 호출하는 메서드입니다.
CommentBuilder는 Commet 스펙에 맞게 필드와 setter 메서드를 가지고 있습니다. 각각을 필요에 따라 호출하면 됩니다.
public Comment build() {
return commentRepository.save(Comment.builder()
.member(member != null ? member : memberTestHelper.generate())
.post(post != null ? post : postTestHelper.generate())
.parent(parent)
.content(content != null ? content : "댓글내용")
.ipAddress(ipAddress != null ? ipAddress : "0.0.0.0")
.build());
}
마지막으로 build()를 호출하면 되는데, 필드 값이 null 일 경우(빌더의 setter 메서드를 호출해서 값을 설정하지 않았을 경우) default 값을 설정하는 코드가 추가되어 있습니다.
이는 db에 값이 들어갈 때, not null 처리된 컬럼에 값을 채워넣기 위해 설정해두었습니다.
+) 책의 예제에서는 생성자를 호출했지만, 백엔드 코드에서는 Comment의 빌더를 호출하고 CommentBuilder의 값을 채워넣고 있습니다.
public Comment generate() {
return this.builder().build();
}
generate() 메서드는 기본 객체를 생성할 때 CommentTestHelper.builder().build(); 대신 호출하도록 만들어졌습니다.
앞에서 빌더 패턴의 단점이, 객체를 생성하기 전에 빌더를 작성해야 한다는 점이였습니다. 하지만 Lombok에서 제공하는 @Builder를 사용하기만 하면 수고 없이 빌더를 사용할 수 있습니다. 프로젝트에서는 private 생성자에 @Builder를 붙여 사용하고 있습니다.
@Builder
private Comment(Member member, Post post, Comment parent, String content, String ipAddress) {
this.member = member;
this.post = post;
this.parent = parent;
this.content = content;
this.ipAddress = ipAddress;
}
클래스에도 @Builder를 붙일 수 있습니다. 실제로 리뉴얼2 이전 프로젝트에서는 클래스 단위에 @Builder를 붙였었습니다.
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder // 클래스 단위에 붙인 빌더 어노테이션
@ToString
@Table(name = "comment")
public class CommentEntity {
If a member is annotated, it must be either a constructor or a method. If a class is annotated, then a package-private constructor is generated with all fields as arguments (as if
@AllArgsConstructor(access = AccessLevel.PACKAGE)is present on the class), and it is as if this constructor has been annotated with@Builderinstead. Note that this constructor is only generated if you haven't written any constructors and also haven't added any explicit@XArgsConstructorannotations. In those cases, lombok will assume an all-args constructor is present and generate code that uses it; this means you'd get a compiler error if this constructor is not present.
출처 - 공식 문서
클래스 단위에 @Builder를 붙이는 것은 @AllArgsContructor(access = PACKAGE)를 추가하고, 이로 인해 만들어진 생성자에 @Buider를 붙이는 것과 동일하다고 합니다.
이 방식의 단점에 대한 자세한 설명은 아래 블로그 링크에 잘 나와있습니다. 그렇기에 리뉴얼2 프로젝트에서는 클래스 단위가 아니라 직접 만든 생성자 단위에 @Builder 어노테이션을 붙이는 방식을 택하고 있습니다.
공식 문서를 통해 알아보는 @NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor + 사용시 주의사항(단점)
TODO: @Builer 어노테이션을 붙이고 생성된 클래스 파일을 IntelliJ로 바이트 코드 분석해보기
item 17