👥🚨 같은 유저가 좋아요를 동시에 연타할 때 동시성 테스트가 깨지는 이유

2026. 2. 14. 19:40·프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트

문제 상황

“같은 유저가 좋아요 버튼을 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
'프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
  • 👥🚨 좋아요 상태 변경 후 본인 게시글 삭제 오류 해결 (FK 제약 & DB CASCADE)
  • 👥💡 좋아요(Like) 기능 설계: 상태, 집계, 동시성까지 고려한 구현 정리
  • 👥🚨 좋아요 동시성 테스트에서 교착상태가 발생한 이유와 해결 방법
  • 👥🚨 좋아요 토글을 빠르게 연타하면 깨지는 이유와 해결 방법 (프론트 + 백엔드 동시성 정리)
hak0622
hak0622
개발하면서 “이게 뭐지?”라는 순간마다 궁금한 점을 바탕으로 정리한 개발 블로그입니다.
  • hak0622
    궁금한 개발 이야기 Why?
    hak0622
  • 전체
    오늘
    어제
    • 분류 전체보기 (68)
      • 공부 (36)
        • 1. 자바 ORM 표준 JPA 프로그래밍 - 기본.. (35)
        • 시험 (1)
      • 프로젝트 (32)
        • 스프링 부트 3 백엔드 개발자 되기 Blog + .. (32)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hak0622
👥🚨 같은 유저가 좋아요를 동시에 연타할 때 동시성 테스트가 깨지는 이유
상단으로

티스토리툴바