왜 Java Records를 JPA 엔티티로 사용할 수 없는가
Source: Dev.to
소개
Java Records(공식적으로 Java 16에 도입) 덕분에 개발자들은 equals(), hashCode(), toString() 같은 보일러플레이트 코드를 작성하지 않고도 불변 데이터 캐리어 클래스를 간결하게 만들 수 있게 되었습니다.
당연히 우리는 방대한 JPA 엔티티들을 보면서 다음과 같이 생각했습니다:
“이것들을 레코드로 바꿔서 코드를 더 깔끔하게 만들 수 있을까?”
짧게 답하면 아니오 입니다.
레코드가 JPA와 호환되지 않는 이유
불변성 vs. 가변성
- 레코드 철학 – 모든 필드는 암묵적으로
final이며, 인스턴스가 생성된 뒤에는 상태를 변경할 수 없습니다. - JPA 요구사항 – 엔티티는 가변이어야 합니다. JPA는 데이터베이스에서 엔티티를 로드하고, 필드를 변경한 뒤, 그 변경을 영속화함으로써 엔티티의 상태를 관리합니다.
가변성에 의존하는 주요 JPA 메커니즘:
- 더티 체킹 – Hibernate는 객체의 현재 상태와 원본 스냅샷을 비교합니다.
- 세터 – JPA는 데이터베이스에서 읽어올 때 세터(또는 필드 접근)를 사용해 엔티티 필드를 채웁니다.
레코드에는 세터가 없고 필드가 final이기 때문에 JPA는 레코드의 라이프사이클을 관리하거나 상태를 업데이트할 수 없습니다.
지연 로딩과 프록시
JPA의 가장 강력한 기능 중 하나는 지연 로딩으로, 관련 데이터를 실제로 접근할 때만 가져옵니다. 이를 구현하기 위해 Hibernate와 같은 제공자는 프록시 클래스를 생성합니다:
- 원본 엔티티 클래스를 상속한다.
- getter 메서드를 오버라이드하여 필요 시 데이터베이스 호출을 트리거한다.
레코드는 final 클래스이므로 서브클래싱이 불가능합니다. 따라서 JPA는 레코드에 대한 프록시를 만들 수 없으며, 지연 로딩(및 기타 프록시 기반 기능)은 완전히 깨집니다.
대안: Lombok으로 엔티티를 간결하게 유지하기
레코드는 JPA 엔티티에 적합하지 않지만, 일반 클래스를 사용할 때 Lombok을 이용하면 보일러플레이트를 줄일 수 있습니다.
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import lombok.Getter;
import lombok.Setter;
import lombok.NoArgsConstructor;
import lombok.AccessLevel;
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 기본 생성자 접근을 제한
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private String email;
}
Note: 다음 레코드 정의는 레코드가 불변이고 final이기 때문에 JPA와 충돌을 일으킵니다.
// This will cause issues with JPA!
public record User(Long id, String name, String email) {}
결론
- 표준 클래스(선택적으로 Lombok 사용) 를 JPA 엔티티에 사용하여 가변성, 기본 생성자, non‑final 클래스 요구사항을 만족시킵니다.
- 레코드는 JPA 영속성 기능이 필요 없는 DTO와 같은 불변 데이터 캐리어에 사용합니다.