- Java에서 ReentrantLock을 활용한 동시 입금 제어 및 실패 테스트 정리2025년 05월 23일 21시 20분 27초에 업로드 된 글입니다.작성자: do_hyuk728x90반응형
동시 입금 요청이 들어왔을 때, 하나만 성공하고 나머지는 실패하도록 제어하는 로직을 테스트하던 중 예상과 다르게 모든 요청이 처리되는 문제가 있었다. 아래는 해당 이슈를 해결한 과정이다.
🧩 문제 상황
다음은 특정 사용자 계좌에 입금을 처리하는 credit 메서드이다.
입금 시 ReentrantLock을 활용해 동시에 여러 요청이 처리되지 않도록 제어하고 있다.public Wallet credit(Long id, int amount) { checkAmount(amount); ReentrantLock lock = getReentrantLock(id); boolean locked = lock.tryLock(); if (!locked) { throw new HttpClientErrorException(HttpStatus.CONFLICT); } try { Wallet wallet = bank.findById(id); int newBalance = wallet.getBalance() + amount; return wallet.update(newBalance); } finally { if (locked) { lock.unlock(); } } }
이 메서드는 락 획득에 실패한 경우 409 Conflict 예외를 던지도록 설계되어 있다.
그리고 아래는 동시 요청 실패를 테스트하는 JUnit 테스트 코드이다
@Test void 한명에게_동시에_입금요청이_올_경우_실패_테스트() throws InterruptedException { log.info("동시 입금 요청 실패 테스트 시작"); ExecutorService executor = Executors.newFixedThreadPool(10); List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>()); Long id = 1L; int creditAmount = 1000; int cnt = 10; CountDownLatch countDownLatch = new CountDownLatch(cnt); Wallet newWallet = walletService.createAccount(id); for (int i = 0; i < cnt; i++) { executor.submit(() -> { try { walletService.credit(id, creditAmount); } catch (Exception e) { exceptions.add(e); log.info("입금 실패: {}", e.getMessage()); } finally { countDownLatch.countDown(); } }); } countDownLatch.await(); executor.shutdown(); for (Throwable e : exceptions) { assertInstanceOf(HttpClientErrorException.class, e); assertEquals(((HttpClientErrorException) e).getStatusCode(), HttpStatus.CONFLICT); } log.info("동시 입금 실패 테스트 완료 / 현재 잔액: {}", newWallet.getBalance()); }
🧨 문제 원인: getReentrantLock()이 항상 새로운 락 반환
private ReentrantLock getReentrantLock(Long id) { ReentrantLock lock = locks.computeIfAbsent(id, k -> new ReentrantLock()); lock.lock(); return lock; ❌ 매번 새 인스턴스 반환 }
이 방식은 호출할 때마다 새로운 ReentrantLock 인스턴스를 반환하므로 락 자체가 무의미해지고, 동시 요청을 막지 못하게 된다.
✅ 해결 방법: ConcurrentHashMap을 사용한 락 재사용
ReentrantLock lock = locks.computeIfAbsent(id, k -> new ReentrantLock());
이렇게 수정하면 id마다 동일한 락 인스턴스를 사용하게 되어 tryLock()이 정확히 동작하게 됩니다.
🧼 정리
- 동시성 제어에서 중요한 건 락이 객체 간 공유되는지이다.
- ReentrantLock은 직접 객체를 관리해야 하므로 ConcurrentMap 같은 구조를 통해 공유 락을 구현해야 한다.
- tryLock()을 사용할 때는 잘못된 락 관리로 인해 무의미하게 될 수 있으니 주의해야 한다.
728x90반응형'백엔드' 카테고리의 다른 글
어떤 이유로 코루틴을 사용한 작업 처리가 기존 스레드 방식보다 가벼운지 설명해주세요. (0) 2025.05.26 🔍 Java 동시성 제어와 테스트 로드맵 요약 (0) 2025.05.23 💼 프로젝트 과제 예제: “은행 계좌 관리 시스템” (1) 2025.05.21 RDB에서 페이징 쿼리의 필요성을 설명해 주세요. (0) 2025.05.19 Spring에서 객체를 Bean으로 관리하는 이유를 설명해주세요. (0) 2025.05.16 댓글