2024. 2. 4. 03:41γBackend/Spring
JPAμ @Transactional μ΄λ Έν μ΄μ μ λν μ΄ν΄λ₯Ό μ¬λ¦΄ κ²Έ, μλ λ ν¬λ₯Ό μμ±νκ³ νμ΅ ν μ€νΈλ₯Ό μ§ννλ€.
ν μ€νΈ μμ λ μΌλ°μ μΌλ‘ λμμ± μμ μ λ§μ΄ λ±μ₯νλ κ³μ’ μ΄μ²΄ 컨ν μ€νΈλ₯Ό κ°λ¨νκ² κ΅¬ννκ³ μ νλ€.
μλλ μ΄λ₯Ό μν μνμ€λ₯Ό λ§λ‘μ¨ μ 리ν λ΄μ©μ΄λ€.
- μ¬μ©μ Aλ₯Ό μ‘°ν
- μ¬μ©μ Bλ₯Ό μ‘°ν
- μ¬μ©μ 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μ κ²½μ° κ°μ₯ μ΅κ·Όμ λ°μν λ°λλ½μ νμΈν μ μκΈ° λλ¬Έμ μ΄λ₯Ό μ΄μ©νμ¬ μ λ°λλ½μ΄ λ°μνλμ§ λΆμν μ μμλ€.
μ΄λ₯Ό 보면 μν©μ μλμ²λΌ μ 리ν μ μλ€.
- λͺ¨λ μ€λ λλ€μ μμ μ΄ μ λ°μ΄νΈ ν΄μΌ ν λ μ½λλ€μ μ‘°ννλ€. μ΄ λ 격리μμ€μ μν΄ λ μ½λμ S λ½μ 건λ€.
- κ·Έ ν, μμ°¨μ μΈ μ λ°μ΄νΈλ₯Ό μν΄ λ μ½λ νλμ λν΄ X λ½μ νλνκ³ μ νλ€.
- μ΄ λ, λ€λ₯Έ μ€λ λμμλ ν΄λΉ λ μ½λμ λν 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);
}
λ€λ§ μ€μ νμ μμ κ°μ μν©μ κ²ͺκ² λλ€λ©΄ κ·Έ λ λ λ§μ μμλ₯Ό κ³ λ €ν΄λ΄μΌ ν κ²μΌλ‘ 보μΈλ€.