따라서 데이터베이스 락을 이용하는 비관적 락이 아닌 애플리케이션 레벨에서 버전 관리하는 낙관적 락을 사용했습니다.
게시물을 수정하는 데 단순히 첨부파일만 추가하거나 삭제하면 게시물의 버전은 증가하지 않는다. 하지만 해당 게시물은 물리적으로는 변경되지 않았지만, 논리적으로는 변경되었다.
OPTIMISTIC_FORCE_INCREMENT 옵션을 사용한다면 엔티티를 수정하지 않아도 트랜잭션을 커밋 할 때 UPDATE 쿼리를 사용해서 버전 정보를 강제로 증가시킨다. 엔티티 수정 시 버전 증가가 발생하고 커밋 때 버전 증가가 발생하므로 총 2번의 버전 증가가 나타날 수 있다.
따라서 OPTIMISTIC_FORCE_INCREMENT 사용하여 게시물의 버전 증가를 강제하여 논리적인 단위의 엔티티 묶음을 버전 관리할 수 있다.
업데이트 시 버전 충돌이 발생하면 ObjectOptimisticLockingFailureException 예외가 발생한다.
예외 처리를 통해 수동으로 업데이트를 반복 수행하는 코드를 직접 추가했습니다.
재시도 로직을 Spring AOP를 사용해서 비즈니스 코드에서 분리했습니다.
@Order(1)
@Aspect
@Component
public class UpdateRetryAspect {
@Around("@annotation(updateRetry)")
public Object doUpdateRetry(ProceedingJoinPoint joinPoint,
UpdateRetry updateRetry) throws Throwable {
int retryCount = updateRetry.value();
ObjectOptimisticLockingFailureException exception = null;
for (int cnt = 0; cnt < retryCount; cnt++) {
try {
return joinPoint.proceed();
} catch (ObjectOptimisticLockingFailureException ex) {
exception = ex;
// 5번 재시도 이후 50millis 딜레이
if (cnt > 4) Thread.sleep(50);
}
}
throw new UpdateFailureException("잠시후 다시 시도해 주세요.", exception);
}
}
※ 참고
이전 실행이 롤백 되고 난 뒤 재시도가 이뤄져야 한다.
따라서 재시도 어드바이스가 트랜잭션 어드바이스 보다 우선순위가 높아야 한다.
트랜잭션 어드바이스는 @Order 어노테이션이 없고 Odered 인터페이스를 구현하지 않은 경우
AbstractPointcutAdvisor 클래스의 메서드 getOrder 반환값
Ordered.LOWEST_PRECEDENCE(Integer.MAX_VALUE)으로 정해진다.
reference
- https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/chapters/locking/Locking.html
- https://velog.io/@backtony/JPA-Lock
- https://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html
- https://stackoverflow.com/questions/32057910/custom-spring-aop-around-transactional/33509777#33509777