문제 상황
“여러 명이 동시에 좋아요를 누르는 상황”을 검증하기 위해
ExecutorService + CountDownLatch를 사용해 동시성 테스트를 작성했다.
하지만 테스트를 실행하면:
- Article은 생성됨
- like는 0
- 테스트가 멈춘 것처럼 보임
- 교착 상태 발생
처음에는 DB 문제처럼 보였지만, 실제 원인은 테스트 코드 구조 자체의 교착이었다.
1️⃣ 기존 테스트 구조
핵심 부분은 다음과 같았다.
ExecutorService pool = Executors.newFixedThreadPool(32);
CountDownLatch ready = new CountDownLatch(threads); // threads = 100
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(threads);
각 작업 흐름:
ready.countDown();
start.await(); // 여기서 대기
likeService.toggleLike(...);
done.countDown();
그리고 메인 스레드:
ready.await(); // 100개가 다 준비될 때까지 대기
start.countDown(); // 시작 신호
done.await();
2️⃣ 왜 교착상태가 발생했는가?
핵심 원인은 스레드풀 크기(32)와 readyLatch(100)의 조합이다.
실행 흐름을 보면
- 스레드풀은 32개뿐이다.
- 처음 32개 작업만 실행된다.
- 그 32개는 start.await()에서 대기한다.
- 이 32개가 스레드를 전부 점유한다.
- 나머지 68개 작업은 큐에서 실행되지 못한다.
- 그런데 메인 스레드는 ready.await()로 100개가 모두 준비되길 기다린다.
하지만 68개는 실행 자체를 못 하므로
ready.countDown()이 절대 100까지 내려가지 않는다.
즉,
ready가 100이 되어야 start를 풀 수 있는데
start를 풀기 전에는 나머지 작업이 실행될 수 없다.
완전한 교착 구조다.
3️⃣ 해결 방법: readyLatch 제거 (현실적인 동시성 테스트)
“100개가 완전히 같은 순간에 시작”을 강제할 필요가 없다면
readyLatch는 제거하는 것이 가장 안전하다.
수정된 테스트:
@Test
void manyUsers_likeConcurrently() throws Exception {
int threads = userEmails.size();
ExecutorService pool = Executors.newFixedThreadPool(32);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(threads);
for (String email : userEmails) {
pool.submit(() -> {
try {
start.await();
likeService.toggleLike(articleId, email);
} catch (Exception e) {
e.printStackTrace();
} finally {
done.countDown();
}
});
}
start.countDown();
boolean finished = done.await(30, TimeUnit.SECONDS);
pool.shutdown();
assertThat(finished).isTrue();
em.clear();
long likeCount = blogRepository.getLikeCount(articleId);
long likeRows = likeRepository.countByArticleId(articleId);
assertThat(likeCount).isEqualTo(threads);
assertThat(likeRows).isEqualTo(threads);
}
4️⃣ 이게 동시성 테스트가 맞을까?
맞다.
다만 중요한 차이가 있다.
기존 목표
- 100명이 “완전히 같은 순간”에 시작
실제 동작
- 스레드풀 32개
- 동시에 실행 가능한 작업은 최대 32개
- 나머지는 큐에서 대기
즉,
동시성은 존재하지만, 강도는 32로 제한된 테스트
5️⃣ 그런데 오히려 이 방식이 더 현실적이다
실제 서버 환경을 생각해보자.
- 톰캣 스레드 풀
- DB 커넥션 풀
- 트랜잭션 처리
현실에서는:
100명이 동시에 클릭해도
DB에 100개 쿼리가 “완전히 같은 순간”에 들어가지는 않는다.
항상 일정 수만 동시에 처리되고, 나머지는 큐잉된다.
따라서
- 32개 동시 실행
- 나머지 대기 후 순차 처리
이 구조는 오히려 현실 서버 환경을 더 잘 흉내 낸 테스트다.
6️⃣ 동시성 검증 관점에서 충분한가?
좋아요 토글과 같은 경우 검증 목표는 보통 다음이다.
- 유실 업데이트가 없는가?
- likeCount와 실제 like row 개수가 일치하는가?
- 유니크 충돌이 안전하게 처리되는가?
- 카운트가 음수로 내려가지 않는가?
이 목적에는
32개 동시 실행 테스트만으로도 충분히 의미 있다.
극단적인 “100개 완전 동시 시작”은
실무에서는 거의 필요하지 않다.
7️⃣ 최종 정리
교착 원인
- 스레드풀 32개
- readyLatch 100
- start.await()에서 스레드 점유
- ready.await()는 100까지 기다림
- 실행 불가능 구조 → 교착
해결
- readyLatch 제거
- startLatch + doneLatch만 사용
- 타임아웃을 둬서 무한 대기 방지
핵심 교훈
동시성 테스트는 “완전한 동시 시작”을 만드는 것이 목적이 아니라
여러 스레드가 동시에 같은 자원을 건드리는 상황을 재현하는 것이 목적이다.
'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
| 👥💡 좋아요(Like) 기능 설계: 상태, 집계, 동시성까지 고려한 구현 정리 (0) | 2026.02.14 |
|---|---|
| 👥🚨 같은 유저가 좋아요를 동시에 연타할 때 동시성 테스트가 깨지는 이유 (0) | 2026.02.14 |
| 👥🚨 좋아요 토글을 빠르게 연타하면 깨지는 이유와 해결 방법 (프론트 + 백엔드 동시성 정리) (0) | 2026.02.14 |
| 👥💡 원자적 방법이란? 벌크성 내용이랑 무슨 차이가 있는지? (0) | 2026.02.14 |
| 👥💡 조회수 동시성 문제 해결(원자적) (0) | 2026.02.14 |