처음 스프링으로 개발을 할 때는 생성자를 사용한 객체 생성만 사용했다. 여러 코드를 보다 보니 자연스레 builder 패턴에 대해 알게 되었고 지금은 Builder 패턴으로만 객체를 생성하는 것 같다. 이때 @NoArgsConstructor, @AllArgsConstructor를 @Builder와 함께 항상 작성했는데 원리를 정확하게 알고 싶어 공부할 겸 글을 작성한다.
(builder와 생성자 방법의 차이점, builder의 장점 등은 생략한다. )
어노테이션 없이 builder 사용하기
@Builder를 사용하기 전에 어노테이션 없이 builder 패턴을 사용하는 예제를 확인하겠다
public class User {
private final String username;
private final String email;
private int age;
private User(Builder builder) { // 1. User의 생성자
this.username = builder.username;
this.email = builder.email;
this.age = builder.age;
}
public static class Builder {
private final String username;
private final String email;
private int age;
public Builder(String username, String email) { // 2. 이름과 이메일은 필수값으로 설정
this.username = username;
this.email = email;
}
public Builder age(int age) {
this.age = age;
return this;
}
public User build() { 3. builder 마무리하며 User 객체 생성
if (username == null || email == null) { // 에러 상황 작성
throw new IllegalStateException("username과 email은 필수값입니다.");
}
return new User(this);
}
}
}
1. User의 생성자에서 Builder로 설정 할 필드 값을 설정한다.
2. Builder 클래스를 만들어서 Builder 생성자에서 필수로 설정해야 하는 값이 있다면 설정한다. (필수로 설정해야 할 값이 없다면 Builder생성자는 작성하지 않아도 된다.)
3. 우리가 Builder 패턴을 사용할 때 마지막에 .build()로 끝내는 부분이다. 확인해 보니 내부에서 User의 생성자를 통해 User 객체를 return 한다.
모든 엔티티마다 Builder 클래스를 작성하기는 너무 귀찮으니 @Builder 어노테이션이 존재한다.
생성자에 @Builder 사용하기
나는 보통 @Builder를 클래스에 붙였다.(다른 분들도 지금까지는 그렇게 작성하는 것 같았다.) 하지만 이번에 찾아보면서 생성자를 만들어 @Builder를 붙이는 것을 보았다. 보다 보니 이 방법도 중요한 것 같았다.
클래스에 @Builder를 사용할 경우 편리하게 모든 필드 값을 설정할 수 있고 필요 없는 것은 따로 값을 설정하지 않아도 된다. 여기서 문제점이 생기는데 엔티티에는 @Id를 사용한 db에 저장될 id값을 설정한다. 이 말은 id값도 개발자가 코드로 설정할 수 있게 되는데 이는 매우 위험하다. 보통은 빌더를 사용할 때 알아서 id값을 제외하고 코드를 작성하지만 id를 임의로 개발자가 설정할 수 있는 옵션이 열려있다는 것 자체가 위험하다.
이렇게 기능 자체를 막고 싶다면 생성자에 @Builder를 작성해주면 된다.
@Getter
@Entity
@NoArgsConstructor
@Table(name = "User")
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(name = "student_number")
private String studentNumber;
private String password;
@Builder
public User(String name, String studentNumber, String password) {
// Assert.hasText(name, "name은 필수"); 필수값을 설정하고 싶다면 작성
this.name = name;
this.studentNumber = studentNumber;
this.password = password;
}
}
클래스에 @Builder 사용하기 (@NoArgsConstructor, @AllArgsConstructor와 사용하는 이유)
보통 많이 사용하는 방식은 class에 @NoArgsConstructor, @AllArgsConstructor, @Builder를 붙여 사용하는 방법이다.
이때 왜 생성자 관련 어노테이션을 작성해야하는지 알아봤다.
@Builder의 성질
우선 @Builder의 성질을 파악해야 한다. 공식 문서에 따르면 @Builder는 클래스 단위에 작성할 경우 해당 클래스에 @~ArgsConstructor 주석을 추가하지 않은 경우 (생성자 관련 어노테이션) 자동으로 @AllArgsConstructor를 생성해서 빌더 패턴을 적용한다고 한다. 그렇다면 @NoArgsConstructor을 작성 안 하면 되는 게 아닌가???
@NoArgsConstructor가 필요한 이유
아쉽지만 그럴 수 없다. 보통 스프링 프로젝트를 사용할 때는 JPA를 사용한다. 여기서 문제가 발생하는데 Jpa가 객체를 생성해서 값을 주입할 때 Reflection API를 사용한다. 이때 Reflection API는 기본 생성자로 객체를 생성 한 후에 값을 매핑한다.
그러므로 JPA를 사용하려면 엔티티로 사용되는 클래스에 @NoArgsConstructor는 필수이다.
(추가적인 내용으로 @NoArgsConstructor은 public or protected이어야 한다. 이유는 우리가 jpa의 지연 로딩을 사용할 때 필요하기 때문이다. 지연 로딩으로 객체에 접근할 경우 프록시 객체를 생성한다. 프록시 객체는 실제 엔티티 클래스를 상속받은 객체이므로 실제 엔티티 클래스의 기본 생성자를 호출해야 한다. -> private 사용하면 지연 로딩 사용 불가)
결론
1. JPA를 사용하려면 엔티티로 사용될 클래스에 @NoArgsConstrcutor 필수
2. Builder를 사용하려면 매개 변수가 있는 생성자가 필요한데 @Builder는 클래스에 생성자 관련 어노테이션이 있을 경우 추가적인 생성자를 생성하지 않음
3. 그러므로 @NoArgsConstructor, @AllArgsConstructor, @Builder를 함께 사용한다.
4. (@AllArgsConstructor를 사용하기 때문에 빌더를 사용해 모든 필드의 값을 설정할 수 있다. 이것이 싫다면 접근이 가능한 필드 값을 매개변수로 갖는 생성자를 만들고 해당 생성자에 @Builder를 붙인다.)
'Spring' 카테고리의 다른 글
@ModelAttribute, @SessionAttribute name 속성 차이 (0) | 2023.09.26 |
---|---|
[Spring] 나만 보는 스프링 사전(스프링 어노테이션) (0) | 2023.06.30 |
[Spring JPA] 영속성 컨텍스트 완벽 이해!, 연관 관계 주인 mapping (cascade와 차이?) (0) | 2023.03.30 |
[Spring] JPA란? (간단정리) (0) | 2023.02.04 |
[Spring] @JoinColumn & 연관관계 맵핑 @Mappedby (외래키 주인? 연관관계 주인?) (1) | 2023.01.13 |