실무에서 동시성 이슈를 MongoDB의 Document를 활용한 분산락을 사용해서 제어하는 개발건을 맡아 진행했다. 본디 Redis를 쓰는게 정배이긴 하지만, 상황이 여의치 않아서 MongoDB로 진행하였다.
그런데 내가 개발했던 건에서 잘못된try-catch-finally
사용으로 중복제어 에러를 반환하면서 바로 Lock을 해제해버리는 바람에 동시성 이슈가 제대로 해소되지 않는 문제가 발생하는데..
public String delete(String id) throws InterruptedException {
try{
delayUtil.randomDelay();
lockService.tryLock(id);
// 비즈니스 로직 수행
delayUtil.randomDelay();
resourceRepository.deleteById(id);
} catch (DuplicateKeyException e) {
log.info("DUP ERROR in Thread = {}", Thread.currentThread().getName());
return "Fail (Lock)";
} finally {
log.info("UNLOCK in Thread = {}", Thread.currentThread().getName());
lockService.unLock(id);
}
return "Success";
}
이를 위한 테스트 코드
@Test
@DisplayName("삭제시 지연시간이 있을경우, 그리고 스레드가 한꺼번에 병렬로 실행될 경우 중복삭제가 일어난다")
void delete_withWeakLockLogicWithDelayAndParallel_shouldAllowMultipleDeletions() throws InterruptedException {
int concurrency = 100;
ExecutorService executor = Executors.newFixedThreadPool(concurrency);
CountDownLatch allThreadsLatch = new CountDownLatch(1); // 모든 스레드를 일괄 시작
CountDownLatch doneLatch = new CountDownLatch(concurrency); // 완료 대기
List<String> results = Collections.synchronizedList(new ArrayList<>());
delayUtil.setTestDelayMs(100);
for (int i = 0; i < concurrency; i++) {
executor.submit(() -> {
try {
allThreadsLatch.await();
String result = userService.delete("shared-id");
results.add(result);
} catch (DuplicateKeyException e) {
results.add("Duplication Exception");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
doneLatch.countDown();
}
});
}
allThreadsLatch.countDown(); // 모든 스레드 동시에 출발 -> for문 돌면서 순차적으로 진행하는 것을 막음 병렬화되는 느낌
doneLatch.await(); // 모두 종료될 때까지 대기
executor.shutdown();
long successCount = results.stream().filter("Success"::equals).count();
System.out.println(results.toString());
assertThat(successCount)
.as("Delay시간이 없으므로, 하나의 스레드만 삭제 성공 ")
.isGreaterThan(1);
}
왜 중복이슈가 발생하지 않고 모두 정상적으로 Unlock 처리될까?