JPA의 Optimistic Lock

INSANEZINDOL·2024년 2월 14일

seminar

목록 보기
5/9

JPA의 낙관적 잠금(Optimistic Lock)

개요

  • 요청이 많은 서버에서 여러 트랜잭션이 동시에 같은 데이터에 업데이트를 발생시킬 경우에 일부 요청이 유실되는 경우가 발생하여 장애로 이어질 수 있습니다.
  • 그 만큼 엔터프라이즈 애플리케이션의 경우 데이터베이스에 대한 동시 액세스(concurrency)를 적절하게 관리하는 것이 중요합니다.
  • 여러곳에서 동시에 발생하는 트랜잭션을 처리 할 수 있어야 하며 동시 읽기/업데이트 간에 데이터가 일관되게 유지되도록 해야합니다.

비관적 잠금 이란

  • 자원 요청에 따른 동시성문제가 발생할것이라고 예상하고 락을 걸어버리는 방법론
  • 트랜잭션의 충돌이 발생한다고 가정합니다.
  • 하나의 트랜잭션이 자원에 접근시 락을 걸고, 다른 트랜잭션이 접근하지 못하게 합니다.
  • 데이터베이스에서 Shared Lock(공유, 읽기 잠금) 이나 Exclusive Lock(배타, 쓰기 잠금) 을 겁니다.

장점

  • 충돌이 자주 발생하는 환경에 대해서는 롤백의 횟수를 줄일 수 있으므로 성능에서 유리합니다.
  • 데이터 무결성을 보장하는 수준이 매우 높습니다.

단점

  • 데이터 자체에 락을 걸어버리므로 동시성이 떨어져 성능 손해를 많이 보게 됩니다. 특히 읽기가 많이 이루어지는 데이터베이스의 경우에는 손해가 더 두드러집니다.
  • 서로 자원이 필요한 경우에, 락이 걸려있으므로 데드락이 일어날 가능성이 있습니다.

낙관적 잠금 이란

  • 낙관적 잠금 이란 데이터 갱신시 충돌이 발생하지 않을 것이라고 낙관적으로 보고 잠금을 거는 기법입니다.
디비에 락을 걸기보다는 충돌 방지(Conflict detection)에 가깝다고 볼 수 있음
자원에 락을 걸어서 선점하지말고, 동시성 문제가 발생하면 그때 가서 처리 하자는 방법론입니다.
  • 동시성 처리를 위해 JPA 에서는 낙관적 잠금(Optimistic Lock)을 손쉽게 사용할 수 있도록 제공합니다.
  • 낙관적 잠금이란 동시에 동일한 데이터에 대한 여러 업데이트가 서로 간섭하지 않도록 방지하는 version이라는 속성을 확인하여 Entity의 변경사항을 감지하는 메커니즘 입니다.
  • JPA에서 낙관적 잠금을 사용하기 위해서는 Entity 내부에 @Version Annotation이 붙은 Int, Long Type의 변수를 구현하여줌으로써 간단하게 구현이 가능합니다.

장점

  • 충돌이 안난다는 가정하에, 동시 요청에 대해서 처리 성능이 좋습니다.

단점

  • 잦은 충돌이 일어나는경우 롤백처리에 대한 비용이 많이 들어 오히려 성능에서 손해를 볼 수 있습니다.
  • 롤백 처리를 구현하는게 복잡할 수 있습니다.

샘플 코드

  • Table
CREATE TABLE `student`
(
    `id`        int(11) NOT NULL AUTO_INCREMENT,
    `name`      varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
    `version`   int(11) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

  • Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Version
    private Integer version;

    @Builder
    public Student(String name) {
        this.name = name;
    }

    public void changeName(String name) {
        this.name = name;
    }

}
  • Repository
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    @Query("SELECT s FROM Student s WHERE s.id = :id")
    Student findStudentById(Long id);

    Student findByName(String name);

}
  • Service
@Transactional(readOnly = true)
public Student findStudent(long id) {
    log.info("findStudent : {}", id);
    return studentRepository.findStudentById(id);
}

@Transactional
public Student addStudent(String name) {
    log.info("addStudent : {}", name);
    Student student = Student.builder()
            .name(name)
            .build();
    return studentRepository.save(student);
}

