간단한 질문과 답변으로 이루어진 게시판을 만드는 과정에서 질문 엔티티인 Question에 Builder패턴을 적용하는 과정에서 발생한 오류에 관한 게시물입니다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
private LocalDateTime createDate;
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
}
Question는 엔티티 객체이므로 JPA의 기본 스펙상 protected접근자의 기본 생성자를 Lombok의 @NoArgsConstructor애노테이션을 이용해서 추가해주었다.
@Getter
@Setter
public class QuestionDto {
private Integer id;
private String subject;
private String content;
private LocalDateTime createDate;
}
게시판 구현을 진행하면서 Question 엔티티를 QuestionDto로 변경하는 과정에서는 ModelMapper를 이용해서 쉽게 할 수 있었지만, 그 반대 과정인 DTO를 Entity로 바꿔주는 기능이 필요하게 되었다.
검색을 통해서 DTO를 Entity로 변경하는 메서드는 일반적으로 toEntity라는 이름으로 DTO에 만들어주고 Lombok라이브러리의 @Builder애노테이션을 이용해서 간편하게 구현할 수 있음을 알게되었다.
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
@Builder.Default
private LocalDateTime createDate = LocalDateTime.now();
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
}
@Builder애노테이션을 추가해주면 바로 동작할 줄 알았지만 실행결과 아래와 같은 에러가 발생했다.
에러 메시지를 번역해보면 required : no arguments 아무 인자값을 필요로 하지 않는데, found : java.lang.Integer, ....이 발견되었다고 한다.
처음에는 이 오류가 도대체 왜 발생하는지 알 수가 없었다. 그래서 @Builder가 어떻게 동작하는지 찾아보았다.
@Builder
public class BuildMe {
private String username;
private int age;
}
username과 age 두개의 필드를 가진 BuildMe클래스에 @Builder 애노테이션이 붙으면 아래의 코드와 같다.
public class BuildMe {
private String username;
private int age;
BuildMe(String username, int age) {
this.username = username;
this.age = age;
}
public static BuildMe.BuildMeBuilder builder() {
return new BuildMe.BuildMeBuilder();
}
public static class BuildMeBuilder {
private String username;
private int age;
BuildMeBuilder() {
}
public BuildMe.BuildMeBuilder username(String username) {
this.username = username;
return this;
}
public BuildMe.BuildMeBuilder age(int age) {
this.age = age;
return this;
}
public BuildMe build() {
return new BuildMe(this.username, this.age);
}
public String toString() {
return "BuildMe.BuildMeBuilder(username=" + this.username + ", age=" + this.age + ")";
}
}
}
@Builder애노테이션 붙은 클래스의 이름을 Target이라고 했을때@Builder애노테이션의 동작과정을 순서대로 나열하면 다음과 같다.
Target클래스의 모든 필드값을 인자값으로 하는 생성자를 추가한다.Target클래스 내부에Target.TargetBuilder라는 이름으로Target과 같은 필드값을 가지는 내부 클래스를 만든다.Target클래스의builder()메서드를 호출하여 내부 클래스를 생성한다.Target클래스의 필드명과 같은 내부 클래스의 메서드를 호출하여Target클래스와 대응되는 내부 클래스의 필드를 초기화한다.- 내부 클래스의
build()메서드를 호출하면 0번 과정에서 만든 생성자를 호출하여Target클래스를 생성한다.
어떻게 동작하는지 자세히 알면 더 좋겠지만 여기서 확인해야할 것은 @Builder애노테이션을 적용하면 적용한 클래스의 모든 필드를 인자값으로 하는 생성자가 자동으로 추가된다는 것이다.
앞서 간단하게 @Builder 애노테이션이 어떻게 동작하는지 확인해보았고 @Builder애노테이션이 붙은 클래스에는 모든 필드값을 인자값을 가지는 생성자가 추가됨을 알 수 있었다.
이제 발생한 오류를 다시 한번 보자.
다시보니 발생한 오류의 found 부분은 위 동작과정의 4번과정에서 Target의 클래스를 생성하는 과정에서 발생한 오류인 것 같다!
즉, 위의 오류 메시지의 의미는 Target클래스의 모든 필드를 인자값으로 하는 생자를 호출했는데 왠진 모르겠지만 모든 필드값을 가지는 생성자를 인식하지 못하였고 @NoArgsConstructor에서 생성한 기본생성자만을 인식해서 발생한 오류임을 알 수 있다.
그런데 뭔가 이상하다..
이전에 @Builder가 클래스에 붙으면 해당 클래스의 모든 필드값을 인자값으로 하는 생성자가 자동으로 추가된다고 했는데 뭐지? @Builder애노테이션에 에러가 있나?
@Builder 애노테이션을 열어보면 주석에 다음과 같은 내용이 포함되어 있다.
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 @Builder instead. Note that this constructor is only generated if you haven't written any constructors and also haven't added any explicit @XArgsConstructor annotations. 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.
그대로 번역하면
클래스에 주석이 달린 경우 모든 필드를 인수로 사용하여 패키지 전용 생성자가 생성되며(마치 @AllArgsConstructor(access = AccessLevel.PACKAGE)가 클래스에 있는 것처럼) 이 생성자에 @ 주석이 달린 것과 같습니다. 대신 빌더. 이 생성자는 생성자를 작성하지 않았고 명시적인 @XArgsConstructor 주석을 추가하지 않은 경우에만 생성됩니다. 이러한 경우 lombok은 모든 인수를 갖는 생성자가 있다고 가정하고 이를 사용하는 코드를 생성합니다. 이는 이 생성자가 없으면 컴파일러 오류가 발생한다는 것을 의미합니다.
그렇다 @Builder 애노테이션과 @NoArsgConstructor을 클래스에 같이 적용했기때문에 @Builder 애노테이션에서 자동으로 모든 필드 값을 인자값으로하는 생성자를 만들지 않았고.. lombok은 모든 인수를 갖는 생성자가 있다고 가정하고 코드를 생성해서 컴파일 에러가 난 것이다..
해결 방법은 너무나 간단하다.
@Builder와 @XArgsConstructor를 같이 사용할때 적용한 클래스의 모든 필드를 인자로 받는 생성자를 만들어주면 끝이다. 즉, lombok의 @AllArgsConstructor를 추가해주면 끝이다.
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(length = 200)
private String subject;
@Column(columnDefinition = "TEXT")
private String content;
@Builder.Default
private LocalDateTime createDate = LocalDateTime.now();
@OneToMany(mappedBy = "question", cascade = CascadeType.REMOVE)
private List<Answer> answerList;
}
외부 라이브러리를 적용할때 무작정 갖다 쓰지말고 제대로 알아보고 사용하자.
[Spring Boot] dto의 toEntity를 어떻게 사용해야할까?
Lombok @Builder의 동작 원리
Entity 는 어떻게 작성해야 하나요? (JPA)
[Spring] Lombok @Builder 기본값에 관하여