비관적 락을 ν†΅ν•œ κ³„μ’Œμ΄μ²΄ λ™μ‹œμ„± 핸듀링 (feat. @Transactional ν•™μŠ΅ ν…ŒμŠ€νŠΈ)

2024. 2. 4. 03:41ㆍBackend/Spring

JPA의 @Transactional μ–΄λ…Έν…Œμ΄μ…˜μ— λŒ€ν•œ 이해λ₯Ό 올릴 κ²Έ, μ•„λž˜ 레포λ₯Ό μƒμ„±ν•˜κ³  ν•™μŠ΅ ν…ŒμŠ€νŠΈλ₯Ό μ§„ν–‰ν–ˆλ‹€.

 

GitHub - One-armed-boy/transactional_test: ν•™μŠ΅ ν…ŒμŠ€νŠΈ of @Transactional

ν•™μŠ΅ ν…ŒμŠ€νŠΈ of @Transactional . Contribute to One-armed-boy/transactional_test development by creating an account on GitHub.

github.com

ν…ŒμŠ€νŠΈ μ˜ˆμ œλŠ” 일반적으둜 λ™μ‹œμ„± μ˜ˆμ œμ— 많이 λ“±μž₯ν•˜λŠ” κ³„μ’Œ 이체 μ»¨ν…μŠ€νŠΈλ₯Ό κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„ν•˜κ³ μž ν–ˆλ‹€.

μ•„λž˜λŠ” 이λ₯Ό μœ„ν•œ μ‹œν€€μŠ€λ₯Ό 말둜써 μ •λ¦¬ν•œ λ‚΄μš©μ΄λ‹€.

  1. μ‚¬μš©μž Aλ₯Ό 쑰회
  2. μ‚¬μš©μž Bλ₯Ό 쑰회
  3. μ‚¬μš©μž Aμ—κ²Œ μ†‘κΈˆ μš”μ²­μ΄ λ“€μ–΄μ˜¨ X 만큼의 μž”μ•‘μ΄ μ‘΄μž¬ν•˜λŠ”μ§€ 확인
    • μ‘΄μž¬ν•œλ‹€λ©΄, μ‚¬μš©μž B의 μž”μ•‘μ— Xλ₯Ό 더함
    • μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄, 아무 일도 λ°œμƒμ‹œν‚€μ§€ μ•ŠμŒ

이λ₯Ό κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄, UserService μΈν„°νŽ˜μ΄μŠ€λ₯Ό μƒμ„±ν•˜κ³  transfer λ©”μ„œλ“œλ₯Ό μ•„λž˜μ²˜λŸΌ κ΅¬ν˜„ν•΄μ£Όμ—ˆλ‹€.

public void transfer(String fromUserId, String toUserId, long money) {
  User fromUser = userRepository.findById(fromUserId).orElseThrow(EntityNotFoundException::new);
  User toUser = userRepository.findById(toUserId).orElseThrow(EntityNotFoundException::new);
  long fromBalance = fromUser.getBalance();

  if (fromBalance < money) {
    return;
  }
  fromUser.setBalance(fromUser.getBalance() - money);
  toUser.setBalance(toUser.getBalance() + money);
}

κ·Έ ν›„ @Transactional 유무λ₯Ό λΉ„κ΅ν•˜κΈ° μœ„ν•΄ μ–΄λ…Έν…Œμ΄μ…˜μ„ λΆ™μ—¬μ€€ UserServiceWithTx와 뢙이지 μ•Šμ€ UserServiceWithoutTxλ₯Ό 생성해주고 각각 μ•„λž˜ ν…ŒμŠ€νŒ… λ©”μ„œλ“œμ— 인자둜 μ£Όμž…ν•΄μ£Όμ—ˆλ‹€.

