Post

@Data vs 개별 어노테이션 — JPA 복합 키 클래스의 트레이드오프

JPA @IdClass 복합 키 클래스에서 @Data의 편의성과 @Setter 제거 사이의 트레이드오프를 정리합니다. adapter 인프라 클래스에 한해 @Data 수용이 합리적인 이유를 설명합니다.

@Data vs 개별 어노테이션 — JPA 복합 키 클래스의 트레이드오프

TL;DR JPA @IdClass 복합 키 클래스에서 @Data의 편의성을 수용할 것인가, @Setter 제거를 위해 어노테이션을 개별 선언할 것인가의 트레이드오프입니다. adapter 인프라 클래스에 한해 @Data 수용은 합리적 판단입니다. 단, @Data는 @NoArgsConstructor를 포함하지 않으므로 반드시 명시해야 합니다.

핵심 원리

Lombok @Data가 생성하는 것

@Getter + @Setter + @EqualsAndHashCode + @ToString + @RequiredArgsConstructor

주의: @RequiredArgsConstructor ≠ @NoArgsConstructor

  • @RequiredArgsConstructor는 final/@NonNull 필드 대상 생성자를 생성합니다.
  • final 필드가 없으면 매개변수 없는 생성자가 만들어지지만, 이것은 @NoArgsConstructor와 동일하지 않습니다.
  • Hibernate는 @IdClass/@Embeddable 클래스에 명시적 no-arg constructor를 요구합니다.
  • @Data + @AllArgsConstructor만으로는 Hibernate가 인스턴스를 생성하지 못해 런타임 에러가 발생합니다.

실제 장애 사례: Unable to locate constructor for embeddable 'CompositeKey' @Data + @AllArgsConstructor만 선언하고 @NoArgsConstructor를 누락하여 배치 잡이 실패.

@IdClass vs @EmbeddedId 선택 기준

항목@IdClass@EmbeddedId
@GeneratedValue지원 (필드가 엔티티에 직접 선언)JPA 스펙 미보장 (@Embeddable 내 비표준)
필드 접근entity.getId() (flat)entity.getId().getId() (중첩)
Record 사용불가Hibernate 6+ 지원

결정적 기준: PK에 @GeneratedValue가 있으면 @IdClass가 유일한 선택지입니다.

@IdClass에서 Record를 쓸 수 없는 이유

JPA 스펙은 @IdClass에 no-arg constructor를 요구합니다. Hibernate 구현체는 no-arg constructor로 인스턴스 생성 후 리플렉션으로 필드를 세팅하는 순서로 동작합니다.

Java 리플렉션 자체는 Record의 final 필드도 setAccessible(true)로 강제 세팅 가능하지만, Hibernate가 @IdClass에 대해 이 방식을 지원하지 않습니다. 기술적 제약이 아니라 구현체의 선택입니다.

실무 적용

어노테이션 비교

방식어노테이션 수Setter
@Data + @NoArgsConstructor + @AllArgsConstructor3개있음
개별 선언 (@Getter + @EqualsAndHashCode + @ToString + @NoArgsConstructor + @AllArgsConstructor)5개없음

판단 기준

  • no setter 원칙의 본래 목적은 도메인 불변식 보호입니다.
  • @IdClass 키 클래스는 adapter 계층의 인프라 코드이며, 비즈니스 불변식이 없습니다.
  • setter 호출처가 존재하지 않으므로 실질적 위험이 없습니다.
  • 미끄러운 경사면(adapter에서 domain으로 확산) 위험은 헥사고날 아키텍처 경계로 통제됩니다.

결론: adapter 인프라 클래스에 한해 @Data 편의성 수용은 합리적 트레이드오프입니다.

코드 예시

// @IdClass 키 클래스 - @Data 사용
// ⚠️ @NoArgsConstructor 필수 — @Data는 이를 포함하지 않음
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderItemKey implements Serializable {
  private Long id;
  private LocalDate orderDate;
}

// 엔티티 - @GeneratedValue 때문에 @IdClass 필수
@IdClass(OrderItemKey.class)
@Entity
public class OrderItemEntity {
  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  @Id
  private LocalDate orderDate;
}

이 글은 Claude의 도움을 받아 작성했습니다.

This post is licensed under CC BY 4.0 by the author.