PK, Id 생성 전략
진행중인 프로젝트에서는 JPA Entity 클래스의 Id 필드가 GenerationType.IDENTITY가 아닌 경우도 있습니다.
class QuestionPaymentEntity(
@Id var orderId: String,
@Column var userId: Long,
@Column var userCouponId: Long?,
@Column var amount: Int,
@Enumerated(EnumType.STRING) @Column var status: QuestionPaymentStatus,
@Column var createdAt: LocalDateTime
)
class UserCouponEntity private constructor(
@GeneratedValue(strategy = GenerationType.IDENTITY) @Id var id: Long = 0,
@Column var userId: Long,
@Column var couponId: Long,
@Column var isUsed: Boolean,
@Column var createdAt: LocalDateTime,
@Column var endAt: LocalDateTime
)
결제 정보 관련 데이터를 보관하는 ChargePointPayment의 PK는 orderId(주문번호)로 String 타입입니다.
데이터 타입이 정수형이 아니기 때문에 DB에서 제공하는 Auto Increment를 사용할 수 없습니다. 그렇기에 UserCoupon의 PK와는
달리 @GeneratedValue(GenerationType.IDENTITY)를 사용하지 않고 직접 PK를 지정해주고 있습니다.
Spring Data JPA save()
Spring Data JPA를 사용하기에 Spring Data Jpa의 구현대로 Entity들이 관리되고 DB에 반영됩니다.
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (this.entityInformation.isNew(entity)) {
this.entityManager.persist(entity);
return entity;
} else {
return (S)this.entityManager.merge(entity);
}
}
JpaRepository를 통해 DB에 데이터를 저장할 때 사용되는 save 메서드는 위와 같습니다.
isNew()가 True라면 아얘 새로운 Entity라고 판단해서 persist를 통해 영속성 컨텍스트에 저장하게 됩니다.
반면에 isNew()가 False라면 이미 존재하는 Entity일수도 있기에 merge가 수행됩니다.
merge는 제일 먼저 영속성 컨텍스트에 동일한 Id를 가진 Entity가 있는지 조회하고 없다면 DB에서 조회하게 됩니다.
따라서 때로는 불필요한 select가 사용될 수 있습니다.
Spring Data JPA isNew()
public boolean isNew(T entity) {
ID id = (ID)this.getId(entity);
Class<ID> idType = this.getIdType();
if (!idType.isPrimitive()) {
return id == null;
} else if (id instanceof Number) {
return ((Number)id).longValue() == 0L;
} else {
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
}
}
기본적으로 spring data jpa의 isNew 메서드는 위와 같습니다.
만약 @Id 필드가 참조 타입인데 null이 아니라면 isNew는 False가 됩니다.
한마디로 @Id 필드가 Long, String, Object와 같은 참조 타입이지만 null이 아니라면 False로 판단되어 merge가 수행됩니다.
만약 원시 타입인데 정수, 실수형이면서 값이 0이면 isNew가 True가 됩니다.
즉 숫자인데 0이 아니라면 False로 판단되어 merge가 수행됩니다.
class QuestionPaymentEntity(
@Id var orderId: String,
@Column var userId: Long,
@Column var userCouponId: Long?,
@Column var amount: Int,
@Enumerated(EnumType.STRING) @Column var status: QuestionPaymentStatus,
@Column var createdAt: LocalDateTime
)
ChargePointPayment의 경우 @Id는 String으로 참조 타입입니다. orderId(주문번호)의 경우는 UUID로 미리 부여되는 값으로 항상
특정 값으로 설정되어 저장됩니다. 아직 데이터베이스에 저장되지 않은 데이터이지만 orderId는 null이 아니기 때문에 isNew의 로직으로 항상 merge로 처리됩니다.
merge로 처리되는 경우 발생할 수 있는 문제는 위에서 언급되었듯이 1차적으로 영속성 컨텍스트를 확인하고 존재하지 않는다면
DB로부터 조회하는 select가 실행됩니다. 따라서 불필요한 select가 계속 사용되게 됩니다.
Persistable 구현으로 isNew 오버라이딩 하기
위에서 본 isNew() 로직은 기본 로직입니다. Entity의 isNew를 직접 정의할 수도 있습니다.
public interface Persistable<ID> {
@Nullable
ID getId();
boolean isNew();
}
Entity가 Persistable을 구현하기만 하면 됩니다.
@MappedSuperclass
abstract class BaseCustomIdEntity<T> : Persistable<T> {
@Transient
private var isNewEntity: Boolean = true
abstract override fun getId(): T
override fun isNew(): Boolean {
return isNewEntity
}
@PostPersist
@PostLoad
fun markNotNew() {
this.isNewEntity = false
}
}
프로젝트에는 직접 @Id를 할당해주는 Entity가 여러개이기 때문에 BaseCustomIdEntity라는 추상 클래스를 정의하고
Entity 클래스들이 상속하여 재사용할 수 있도록 하였습니다.
먼저 BaseCustomIdEntity는 isNewEntity라는 필드를 가지고 있습니다. 기본값으로는 true입니다.
따라서 BaseCustomIdEntity 객체는 생성되면 기본적으로 항상 true입니다.
isNew 메서드는 단순히 isNewEntity 필드의 값을 반환합니다.
class QuestionPaymentEntity(
@Id var orderId: String,
@Column var userId: Long,
@Column var userCouponId: Long?,
@Column var amount: Int,
@Enumerated(EnumType.STRING) @Column var status: QuestionPaymentStatus,
@Column var createdAt: LocalDateTime
) : BaseCustomIdEntity<String>()
만약 QuestionPaymentEntity가 BaseCustomIdEntity를 상속하는 경우
새로운 QuestionPaymentEntity가 생성되면 isNewEntity는 True인 상태가 됩니다.
디버깅을 하면서 다시 save 메서드를 호출해보았습니다. 동일하게 isNew 메서드를 호출하게 됩니다.
이전과는 달리 BaseCustomIdEntity의 오버라이딩 된 isNew 메서드가 호출됩니다.
또한 새로 생성된 Entity이기 때문에 isNewEntity는 True입니다. 따라서 isNew는 True로 반환됩니다.
따라서 merge가 아닌 persist가 호출되어 별도의 Entity 조회 로직이 사용되지 않게 됩니다.
@MappedSuperclass
abstract class BaseCustomIdEntity<T> : Persistable<T> {
@Transient
private var isNewEntity: Boolean = true
// ...
@PostPersist
@PostLoad
fun markNotNew() {
this.isNewEntity = false
}
}
근데 한가지 주의해야 할 점이 있습니다. isNewEntity는 기본 값이 True입니다.
근데 더 이상 isNewEntity가 True가 아닌 경우도 있습니다.
예를 들어 DB에 저장되고 나서는 isNewEntity는 False로 변경되어야 합니다.
따라서 @PostPersist를 통해 persist 메서드가 실행되고 난 후 isNewEntity를 False로 변경될 수 있도록 합니다.
또한 DB에서 데이터를 조회하고 Entity로 변환하는 과정에서도 isNewEntity는 기본값 True로 할당됩니다.
그런데 DB에서 저장된 데이터는 새로운 데이터가 아니기에 isNewEntity는 False로 변경되어야합니다.
따라서 @PostLoad를 통해 Entity가 조회(생성)된 직후 바로 isNewEntity를 False로 변경될 수 있도록 합니다.
'spring' 카테고리의 다른 글
[Spring] doDispatch 예외 처리 (1) | 2025.06.02 |
---|---|
[Spring] Spring Micrometer + Grafana Tempo 찍먹 하기 (1) | 2025.05.30 |
Spring Logging 구축하기 - (메서드 로그) (0) | 2025.05.17 |
[Spring] AWS SNS 토픽 발행 - 블로킹 VS 논블로킹 (0) | 2025.05.01 |
이벤트 재발행 처리하기 - Transactional Outbox Pattern (0) | 2025.04.08 |