이번 글에서는 영속성 프레임워크(JPA, R2DBC)에서 save를 두 번 호출할 때 발생할 수 있는 '생성 시간' 처리 이슈와 그 해결 방법을 소개하고자 합니다.
우선은 일반적으로 생각할 수 있는 생성 시간 처리 방법 부터 소개하겠습니다.
생성 시간 처리의 일반적인 방법들
코드에서 직접 생성 시간 설정
가장 간단한 방법은 코드를 통해 직접 생성 시간을 설정하는 것입니다.
entity.setCreateTime(LocalDateTime.now());
엔티티의 createTime 필드에 LocalDateTime.now()와 같은 메서드를 호출하여 값을 할당하는 방식입니다.
하지만 코드 중복이 많아진다는 단점이 있습니다.
JPA의 @PrePersist 또는 @PreUpdate 사용
@Entity
public class MyEntity {
@PrePersist
protected void onCreate() {
createTime = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updateTime = LocalDateTime.now();
}
}
JPA의 엔티티 라이프사이클 콜백 메서드를 사용하여 @PrePersist나 @PreUpdate를 적용하면, 엔티티가 저장되기 전에 자동으로 생성 시간과 수정 시간을 설정할 수 있습니다.
이 방식은 JPA에서 자주 사용하는 방법이며, R2DBC에서도 유사한 방식으로 적용할 수 있습니다.
데이터베이스에서 자동으로 생성 시간 처리
엔티티를 데이터베이스에 삽입할 때, 테이블 스키마에서 createTime을 자동으로 생성되도록 설정하는 방식입니다.
예를 들어, MySQL의 경우 테이블을 생성할 때 아래와 같이 createTime을 자동으로 설정할 수 있습니다.
CREATE TABLE my_table (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
이 방식은 매우 깔끔하고 별도의 코드 수정 없이 생성 시간과 수정 시간을 자동으로 관리할 수 있어 자주 사용됩니다.
문제 발생 상황(save 2번)
3번 방식(데이터베이스에서 시간 자동 생성)도 깔끔하지만 영속성 프레임워크를 사용할 때는 문제가 발생할 여지가 있습니다.
예를 들어, 하나의 비즈니스 로직에서 동일한 엔티티를 두 번 save하는 상황을 가정해봅시다.
흔한 경우는 아니지만 save를 하고나서 추가적인 정보를 담고 다시 save를 할 수도 있는 것이죠.
이제 흐름을 살펴보겠습니다.
- 1번: 첫 번째 save 호출 – 영속성 컨텍스트 1차 캐시에 저장되지만 아직 DB에는 반영되지 않음.
- 2번: 두 번째 save 호출 – 1차 캐시 값에 새로운 값을 넣어서 저장하려고 하지만, createTime 값이 없음.
- 3번: 트랜잭션 커밋 – 두 번의 save가 끝나고 커밋 시 두개의 쿼리가 실행되면서 첫번째 쿼리에서는 createTime이 생성되지만 즉시 두번째 쿼리에서 createTime이 null로 반영됨.
이 문제를 해결하기 위해 첫 번째 또는 두 번째 방법으로 되돌아갈 수 있습니다. 첫 번째 방법(코드에서 직접 시간 설정)은 이상적이지 않지만, 두 번째 방법(PrePersist나 PreUpdate 사용)은 좋은 대안입니다.
그렇다면, 세 번째 방법(데이터베이스 자동 처리)을 유지하면서 좋은 대안이 있을까요?
JPA를 사용하는 경우 flush()를 사용해 데이터를 먼저 삽입한 후 비즈니스 로직을 진행할 수 있습니다. 그러나 이 방법은 덜 우아한 코드를 초래할 수 있습니다. 비즈니스 로직 내에서 영속성 관리나 트랜잭션 관리를 혼합하면 코드가 복잡해지고 유지 관리가 어려워질 수 있습니다.
이 때 또 다른 해결 방법은 세 번째 방법을 이용하면서 트리거를 이용하면 쉽게 해결이 가능합니다.
트리거를 사용한 해결 방법
새로운 테이블과 트리거를 활용한 방법
저와 같은 경우에는 상태값에 따라 시간을 기록하고 싶었기 때문에 아래와 같이 구현해봤습니다.
별도의 시간을 담는 로그 테이블을 생성한 후, 데이터베이스 트리거를 사용하여 상태 변경 시 시간을 기록하는 방법입니다.
CREATE TRIGGER status_change_trigger
AFTER UPDATE ON your_table
FOR EACH ROW
BEGIN
IF NEW.status <> OLD.status THEN
INSERT INTO status_log_table (record_id, status, update_time)
VALUES (NEW.id, NEW.status, NOW());
END IF;
END;
이 트리거는 기존 테이블(your_table)에 상태 변경이 발생할 때마다 로그 테이블(status_log_table)에 상태와 시간을 기록합니다.
이렇게 하면 save가 여러 번 발생하더라도 트리거에 의해 시간이 정확하게 관리되며, 확장성과 일관성을 유지할 수 있습니다.
기존 테이블과 트리거를 활용한 방법
새로운 테이블을 만들지 않아도 사용이 가능합니다.
생성 시간 컬럼을 NOT NULL로 설정하고 아래와 같이 트리거를 생성해보겠습니다.
CREATE TRIGGER prevent_null_update
BEFORE UPDATE ON your_table
FOR EACH ROW
BEGIN
IF NEW.create_time IS NULL THEN
SET NEW.create_time = OLD.create_time;
END IF;
END;
이를 통해 save가 두 번 호출되더라도 createTime 필드가 유지되도록 보장할 수 있습니다.
결론
영속성 프레임워크에서 save를 두 번 호출하는 상황에서 발생하는 생성 시간 처리 문제는 여러 가지 방법으로 해결할 수 있습니다.
각각의 방식은 상황에 맞게 선택할 수 있으며, 특히 트리거를 사용한 방식은 확장성과 일관성을 유지하면서 문제를 해결할 수 있는 유용한 방법입니다.
'🍃 Spring' 카테고리의 다른 글
[Spring] 동시성(Concurrency) 이슈 - 변수(1) (2) | 2024.10.25 |
---|---|
[Spring] Web Push API(Push Notification) 구현 방법 (1) | 2024.10.21 |
[Spring] R2DBC DatabaseClient 잘 사용하기 (1) | 2024.09.19 |
[Spring] Webflux에 Mongo 연결(+Kubernetes) (0) | 2024.09.05 |
[Spring] 스프링에서 가상 스레드를 이용한 부하테스트 (2) | 2024.03.15 |