private void execTestLogic(UserService userService) throws InterruptedException {
    // given
    User user1 = new User("Jamal", 100);
    User user2 = new User("Nikola", 30);
    userService.addUser(user1);
    userService.addUser(user2);

    var numOfThread = 10;
    var moneyForTransfer = 20;
    CountDownLatch startLatch = new CountDownLatch(1);
    CountDownLatch endLatch = new CountDownLatch(numOfThread);

    // when
    for (int i = 0; i < numOfThread; i++) {
      new Thread(()->{
        try {
          startLatch.await();

          userService.transfer(user1.getUser(), user2.getUser(), moneyForTransfer);
        } catch (Exception e) {
          System.out.println(e.getMessage());
        } finally {
          endLatch.countDown();
        }
      }).start();
    }
    startLatch.countDown();
    endLatch.await();

    // then
    User user1After = userService.getUserById(user1.getUser());
    User user2After = userService.getUserById(user2.getUser());
    Assertions.assertThat(user1After.getBalance()).isEqualTo(Math.max(0, user1.getBalance() - numOfThread * moneyForTransfer));
    Assertions.assertThat(user2After.getBalance()).isEqualTo(Math.min(user1.getBalance() + user2.getBalance(), user2.getBalance() + numOfThread * moneyForTransfer));
  }

ν…ŒμŠ€νŠΈ κ²°κ³Ό, μ–΄λ…Έν…Œμ΄μ…˜μ΄ μ—†λŠ” λ‘œμ§μ€ λ‹Ήμ—°νžˆ μ˜λ„ν•œλŒ€λ‘œ ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν–ˆμ§€λ§Œ, μ–΄λ…Έν…Œμ΄μ…˜μ΄ μžˆλŠ” λ‘œμ§λ„ λ™μΌν•˜κ²Œ μ‹€νŒ¨ν–ˆλ‹€.

μ–΄λ…Έν…Œμ΄μ…˜μ΄ μžˆλŠ” λ‘œμ§λ„ μ‹€νŒ¨ν•˜λŠ” 것은 μ˜λ„μ™€ λ²—μ–΄λ‚˜κΈ° λ•Œλ¬Έμ— 이에 λŒ€ν•œ 원인 뢄석을 μˆ˜ν–‰ν–ˆλ‹€.

 

1μ°¨ ν…ŒμŠ€νŠΈ μ‹€νŒ¨ 원인 뢄석

λ‹¨μˆœ @Transactional 만 걸어쀄 경우, νŠΈλžœμž­μ…˜ 격리 μˆ˜μ€€μ€ DB의 κΈ°λ³Έ 섀정을 λ”°λΌκ°€λŠ” κ²ƒμœΌλ‘œ μ΄ν•΄ν–ˆλ‹€.

ν…ŒμŠ€νŠΈ 용으둜 Mysql을 μ‚¬μš©ν•˜κ³  μžˆμ—ˆκΈ° λ•Œλ¬Έμ— κΈ°λ³Έ 격리 μˆ˜μ€€μ€ REPEATABLE READκ°€ λœλ‹€.

이 경우 μ—¬λŸ¬ νŠΈλžœμž­μ…˜μ΄ λ™μ‹œ λ‹€λ°œμ μœΌλ‘œ μœ μ €μ˜ μž”μ•‘ 정보에 μ ‘κ·Όν•  경우, νŠΉμ • νŠΈλžœμž­μ…˜μ—μ„œ μž”μ•‘μ„ μˆ˜μ •ν•˜κ³  컀밋을 ν•˜λ”λΌλ„ 이미 μž”μ•‘ 정보에 μ ‘κ·Όν•œ νŠΈλžœμž­μ…˜μ—λŠ” 영ν–₯을 λ―ΈμΉ˜μ§€ λͺ»ν•œλ‹€. (λ‚˜μ€‘μ— μ•ˆ μ‚¬μ‹€μ΄μ§€λ§Œ, λ§Œμ•½ μ˜μ†μ„± μ»¨ν…μŠ€νŠΈλ₯Ό ν™œμš©ν•˜κ³  μžˆλ‹€λ©΄ μ•žμ„  λ¬Έμ œλ³΄λ‹€λŠ” μ˜μ†μ„± μ»¨ν…μŠ€νŠΈμ˜ 1μ°¨ μΊμ‹±μœΌλ‘œ μΈν•œ λ¬Έμ œκ°€ 더 μš°μ„ λ˜κΈ°λŠ” ν•œλ‹€.)

