디비에 락을 걸기보다는 충돌 방지(Conflict detection)에 가깝다고 볼 수 있음
자원에 락을 걸어서 선점하지말고, 동시성 문제가 발생하면 그때 가서 처리 하자는 방법론입니다.
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;

@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
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);
}
@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);
}
@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=?
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=?
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=?
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)

@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;
}
@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();
}
}
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=?
특정 Exception이 발생했을 경우 일정 횟수만큼 재시도 할 수 있는 어노테이션
Spring Legacy 환경일 경우에는 spring-aspects dependency를 추가적으로 필요로 하지만 Spring Boot프로젝트의 경우에는 spring-boot-starter-aop 의존성이 걸려있다면 별도로 spring-aspects 의존성을 걸어주지 않아도 무관
사용방법
Sample Code
@Retryable(include = [ResourceAccessException::class], backoff = Backoff(1000))
class ApiCallService {
...
}