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;
    }
}

🚀 추가 고려할 사항

  1. DTO와 엔터티 분리
    • API 요청과 응답을 위한 DTO를 만들어야 엔터티를 외부에 노출하지 않음.
  2. 비즈니스 로직을 서비스 계층에서 처리
    • 엔터티 클래스는 데이터 구조만 정의하고, 트랜잭션 로직은 서비스 계층에서 처리.
  3. 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;를 사용해야 체이닝 방식이 가능함.

 

 

+ Recent posts