λ”°λΌμ„œ μ΄ˆκΈ°μ—λŠ” νŠΈλžœμž­μ…˜ κ²©λ¦¬μˆ˜μ€€μ΄ λ¬Έμ œμΈκ°€ μ‹Άμ–΄μ„œ, μ•„λž˜μ²˜λŸΌ κ²©λ¦¬μˆ˜μ€€μ„ SERIALIZABLE둜 μˆ˜μ •ν–ˆλ‹€.

// @Transactional
@Transactional(isolation = Isolation.SERIALIZABLE)
public void transfer(...) {
  ...
}

λ‹€λ§Œ 이 κ²½μš°μ—” λ°λ“œλ½μ΄ λ°œμƒν•˜μ—¬ μŠ€λ ˆλ“œλ“€ 쀑 단 ν•˜λ‚˜μ˜ μŠ€λ ˆλ“œμ˜ μˆ˜μ •λ§Œ 반영이 λ˜λŠ” λ¬Έμ œκ°€ μƒˆλ‘­κ²Œ λ°œμƒν–ˆλ‹€.

 

2μ°¨ ν…ŒμŠ€νŠΈ μ‹€νŒ¨ 원인 뢄석 (with Second Lost Updates Problem)

Mysql의 경우 κ°€μž₯ μ΅œκ·Όμ— λ°œμƒν•œ λ°λ“œλ½μ„ 확인할 수 있기 λ•Œλ¬Έμ— 이λ₯Ό μ΄μš©ν•˜μ—¬ μ™œ λ°λ“œλ½μ΄ λ°œμƒν–ˆλŠ”μ§€ 뢄석할 수 μžˆμ—ˆλ‹€.

이λ₯Ό 보면 상황을 μ•„λž˜μ²˜λŸΌ 정리할 수 μžˆλ‹€.

  1. λͺ¨λ“  μŠ€λ ˆλ“œλ“€μ€ μžμ‹ μ΄ μ—…λ°μ΄νŠΈ ν•΄μ•Ό ν•  λ ˆμ½”λ“œλ“€μ„ μ‘°νšŒν•œλ‹€. 이 λ•Œ κ²©λ¦¬μˆ˜μ€€μ— μ˜ν•΄ λ ˆμ½”λ“œμ— S 락을 건닀.
  2. κ·Έ ν›„, 순차적인 μ—…λ°μ΄νŠΈλ₯Ό μœ„ν•΄ λ ˆμ½”λ“œ ν•˜λ‚˜μ— λŒ€ν•΄ X 락을 νšλ“ν•˜κ³ μž ν•œλ‹€.
  3. 이 λ•Œ, λ‹€λ₯Έ μŠ€λ ˆλ“œμ—μ„œλ„ ν•΄λ‹Ή λ ˆμ½”λ“œμ— λŒ€ν•œ S 락을 이미 νšλ“ν•œ 상황이고 λ§ˆμ°¬κ°€μ§€λ‘œ λ‹€λ₯Έ μŠ€λ ˆλ“œμ—μ„œλ„ ν•΄λ‹Ή λ ˆμ½”λ“œμ— λŒ€ν•œ X 락을 μš”μ²­ν•  것이기 λ•Œλ¬Έμ— λ°λ“œλ½ μƒνƒœμ— 빠진닀.

이 λ¬Έμ œλŠ” Mysql λ‚΄μ—μ„œ SERIALIZABLE κ²©λ¦¬μˆ˜μ€€μ„ κ΅¬ν˜„ν•˜λŠ” 방식이 Select 문을 Select for share(S 락 νšλ“)둜 μˆ˜μ •ν•˜μ—¬ μ‹€ν–‰μ‹œν‚€λŠ” 방식이기 λ•Œλ¬Έμ— λ°œμƒν•˜κ²Œ λœλ‹€. 즉, νŠΈλžœμž­μ…˜ κ²©λ¦¬μˆ˜μ€€μ„ ν†΅ν•΄μ„œλŠ” μ™„μ „νžˆ ν•΄κ²°ν•  수 μ—†λŠ” 문제인 것이닀.

