[Java] JPA 낙관적락을 이용한 동시성 제어

ChangSol·2024년 3월 21일

개발을 할 때 동시에 들어오는 요청에 대하여 제어를 해야할 경우가 있다.

우선 동시성 제어 방법 중에서 낙관적 락과 비관적 락이 있다.

낙관적 락(Optimistic Lock)

충돌이 발생하지 않을것이라 낙관적으로 가정하는 방법이다.
Application에서 동시성을 제어하는 레벨이며, version을 관리하는 컬럼을 이용한다.
Transaction commit 시점에 충돌을 알 수 있으며, Lock 점유시간을 최소화 할 수 있다.

비관적 락(Pessimistic Lock)

충돌이 무조건 발생한다고 비관적으로 가정하는 방법이다.
DB에서 제어하는 레벨이며, DB가 제공하는 락을 이용한다.
Data 수정 즉시 Transcation 충돌을 알 수 있고, *교착 상태가 자주 발생할 수 있다.

교착상태(DeadLock)

Thread가 서로가 가지고 있는 자원을 기다리면서 진행이 멈춘 상태.
ex) A Thread -> B Thread , B Thread -> A Thread


이번 글에서는 JPA에서 낙관적 락으로 동시성 제어를 할 것이다.

우선 아래와 같이 채번테이블을 구성한다.

[채번테이블 구성]

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
@Entity
@org.hibernate.annotations.Table(appliesTo = "SEQ", comment = "채번테이블")
public class Seq extends BaseDomainWithId {

	public enum Type {
		TEST, // 테스트
	}

	/**
	 * 채번구분
	 */
	@Comment("채번구분")
	@Column(length = 10, unique = true)
	@NotNull
	@Enumerated(EnumType.STRING)
	private Type seqType;

	/**
	 * 채번숫자
	 */
	@Comment("채번숫자")
	@ColumnDefault("0")
	@NotNull
	private Long num;

	/**
	 * 데이터버전
	 */
	@Comment("데이터버전")
	@ColumnDefault("0")
	@NotNull
	@Version
	private Long version;

	public void numUp() {
		this.num++;
	}
}

낙관적 락을 위하여 @Version Annotation을 이용했다.

@Version 지켜야하는 규칙

  1. Entity에 @Version 은 오직 1개만 가능
  2. 타입은 Int, Long, Short, java.sql.Timestamp 만 가능

[채번 서비스 구성]

@Service
@RequiredArgsConstructor
public class SeqService {

	private final SeqRepository seqRepository;

	public String getSeq(Seq.Type type) {
		Long num = this.generationNum(type);
		return String.format("%s-%s", type.name(), StringUtils.leftPad(num.toString(), 6, "0"));
	}

	@Transactional
	Long generationNum(Seq.Type type) {
		Seq seq = seqRepository.findBySeqType(type).orElse(Seq.builder()
															  .seqType(type)
															  .num(0L)
															  .build());
		seq.numUp();
		return seqRepository.save(seq)
							.getNum();
	}
}

위와 같이 서비스 구성 후 동시성 테스트를 진행

[채번 동시성 테스트코드]

@Test
	@DisplayName("채번 동시성제어 테스트")
	void saveConcurrency() throws InterruptedException {
		// given
		final Seq.Type SEQ_TYPE = Seq.Type.TEST;

		// when
		final int COUNT = 100;
		CountDownLatch latch = new CountDownLatch(COUNT);
		for (int i = 0; i < COUNT; i++) {
			new Thread(() -> {
				seqService.getSeq(SEQ_TYPE);
				latch.countDown();
			}).start();
		}
		latch.await();

		// then
		Assertions.assertThat(seqRepository.findBySeqType(SEQ_TYPE)
										   .stream()
										   .findAny()
										   .orElseThrow(() -> new NotFoundException("데이터 없음"))
										   .getNum()).isEqualTo(COUNT);

[채번 동시성 오류 결과]

org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [org.test.app.seq.domain.Seq] with identifier [19]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [org.test.app.seq.domain.Seq#19]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:315)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:233)
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551)
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:243)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)

위 ObjectOptimisticLockingFailureException 오류는 버전이 충돌하여 나타나는 오류로 100개의 스레드로 채번을 진행하였으나 실제로 데이터를 보면 23번까지만 채번되고 오류가 발생하였다.

따라서 해당 동시성 충돌을 해결하려면 try-catch로 잡아 해결을해도되지만 나는 Spring에서 지원하는 Retry 라이브러리를 이용하여 해결을 했다.

해당 내용은 이어서 아래 글에서 진행.
다음 - [Java] JPA 낙관적 락(Optimistic) 동시성 제어 - 2

profile
Back-End Developer

0개의 댓글