1. @Builder 적용 및 사용 방법
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoEntity {
private String id; // 이 오브젝트의 아이디
private String userId; // 이 오브젝트를 생성한 사용자의 아이디
private String title; // Todo 타이틀(예: 운동하기)
private boolean done; // true - todo를 완료한 경우(checked)
}
클래스에 @Builder 어노테이션을 사용하면 builder 패턴을 적용해서 객체를 생성할 수 있다.
** 참고 : 위와 같이 Entity에 @Builder, @NoArgsConstructor, @AllArgsConstructor, @Data 사용 하면 안된다. 간단히 실행하려고 Entity를 직접 사용하는데 실제로는 DTO 등 다른 방법으로 Entity에 접근해야한다.
❌ Entity 사용에 피해야 할 어노테이션
1. @Setter
- 이유:
- 엔터티 클래스는 데이터 무결성을 보장해야 함.
- 필드 값을 외부에서 자유롭게 변경할 경우 예기치 않은 상태 변화를 유발할 수 있음.
- JPA의 영속성 컨텍스트와 충돌할 위험이 있음.
- 대안:
- 필드에 대한 직접적인 setter를 제공하지 않고, 생성자 또는 비즈니스 메서드를 통해 값 변경.
2. @Data (Lombok)
- 이유:
- @Data는 @Getter, @Setter, @EqualsAndHashCode, @ToString 등을 포함하는데, @Setter를 포함하므로 사용하면 안 됨.
- @EqualsAndHashCode가 연관 관계 필드를 포함할 경우 무한 루프 발생 가능.
- @ToString이 연관 관계 필드를 포함하면 Lazy Loading 문제 발생 가능.
- 대안:
- @Getter만 사용하거나 필요한 필드에만 별도로 @EqualsAndHashCode, @ToString을 작성.
3. @Builder (생성자와 필드에 적용할 경우)
- 이유:
- JPA에서 기본 생성자가 필요하지만, @Builder는 기본적으로 모든 필드를 포함하는 private 생성자를 생성하여 JPA가 사용하지 못함.
- 대안:
- 필드에 @Builder를 적용하지 않고, DTO에서 사용하거나 별도의 정적 메서드(of 메서드)를 만들어 사용.
4. @AllArgsConstructor (모든 필드를 포함한 생성자)
- 이유:
- JPA에서 프록시 객체를 활용하는데 기본 생성자가 필요하며, 모든 필드를 초기화하는 생성자를 만들면 JPA가 객체를 관리하기 어려움.
- 대안:
- @NoArgsConstructor(access = AccessLevel.PROTECTED) + @RequiredArgsConstructor 사용.
5. @Transactional (Entity 클래스 내부에서 사용)
- 이유:
- Entity 클래스는 비즈니스 로직이 아닌, 데이터 모델을 표현하는 역할이므로 트랜잭션 관련 로직을 포함하면 안 됨.
- @Transactional은 서비스 계층에서 처리해야 함.
6. @Component, @Service, @Repository 등 스프링 빈 등록 어노테이션
- 이유:
- 엔터티 클래스는 단순한 데이터 모델로, 스프링 빈으로 관리될 필요가 없음.
- 스프링 컨테이너에서 엔터티를 관리하면, JPA의 영속성 컨텍스트와 충돌할 가능성이 있음.
7. @GeneratedValue(strategy = GenerationType.IDENTITY) (IDENTITY 전략을 과도하게 사용)
- 이유:
- IDENTITY 전략은 즉시 insert 쿼리를 실행해야 하므로, 영속성 컨텍스트가 효율적으로 작동하지 않음.
- SEQUENCE나 TABLE 전략이 더 권장됨.
- 대안:
- @GeneratedValue(strategy = GenerationType.SEQUENCE)
8. @JsonIgnore (직렬화 문제)
- 이유:
- REST API에서 Entity 클래스를 JSON 직렬화할 때 특정 필드를 제외하기 위해 사용하면 안 됨.
- API 응답을 위한 DTO 클래스를 따로 만들어야 함.
- 대안:
- DTO를 사용하여 필요한 필드만 직렬화.
9. @Column(nullable = false) (모든 필드에 적용)
- 이유:
- JPA의 기본 설정을 과도하게 변경할 경우 유지보수성이 낮아짐.
- 필드에 따라 다르게 적용해야 하므로, 무조건 붙이는 것은 비효율적임.
✅ 권장하는 엔터티 작성 방법
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(nullable = false, length = 50)
private String name;
@Column(nullable = false, unique = true)
private String email;
// 연관 관계 필드: toString, equalsAndHashCode에서 제외해야 함
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
// 생성자 대신 정적 메서드 제공
public static User createUser(String name, String email) {
User user = new User();
user.name = name;
user.email = email;
return user;
}
}
🚀 추가 고려할 사항
- DTO와 엔터티 분리
- API 요청과 응답을 위한 DTO를 만들어야 엔터티를 외부에 노출하지 않음.
- 비즈니스 로직을 서비스 계층에서 처리
- 엔터티 클래스는 데이터 구조만 정의하고, 트랜잭션 로직은 서비스 계층에서 처리.
- Setter를 사용하지 않고, 비즈니스 메서드 활용
- user.updateName("새로운 이름"); 형태로 명확한 메서드를 제공.
🔥 결론
Setter, @Data, @Builder, @AllArgsConstructor, @Transactional, @Component 같은 어노테이션은 엔터티 클래스에서 사용하지 않는 것이 좋다. 엔터티는 데이터 모델 역할만 해야 하며, 데이터 변경은 비즈니스 로직을 통해 처리하는 것이 바람직하다.
TodoEntity todo = TodoEntity.builder()
.id("1")
.userId("user123")
.title("운동하기")
.done(false)
.build();
이렇게 사용하면 @Builder가 자동으로 생성하는 TodoEntityBuilder 내부 클래스를 활용해 객체를 만들 수 있음.
2. 내부적으로 생성되는 빌더 클래스
@Builder
public class TodoEntity {
private String id;
private String userId;
private String title;
private boolean done;
// 📌 Lombok이 내부적으로 생성하는 정적 빌더 클래스
public static class TodoEntityBuilder {
private String id;
private String userId;
private String title;
private boolean done;
public TodoEntityBuilder id(String id) {
this.id = id;
return this;
}
public TodoEntityBuilder userId(String userId) {
this.userId = userId;
return this;
}
public TodoEntityBuilder title(String title) {
this.title = title;
return this;
}
public TodoEntityBuilder done(boolean done) {
this.done = done;
return this;
}
public TodoEntity build() {
return new TodoEntity(id, userId, title, done);
}
}
// 📌 빌더 객체를 생성하는 정적 메서드
public static TodoEntityBuilder builder() {
return new TodoEntityBuilder();
}
}
위의 @Builder 어노테이션을 사용하면 Lombok이 내부적으로 아래와 같은 빌더 클래스를 생성한다. 즉, TodoEntity.builder()를 호출하면 TodoEntityBuilder 객체가 생성되고, 체이닝 방식으로 필드를 설정한 후 build()를 호출하면 최종 객체가 생성됨.
실제 Outline을 살펴보면 내부적으로 builder( ) static 메소드와 TodoEntityBuilder static 클래스가 생성된 것을 볼 수 있다. 앞에 빨간색 글씨로 S가 붙여져 있으면 static 정적을 나타낸다.
3. 빌더 클래스의 위치 및 구조
Lombok이 @Builder를 처리하면 컴파일 타임에 TodoEntity 내부에 TodoEntityBuilder라는 정적 클래스가 생성된다. 즉, 빌더 클래스는 TodoEntity 내부에 존재하는 static class이며, 외부에서 TodoEntity.builder()를 통해 접근할 수 있음.
1️⃣ TodoEntityBuilder는 TodoEntity 내부에서 static class로 자동 생성됨.
→ @Builder가 적용된 클래스의 필드들을 모아서 build() 메서드를 통해 객체를 생성하는 역할.
2️⃣ 외부에서 TodoEntity.builder()를 호출하면 TodoEntityBuilder 객체가 생성됨.
→ TodoEntityBuilder는 일반적으로 public static으로 선언되며, builder() 메서드를 통해 접근할 수 있음.
3️⃣ 빌더 패턴을 통해 new 없이 객체를 생성할 수 있음.
TodoEntity todo = TodoEntity.builder()
.id("1")
.userId("user123")
.title("운동하기")
.done(false)
.build();
→ 내부적으로 new TodoEntityBuilder()를 호출하여 객체를 생성하고, build() 호출 시 new TodoEntity()가 실행됨.
🚫 잘못된 예제 (체이닝 불가능)
public TodoEntityBuilder id(String id) {
this.id = id;
// return this; ❌ 반환값이 없으면 체이닝 불가능
}
❌ 위 코드에서는 .id("1") 호출 후 반환값이 없으므로 .userId("user123")를 이어서 호출할 수 없음.
✔ 반드시 return this;를 사용해야 체이닝 방식이 가능함.
'Error > Java' 카테고리의 다른 글
[Getter] Boolean, Boolean Getter 메서드 이름 규칙 (0) | 2024.12.02 |
---|---|
cmd, console "has been compiled by a more recent version" 에러 (0) | 2024.02.04 |