@Transactional
public Student modifyStudent(long id, String newName) {
    log.info("modifyStudent : {}, {}", id, newName);
    Student student = studentRepository.findStudentById(id);
    student.changeName(newName);

    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    return student;
}

@Transactional
public void removeStudent(long id) {
    log.info("removeStudent : {}", id);
    studentRepository.deleteById(id);
}
  • Controller
@GetMapping("/student/{id}")
public ResponseDto findStudent(@PathVariable("id") long id) {
    log.info("[BEG] findStudent");
    ResponseDto response = new ResponseDto();
    response.setData(mysqlService.findStudent(id));
    log.info("[END] findStudent");
    return response;
}

@PostMapping("/student")
public ResponseDto addStudent(@RequestBody StudentDto studentDto) {
    log.info("[BEG] addStudent");
    ResponseDto response = new ResponseDto();
    response.setData(mysqlService.addStudent(studentDto.getName()));
    log.info("[END] addStudent");
    return response;
}

@PutMapping("/student/{id}")
public ResponseDto modifyStudent(@PathVariable("id") long id, @RequestBody StudentDto studentDto) {
    log.info("[BEG] modifyStudent");
    ResponseDto response = new ResponseDto();
    response.setData(mysqlService.modifyStudent(id, studentDto.getName()));
    log.info("[END] modifyStudent");
    return response;
}

@DeleteMapping("/student/{id}")
public ResponseDto removeStudent(@PathVariable("id") long id) {
    log.info("[BEG] removeStudent");
    ResponseDto response = new ResponseDto();
    mysqlService.removeStudent(id);
    log.info("[END] removeStudent");
    return response;
}

동작 예시

기본 동작 프로세스

  • 최초 entity 등록 후 version 값은 0으로 등록

  • update 수행 시 version 값은 +1 씩 증가

  • Hibernate Update SQL문

update student set name=?, version=? where id=? and version=?

ObjectOptimisticLockingFailureException 발생 가정 프로세스

  • 2개의 Thread 가 동시에 1개의 Row를 수정하는 가정
    • 1번 Thread는 dean-test-1로 수정 요청
select student0_.id as id1_0_, student0_.name as name2_0_, student0_.version as version3_0_ from student student0_ where student0_.name=?
update student set name=?, version=? where id=? and version=?
  • 2번 Thread는 dean-test-2로 수정 요청 (ObjectOptimisticLockingFailureException 발생)
select student0_.id as id1_0_, student0_.name as name2_0_, student0_.version as version3_0_ from student student0_ where student0_.name=?
update student set name=?, version=? where id=? and version=?
  • ObjectOptimisticLockingFailureException 발생 로그
2023-02-17 17:05:07.147 ERROR 53f36865-8ba9-4fc5-bcfa-eb305d23792c [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.orm.ObjectOptimisticLockingFailureException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update student set name=?, version=? where id=? and version=?; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update student set name=?, version=? where id=? and version=?] with root cause

org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update student set name=?, version=? where id=? and version=?
	at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:67)
	at org.hibernate.jdbc.Expectations$BasicExpectation.verifyOutcome(Expectations.java:54)
	at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:47)
	at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3571)
	at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3438)
	at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3870)
	at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:202)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604)
	at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478)
	at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:684)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475)
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:344)
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107)
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1407)
	at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1394)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.orm.jpa.ExtendedEntityManagerCreator$ExtendedEntityManagerInvocationHandler.invoke(ExtendedEntityManagerCreator.java:362)
	at com.sun.proxy.$Proxy164.flush(Unknown Source)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:311)
	at com.sun.proxy.$Proxy164.flush(Unknown Source)
	at org.springframework.data.jpa.repository.support.SimpleJpaRepository.flush(SimpleJpaRepository.java:727)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:289)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:137)
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:121)
	at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:530)
	at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:286)
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:640)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:164)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:139)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:81)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at com.sun.proxy.$Proxy168.flush(Unknown Source)
	at com.example.micrometerboot.service.MysqlService.modifyStudent(MysqlService.java:102)
	at com.example.micrometerboot.service.MysqlService$$FastClassBySpringCGLIB$$fb1810d2.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
	at com.example.micrometerboot.service.MysqlService$$EnhancerBySpringCGLIB$$db049aea.modifyStudent(<generated>)
	at com.example.micrometerboot.controller.MysqlController.modifyStudent(MysqlController.java:103)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:920)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:699)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:779)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at com.example.micrometerboot.config.MDCLoggingFilter.doFilter(MDCLoggingFilter.java:29)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:891)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1784)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:829)
  • ObjectOptimisticLockingFailureException 가 발생하는 경우