(이후에 μ•ˆ 사싀은, μœ„ κ³„μ’Œμ΄μ²΄ μ˜ˆμ‹œμ²˜λŸΌ μ—¬λŸ¬ νŠΈλžœμž­μ…˜μ΄ λ™μ‹œλ‹€λ°œμ μœΌλ‘œ νŠΉμ • λ ˆμ½”λ“œμ— λŒ€ν•œ 쑰회, μˆ˜μ • μž‘μ—…μ„ μˆ˜ν–‰ν•˜λŠ” μƒν™©μ—μ„œ λ°œμƒν•˜λŠ” 문제λ₯Ό Second lost updates problem(λ‘λ²ˆμ˜ κ°±μ‹  λΆ„μ‹€ 문제)이라고 ν•˜κ³  μ΄λŠ” νŠΈλžœμž­μ…˜ κ²©λ¦¬μˆ˜μ€€ λ§ŒμœΌλ‘œλŠ” ν•΄κ²°ν•  수 μ—†λŠ” 문제둜 유λͺ…ν•˜λ‹€λŠ” 것이닀.)

 

ν•΄κ²°μ±… λͺ¨μƒ‰

JPAμ—μ„œλŠ” λ‘λ²ˆμ˜ κ°±μ‹  λΆ„μ‹€ 문제λ₯Ό μœ„ν•΄ μΆ”κ°€μ μœΌλ‘œ 락을 ν™œμš©ν•  수 μžˆλ‹€.

이 λ•Œ ν™œμš©ν•  수 μžˆλŠ” 락의 μ’…λ₯˜λ‘œλŠ” 낙관적 락과 비관적 락이 μžˆλŠ”λ°, 두 락의 νŠΉμ§•κ³Ό μž₯단점은 μ•„λž˜μ™€ κ°™λ‹€.

낙관적 락 (Optimistic Lock)

  • 방식
    • 엔티티에 Version μΉΌλŸΌμ„ μΆ”κ°€ν•˜κ³ , μ˜μ†μ„± μ»¨ν…μŠ€νŠΈ λ‚΄μ˜ 버전 κ°’κ³Ό μ‹€μ œ DB의 버전 값을 λΉ„κ΅ν•˜μ—¬ λ‹€λ₯Έ νŠΈλžœμž­μ…˜μ— μ˜ν•œ λ ˆμ½”λ“œ μˆ˜μ •μ΄ λ°œμƒν–ˆλŠ”μ§€ ν™•μΈν•˜λŠ” λ°©μ‹μœΌλ‘œ λ™μž‘ν•œλ‹€.
    • λ§Œμ•½ 버전 값이 λ‹€λ₯΄λ‹€λ©΄, μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.
  • μž₯점
    • DB λ ˆμ΄μ–΄μ˜ 락을 μ‚¬μš©ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ—, νŠΈλžœμž­μ…˜ 좩돌이 적은 ν™˜κ²½μ—μ„œλŠ” 쒋은 μ„±λŠ₯을 보여쀀닀.
    • λ§ˆμ°¬κ°€μ§€λ‘œ DB λ ˆμ΄μ–΄μ˜ 락을 μ‚¬μš©ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ—, μƒλŒ€μ μœΌλ‘œ λ°λ“œλ½ λ¬Έμ œμ—μ„œ μžμœ λ‘­λ‹€.
  • 단점
    • μ˜ˆμ™Έ λ°œμƒ μ‹œ μ˜ˆμ™Έ 처리 λ‘œμ§μ„ μΆ”κ°€λ‘œ μž‘μ„±ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€. (ex. Fail over 둜직)
    • νŠΈλžœμž­μ…˜ 좩돌이 μž¦μ€ ν™˜κ²½μ—μ„œλŠ” νŠΈλžœμž­μ…˜ λ‹¨μœ„λ‘œ 둀백이 λΉˆλ²ˆν•΄μ§€κ³  이둜 μΈν•œ νŠΈλžœμž­μ…˜ μž¬μ‹€ν–‰μ΄ λ§Žμ•„μ§€κΈ° λ•Œλ¬Έμ— μ„±λŠ₯이 κΈ‰κ²©ν•˜κ²Œ λ‚˜λΉ μ§ˆ κ°€λŠ₯성이 μžˆλ‹€.

