Java 와 Spring 프레임워크 생태계를 실무에서 사용하는 웹 백엔드 개발자들이 JPA 를 사용할때 종종하는 실수들이 있습니다.
백엔드 개발자라면 JPA 를 사용할 때 이정도는 알고 쓰자는 의미에서 글을 작성합니다.
JPA 구현체로는 Hibernate 를 사용하고 DB 는 MySQL 을 사용하는 것을 전제로 합니다.
엔티티 설계
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 기본 생성자 보호
@AllArgsConstructor(access = AccessLevel.PRIVATE) // AllArgsConstructor도 보호
@Builder
@Table(name = "order")
public class Order {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String orderNumber;
@Enumerated(EnumType.STRING)
private OrderStatus status;
public void cancelOrder() {
if (this.status == OrderStatus.CANCELLED) {
throw new IllegalStateException("Already cancelled");
}
this.status = OrderStatus.CANCELLED;
}
}
- JPA 엔티티는 protected 기본 생성자를 사용하자
- 기본 생성자가 필요한 이유
- JPA 는 DB에서 조회 시 프록시 객체를 통해 리플렉션을 사용하여 엔티티 객체를 생성하고 영속성 컨텍스트에서 값을 채울 때 기본 생성자를 사용합니다.
- 따라서, private 접근 제어 지시자를 제외한 외부에서 접근할 수 있는 기본 생성자가 필요한데요.
- 객체의 일관성을 보장하기 위해 protected 기본 생성자를 사용
- public 기본 생성자가 있다면, 외부에서 아무 값도 없는 엔티티 객체를 만들 수 있게 됩니다.
- 다행히 JPA 는 protected 기본 생성자만으로도 정상적으로 동작합니다.
- 기본 생성자가 필요한 이유
- 객체 생성 시 정적 팩토리 메서드와 Builder 패턴을 사용하자
- protected 기본 생성자로 클래스 내의 정적 팩토리 메서드 안에서 객체 생성이 가능합니다.
- 여기에 추가적으로 Builder 패턴을 이용하여 객체 생성 시 가독성 좋게 표현하고 필드를 유연하게 설정할 수 있게 구현합니다.
public static Order newOne(String orderNumber) {
return Order.builder()
.orderNumber(orderNumber)
.build();
}
- 식별자(ID)는 자연키가 아니라 대리키를 사용하자
- 의미있는 자연키 쓰지말고, 의미없는 대리키인 Long id 를 사용하는 것을 추천합니다.
- 자연키 VS 대리키 차이점
- 자연키 -> 비즈니스적으로 의미가 있습니다.
- EX. 주민등록번호, 이메일, 휴대폰 번호
- 대리키 -> 비즈니스적으로 의미가 없습니다.
- EX. Auto Increment ID, UUID
- 자연키 -> 비즈니스적으로 의미가 있습니다.
- 대리키를 사용해야 하는 이유
- 휴대폰 번호, 이메일 등 자연키는 언제든 변할 수 있습니다.
- 엔티티의 식별자는 변하지 않는 값 이어야 하므로 자연키는 적합하지 않습니다.
- @GeneratedValue strategy 설정
- @GeneratedValue 만 선언(AUTO로 설정)하게 되면 strategy 가 TABLE 로 설정됩니다. 따라서 기본 키 생성을 위한 별도 테이블을 사용하므로 성능상 이슈가 있을 수 있으니 확인해봐야 합니다.
- MySQL 을 사용한다면 보통은 GenerationType.IDENTITY 로 설정하면 충분합니다.
- 다만, 해당 엔티티에 Batch Insert 가 많다면 IDENTITY 전략은 ID 생성을 DB 에 위임 하기 때문에 MySQL 에 auto increment 시키고 그 값을 가져와 다시 쓰는 Transactional Write Behind 방식으로 동작하기 때문에 Insert 성능이 매우 떨어집니다. 제가 알기로는 Batch Insert 건수가 많을수록 90% 이상 성능상 손해를 봅니다.
- GenerationType.IDENTITY 전략을 사용하면서 Batch Insert 성능을 해결할 방법이 있습니다.. JDBCRepository 를 활용하여 JdbcTemplate 으로 해결가능하니 그냥 GenerationType.IDENTITY 쓰시길 권장합니다.
- equals(), hashCode() overriding 은 ID 기반으로 구현
- 오로지 해당 엔티티의 ID 값만으로 구현합니다.
- 그냥 맘편하게 lombok 의 @EqualsAndHashCode(of = "id") 사용해도 무방합니다.
- 만약 JPA 프록시 객체와 비교할 경우가 있다면 equals() 에서 getClass() 가 아니라 instancOf() 로 구현합니다.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order)) return false; // getClass() 대신 instanceof 사용
Order order = (Order) o;
return Objects.equals(id, order.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
오늘은 JPA Entity 엔티티의 기본적인 설계에 대해 이야기 했습니다.
다음은 JPA Entity 에서 값 객체를 사용하는 방법에 대해 이야기 하도록 하겠습니다.
감사합니다~!
'개발 > JAVA' 카테고리의 다른 글
Java 개발자가 JPA 를 쓸 때 이정도는 알고 쓰자 - 엔티티 기본 설계3 (0) | 2025.02.18 |
---|---|
Java 개발자가 JPA 를 쓸 때 이정도는 알고 쓰자 - 엔티티 기본 설계2 (0) | 2025.02.13 |