프로젝트를 진행하면서 아무런 생각없이 @Builder, @AllArgsConstructor, @NoArgsConstructor 어노테이션을 같이 사용해왔다. 아무리 롬복이 편리함을 제공해주지만, 제대로 알지 않고 사용하면 독이 된다고 생각하여 이번에 정리해보고자 한다.
우선 빌더 패턴을 사용하는 이유부터 간단하게 알아보자. 구글링을 하면 다음과 같은 장점에 대해서 기술되어있다.
하단에 첨부한 링크에서 잘 설명이 되어있으니 해당 내용에 대해서는 과감하게 생략하자.
결국, 빌더 패턴을 사용하면 협업을 하거나 변경에 대한 요구 사항이 많은 경우에 대해 손쉽게 대처할 수 있다는 것이다.
이 부분은 직접 생각해본 내용인데, @AllArgsConstructor 어노테이션을 이용하거나 모든 필드를 파라미터로 가지는 생성자가 있을 경우에서 발생할 수 있는 문제를 해결할 수 있다고 생각했다.
@AllArgsConstructor는 선언된 필드의 순서대로 생성자를 생성해준다. 그리고 이를 이용하여 순서대로 파라미터를 지정하여 객체를 만든다고 가정했을 때, 제 3자가 선언된 필드 사이에 임의의 필드를 넣는다면 객체를 만드는 코드에서도 수정이 일어나야하며 협업하는 부분에서 문제가 발생한다.
예를 들어보면 다음과 같다.
@AllArgsConstructor
public class User {
private String name;
private String phone;
private int age;
}
User user = new User("kevin", "010-0000-0000", "29");
@AllArgsConstructor
public class User {
private String name;
private String gender;
private String phone;
private int age;
}
// User user = new User("kevin", "010-0000-0000", "29"); -> 문제 발생!
User user = new User("kevin", "M", "010-0000-0000", "29");
@NoArgsConstructor도 알아보자. @NoArgsConstructor는 기본 생성자를 생성해주는 어노테이션이다. 그리고 대부분의 코드에서는 AccessLevel.PROTECTED
속성을 부여하는데, 이는 무분별한 객체 생성에 대해 한번 더 체크할 수 있는 수단으로 설정해준다고 한다.
해당 속성을 부여하지 않았을 때의 문제를 살펴보자.
하단의 코드처럼 Setter를 이용하여 클래스 필드에 대한 값을 설정할 수 있을 때, 모든 필드가 값을 가져야만하는 상황이라고 가정해보자.
개발자의 실수로 클래스의 필드들 중 하나의 필드에 대한 값 설정을 누락시켰을 경우 객체는 불완전한 상태가 되어버린다.
@Getter
@Setter
@NoArgsConstructor
public class User {
private String name;
private String gender;
private String phone;
private int age;
}
User user = new User();
user.setName("kevin");
user.setGender("M");
user.setPhone("010-0000-0000");
하지만, AccessLevel.PROTECTED
속성을 부여해주면 기본 생성자의 접근 제어가 되어 IDE 단계에서 누락을 방지할 수 있어 위의 문제를 해결할 수 있다. 즉, 기본 생성자의 생성을 방지하고 지정한 생성자를 사용하도록 강제하여 무조건 완전한 상태의 객체를 생성할 수 있도록 도움을 준다는 의미다.
@Getter
@Setter
@NoArgsConstructor
public class User {
private String name;
private String gender;
private String phone;
private int age;
public User(String name, String phone, int age) {
this.name = name;
// Gender Default
this.gender = "M";
this.phone = phone;
this.age = age;
}
}
User user = new User("kevin", "010-0000-0000", "29");
@NoArgsConstructor로 기본 생성자의 생성을 방지하고 @Builder를 이용하여 객체의 생성에 유연성을 더 해주면 좋겠지만, 이 두 개의 어노테이션을 함께 사용하려면 @AllArgsConstructor가 필요하다.
이유는 @Builder를 조금 더 살펴보면 이해할 수 있다. @Builder는 생성자 유무에 따라 다음과 같이 동작한다.
즉, 기본 생성자가 존재하기 때문에 @Builder에서는 생성자를 별도로 생성하지 않는데, 이 기본 생성자에는 접근 제한 속성이 부여되어있어 문제가 발생하는 것이다. 이 두 개의 어노테이션을 부여한 클래스를 컴파일 해보면 문제가 발생하는 위치를 쉽게 확인할 수 있다.
public class User {
private String name;
private String gender;
private String phone;
private int age;
// @NoArgsConstructor(access = AccessLevel.PROTECTED)로 생성된 생성자
protected User() {}
public static User.UserBuilder builder() {
return new User.UserBuilder();
}
public static class UserBuilder {
private String name;
private String gender;
private String phone;
private int age;
UserBuilder() {
}
public User.UserBuilder name(String name) {
this.name = name;
return this;
}
public User.UserBuilder gender(String gender) {
this.gender = gender;
return this;
}
public User.UserBuilder phone(String phone) {
this.phone = phone;
return this;
}
public User.UserBuilder age(int age) {
this.age = age;
return this;
}
public User build() {
/// 일치하는 생성자가 없어 문제 발생
return new User(this.name, this.gender, this.phone, this.age);
}
}
}
결국 문제는 일치하는 생성자가 없어 발생하는 문제이니 생성자를 만들어주면 되는게 아닌가?! 이 말을 다른 말로 하자면, 모든 필드를 파라미터로 가지는 @AllArgsConstructor 하나만 추가해주면 해결할 수 있다는 의미다.
개인적으로는 @AllArgsConstructor를 이용하는 편이 쉽고 코드의 길이도 줄여줄 수 있어 선호하는 편이지만, @AllArgsConstructor만이 유일한 해결책은 아니다.
또 다른 방안으로는 직접 생성자를 만들어주고, 빌더 패턴에서 해당 생성자를 사용하도록 @Builder를 추가해주면 된다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
private String name;
private String gender;
private String phone;
private int age;
@Builder
public User(String name, String phone, int age) {
this.name = name;
// Gender Default
this.gender = "M";
this.phone = phone;
this.age = age;
}
@Builder
public User(String name, String gender, String phone, int age) {
this.name = name;
this.gender = gender;
this.phone = phone;
this.age = age;
}
}
생성자에 @Builder를 붙여 컴파일 해보면 다음과 같다.
public class User {
private String name;
private String gender;
private String phone;
private int age;
public User(String name, String phone, int age) {
this.name = name;
// Gender Default
this.gender = "M";
this.phone = phone;
this.age = age;
}
public User(String name, String gender, String phone, int age) {
this.name = name;
this.gender = gender;
this.phone = phone;
this.age = age;
}
// @NoArgsConstructor(access = AccessLevel.PROTECTED)로 생성된 생성자
public static User.UserBuilder builder() {
return new User.UserBuilder();
}
protected User() {
}
public static class UserBuilder {
private String name;
private String gender;
private String phone;
private int age;
UserBuilder() {
}
public User.UserBuilder name(String name) {
this.name = name;
return this;
}
public User.UserBuilder gender(String gender) {
this.gender = gender;
return this;
}
public User.UserBuilder phone(String phone) {
this.phone = phone;
return this;
}
public User.UserBuilder age(int age) {
this.age = age;
return this;
}
public User build() {
return new User(this.name, this.gender, this.phone, this.age);
}
}
}