비관적 락 (Pessimistic Lock)

  • 방식
    • 엔티티에 λŒ€ν•œ 좔가적인 칼럼 μ†Œμš” 없이, DB λ ˆμ΄μ–΄μ˜ 락을 톡해 문제λ₯Ό ν•΄κ²°ν•œλ‹€.
    • 쑰회 μ‹œ Select for update 쿼리λ₯Ό μˆ˜ν–‰ν•˜μ—¬ X 락을 νšλ“ν•˜μ—¬ λ™μ‹œμ„± 문제λ₯Ό ν•΄κ²°ν•œλ‹€.
  • μž₯점
    • 개발자의 둜직 개발 μ†Œμš”κ°€ 적닀.
    • DB λ ˆμ΄μ–΄μ˜ 락을 ν™œμš©ν•˜κΈ° λ•Œλ¬Έμ— νŠΈλžœμž­μ…˜ 전체가 μž¬μ‹€ν–‰λ˜μ§€ μ•Šκ³  μž μ‹œ λŒ€κΈ°λ§Œ ν•˜λŠ” 방식을 톡해, νŠΈλžœμž­μ…˜ 좩돌이 λΉˆλ²ˆν•œ μƒν™©μ—μ„œ 비ꡐ적 더 쒋은 μ„±λŠ₯을 λ°œνœ˜ν•  수 μžˆλ‹€.
  • 단점
    • μ‘°νšŒμ—λ„ X 락을 κ±ΈκΈ° λ•Œλ¬Έμ— ν•΄λ‹Ή λ ˆμ½”λ“œλ₯Ό μ°Έμ‘°ν•˜λŠ” μ„œλ²„ λ‚΄ λ‹€λ₯Έ λͺ¨λ“  쿼리의 μ„±λŠ₯이 λ‚˜λΉ μ§ˆ κ°€λŠ₯성이 μžˆλ‹€.
    • μ—¬λŸ¬ ν…Œμ΄λΈ” κ°„ μ‘°νšŒκ°€ ν•„μš”ν•  경우, λ°λ“œλ½ λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆλ‹€.

λ‚˜λŠ” λ‹¨μˆœ ν•™μŠ΅ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜λŠ” μ»¨ν…μŠ€νŠΈμ˜€κΈ° λ•Œλ¬Έμ—, 비관적 락을 μ„ νƒν•˜μ—¬ 문제λ₯Ό ν•΄κ²°ν–ˆλ‹€.

이λ₯Ό μœ„ν•΄ Repository λ‚΄ 비관적 락을 μˆ˜ν–‰ν•˜λŠ” μ—…λ°μ΄νŠΈμš© μœ μ € 쑰회 λ©”μ„œλ“œλ₯Ό μƒˆλ‘œ μƒμ„±ν•˜λŠ” μˆ˜κ³ κ°€ μ‘΄μž¬ν–ˆμ§€λ§Œ 낙관적 락을 μ„ νƒν–ˆμ„ κ²½μš°λ³΄λ‹€λŠ” 훨씬 λ‚˜μ€ λ“―.

@Repository
public interface UserRepository extends JpaRepository<User, String> {
  @Transactional
  @Lock(value = LockModeType.PESSIMISTIC_WRITE)
  Optional<User> findForUpdateByUser(String id);
}

λ‹€λ§Œ μ‹€μ œ ν˜„μ—…μ—μ„œ 같은 상황을 κ²ͺ게 λœλ‹€λ©΄ κ·Έ 땐 더 λ§Žμ€ μš”μ†Œλ₯Ό 고렀해봐야 ν•  κ²ƒμœΌλ‘œ 보인닀.