ObjectOptimisticLockingFailureException 발생의 대안

  • Retryable 구현
@Transactional
@Retryable(interceptor = "optimisticLockingAutoRetryInterceptor")
public Student modifyStudent(long id, String newName) {
    log.info("modifyStudent : {}, {}", id, newName);
    Student student = studentRepository.findStudentById(id);
    student.changeName(newName);

    try {
        Thread.sleep(10000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    return student;
}
  • RetryConfiguration 구성
@Slf4j
@EnableRetry
@Configuration
public class RetryConfiguration {

    private static final int OPTIMISTIC_LOCKING_MAX_RETRIES = 3;

    private static final RetryListener RETRY_LISTENER = new RetryListenerSupport() {
        @Override
        public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
            if (throwable instanceof ObjectOptimisticLockingFailureException) {
                log.warn("Retryable error occurred on " + context.getAttribute(RetryContext.NAME) + " - retry no. " + context.getRetryCount() + ": " + throwable.getMessage());
            }
        }
    };

    @Bean
    public RetryOperationsInterceptor optimisticLockingAutoRetryInterceptor() {
        RetryTemplate template = new RetryTemplate();
        template.registerListener(RETRY_LISTENER);
        template.setRetryPolicy(new SimpleRetryPolicy(OPTIMISTIC_LOCKING_MAX_RETRIES, Map.of(ObjectOptimisticLockingFailureException.class, Boolean.TRUE)));
        return RetryInterceptorBuilder.stateless().retryOperations(template).build();
    }

}
  • ObjectOptimisticLockingFailureException 발생하여, update 실패 후 Retryable 1회 후 성공 로그
2023-02-20 11:52:19.136  INFO 74470f49-428a-4ed0-8582-e49d1610a68c [nio-8080-exec-6] c.e.micrometerboot.service.MysqlService  : saveAndFlush : com.example.micrometerboot.mysql.entity.Student@44d93b48
Hibernate: update student set name=?, version=? where id=? and version=?
2023-02-20 11:52:19.163  WARN 74470f49-428a-4ed0-8582-e49d1610a68c [nio-8080-exec-6] c.e.m.config.RetryConfiguration          : Retryable error occurred on public com.example.micrometerboot.mysql.entity.Student com.example.micrometerboot.service.MysqlService.modifyStudent(long,java.lang.String) - retry no. 1: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update student set name=?, version=? where id=? and version=?; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update student set name=?, version=? where id=? and version=?
2023-02-20 11:52:19.166  INFO 74470f49-428a-4ed0-8582-e49d1610a68c [nio-8080-exec-6] c.e.micrometerboot.service.MysqlService  : modifyStudent : 3, dean-test-11
Hibernate: select student0_.id as id1_0_, student0_.name as name2_0_, student0_.version as version3_0_ from student student0_ where student0_.id=?

@Retryable 어노테이션

  • 특정 Exception이 발생했을 경우 일정 횟수만큼 재시도 할 수 있는 어노테이션

  • Spring Legacy 환경일 경우에는 spring-aspects dependency를 추가적으로 필요로 하지만 Spring Boot프로젝트의 경우에는 spring-boot-starter-aop 의존성이 걸려있다면 별도로 spring-aspects 의존성을 걸어주지 않아도 무관

  • 사용방법

    • Spring Application에 @EnableRetry 어노테이션 추가
    • 재시도 하고 싶은 메소드에 @Retryable 어노테이션 추가
      • include : 여기에 설정된 특정 Exception이 발생했을 경우 retry
      • exclude : 설정된 Exception 재시도 제외
      • maxAttempts : 최대 재시도 횟수 (기본 3회)
      • backoff : 재시도 pause 시간
  • Sample Code

@Retryable(include = [ResourceAccessException::class], backoff = Backoff(1000))  
class ApiCallService {
	...
}
profile
Backend Engineer

0개의 댓글