문제 상황
“같은 유저가 좋아요 버튼을 100번 동시에 누르는 상황”을 테스트했다.
int threads = 100;
ExecutorService pool = Executors.newFixedThreadPool(20);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(threads);
100개의 스레드가 동시에:
likeService.toggleLike(articleId, email);
을 실행하도록 구성했다.
기대했던 결과:
assertThat(likeCount).isBetween(0L, 1L);
하지만 실제 결과는:
Expected: 0~1
Actual: 78 (혹은 다른 값)
처음에는 버그처럼 보였지만,
이건 단순 버그가 아니라 토글 연산의 설계 문제였다.
1️⃣ 왜 브라우저에서는 멀쩡해 보일까?
브라우저에서 “빠르게 클릭”하는 상황은 실제로 이렇게 처리된다.
요청1 → 응답1 → 요청2 → 응답2 → 요청3 ...
즉,
거의 직렬 처리(sequential)
완전한 병렬이 아니다.
2️⃣ 그런데 테스트는?
테스트는 이렇게 구성했다.
100 threads
start.await()
동시에 DB 접근
이건 진짜 병렬 실행이다.
즉,
- UI 환경: 거의 순차 실행
- 테스트 환경: 완전 병렬 실행
이 차이가 결과를 완전히 다르게 만든다.
3️⃣ 토글 연산의 구조적 문제
현재 토글 로직은 이런 흐름이다.
1) delete 시도
2) 삭제 성공 → count 감소
3) 삭제 실패 → insert 시도
4) insert 성공 → count 증가
문제는 이 연산이 원자적(atomic)이지 않다는 것.
동시에 두 개가 들어오면?
초기 상태: 좋아요 없음
| Thread A | Thread B |
| delete → 0 | delete → 0 |
| insert 성공 | insert 충돌 또는 성공 |
| count +1 | count +1 |
결과는?
- 0일 수도
- 1일 수도
- 2 이상 누적될 수도
순서가 정의되지 않았기 때문에 결과도 정의되지 않는다.
4️⃣ 핵심 개념
❗ 토글은 비결정 연산이다
같은 유저가 동시에 100번 토글을 실행하면:
T T T T T T T ...
이 질문에 답할 수 있을까?
"최종 상태는 좋아요인가? 취소인가?"
정답:
❌ 알 수 없다
❌ 정의 자체가 불가능하다
왜냐하면
toggle = read + write
이 연산은 원자적이지 않기 때문이다.
5️⃣ 왜 다른 유저 100명 테스트는 통과할까?
user1 like
user2 like
user3 like
이 경우는:
- 서로 다른 row
- 유니크 충돌 없음
- delete 충돌 없음
즉,
단순 카운트 증가 동시성
그래서 정상 동작한다.
6️⃣ 업계에서는 어떻게 해결할까?
① 토글 API를 분리 (가장 일반적)
POST /like → 좋아요 ON
DELETE /like → 좋아요 OFF
토글 금지.
클라이언트가 상태를 명확히 전달하도록 설계한다.
② 프론트에서 중복 클릭 차단
버튼 클릭 → disabled
응답 후 다시 활성화
실서비스에서는 이 방식이 가장 많이 사용된다.
③ Redis 락 사용 (고급 방식)
userId + articleId 키로 짧은 락
하지만 대부분의 서비스에서는 과한 설계다.
7️⃣ 테스트 관점에서 무엇을 검증해야 할까?
다음 테스트는 논리적으로 성립하지 않는다.
assertThat(likeCount).isBetween(0L, 1L);
완전 병렬 환경에서
토글의 최종 상태를 예측하는 것은 불가능하다.
✔ 실무적으로 올바른 검증 기준
1. 서버가 죽지 않는다.
assertThat(errors).isEmpty();
또는 예외가 발생하더라도 제어 가능한 예외여야 한다.
2. DB 무결성이 깨지지 않는다.
assertThat(likeRows).isLessThanOrEqualTo(1);
assertThat(likeCount).isEqualTo(likeRows);
핵심은 이것이다.
좋아요 row는 1개를 초과하면 안 된다
likeCount와 실제 row 수는 반드시 같아야 한다
8️⃣ 진짜 실무 기준 성공 조건
내 구현이 성공했다고 볼 수 있는 조건:
✅ 다른 유저 동시 좋아요 → 정확한 count 증가
✅ 같은 유저 동시 연타 →
- 서버 에러 없음
- 좋아요 row 1개 초과 없음
- likeCount == 실제 row 개수 유지
최종 결론
토글은 겉보기에는 단순하지만,
동시성 환경에서는 원자적으로 정의되지 않는 연산이다.
UI에서는 잘 동작해 보여도,
진짜 병렬 환경에서는 깨질 수 있다.
이 테스트를 통해:
- 토글 연산의 한계
- 병렬 vs 순차 실행 차이
- 동시성 테스트의 올바른 기준
을 이해하게 되었다.
'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
| 👥🚨 좋아요 상태 변경 후 본인 게시글 삭제 오류 해결 (FK 제약 & DB CASCADE) (0) | 2026.02.14 |
|---|---|
| 👥💡 좋아요(Like) 기능 설계: 상태, 집계, 동시성까지 고려한 구현 정리 (0) | 2026.02.14 |
| 👥🚨 좋아요 동시성 테스트에서 교착상태가 발생한 이유와 해결 방법 (0) | 2026.02.14 |
| 👥🚨 좋아요 토글을 빠르게 연타하면 깨지는 이유와 해결 방법 (프론트 + 백엔드 동시성 정리) (0) | 2026.02.14 |
| 👥💡 원자적 방법이란? 벌크성 내용이랑 무슨 차이가 있는지? (0) | 2026.02.14 |