운영 중인 앱: 축잘알 테스트 (구글 플레이)
🐇 비동기 처리로 API 성능과 데이터 정확성을 모두 잡을 수 있을까?
– 퀴즈 결과 저장 API에 JMeter로 부하 테스트를 걸어봤다
혼자 기획하고 개발 한 퀴즈 앱 ‘축잘알 테스트’에서는
사용자가 퀴즈를 풀고 나면 결과를 저장하는 API가 있다.
앱의 특성상, 사용자 수가 많아질수록 가장 자주 호출되는 API가 바로 이 퀴즈 결과 저장 API다.
그리고 단순 조회가 아니라 데이터를 변경하는 API이기도 하다.
그래서 직접 JMeter를 활용해
이 API에 고부하 테스트를 걸어보고,
동기/비동기 구조에 따른 응답 속도와 데이터 정확성을 비교해보기로 했다.
🎯 실험 배경
- 테스트 대상: 퀴즈 결과 저장 API
- 단일 사용자(nickName: tttt)가 여러 번 푼다는 가정
- 실제 앱에서는 5, 10, 15, 20문제를 풀 수 있지만,
테스트에선 3개 문제를 풀고 2개 맞춘 것으로 고정
{
"nickName": "Test",
"totalQuizCount": 3,
"solvedQuizCount": 2
}
- 기대 결과:
- 요청 수: 40,000건
- 기대 누적값:
- totalQuizCount: 3 × 40,000 = 120,000
- solvedQuizCount: 2 × 40,000 = 80,000
- JMeter 조건:
- Threads: 2000
- Ramp-up: 30초
- Loop: 20
① ❌ 동기 + 객체 연산 방식
예시코드
FrontUserInfo user = userRepository.findByNickName(...);
user.setTotalQuizCount(user.getTotalQuizCount() + 3 (사용자가 푼 문제) );
user.setSolvedQuizCount(user.getSolvedQuizCount() + 2 (사용자가 맞춘 문제) );
userRepository.save(user);
기본적인 방식.
하지만 DB에서 값을 읽고 객체에서 더한 뒤 저장하는 방식은
동시성 문제가 그대로 드러난다.
📸결과:
항목 | 값 |
totalQuizCount | ❌ 12,045 |
solvedQuizCount | ❌ 8,030 |
평균 응답 시간 | 1,404ms |
Throughput | 684.8/sec |
→ 응답은 OK지만, 데이터는 대부분 유실됨.
→ race condition으로 인해 정합성 완전 실패.
② ⚠️ 동기 + SQL 쿼리 연산 방식
다음은 DB에서 직접 연산하도록 구조를 변경했다.
@Modifying
@Query("UPDATE FrontUserInfo f SET " +
"f.totalQuizCount = f.totalQuizCount + :total, " +
"f.solvedQuizCount = f.solvedQuizCount + :solved " +
"WHERE f.nickName = :nickName")
void incrementQuizCount(@Param("nickName") String nickName,
@Param("total") long total,
@Param("solved") long solved);
결과:
항목 | 값 |
totalQuizCount | ⚠️ 119,778 |
solvedQuizCount | ⚠️ 79,852 |
평균 응답 시간 | 6,621ms |
Throughput | 240.8/sec |
→ race condition은 거의 해결,
→ 하지만 여전히 222건 누락,
→ 성능은 여전히 낮은 수준.
③ ✅ 비동기 처리 (RabbitMQ)
마지막으로, 메시지 큐를 사용한 구조로 바꿨다.
API는 데이터를 MQ에 넣고 바로 응답하며,
실제 저장은 Consumer가 순차적으로 처리한다.
@PostMapping("/front-user/update")
public String updateUserAsync(@RequestBody QuizResultMessage msg) {
rabbitProducer.send(msg);
return "OK";
}
@RabbitListener(...)
public void consumeQuizResult(QuizResultMessage msg) {
userRepository.incrementQuizCount(...);
}
API는 요청만 받아서 RabbitMQ에 메시지를 넣고,
백그라운드 Consumer가 순차적으로 DB를 갱신한다.
→ 병목 없이, race condition이 완전히 사라진 구조.
결과:
항목 | 값 |
totalQuizCount | ✅ 120,000 |
solvedQuizCount | ✅ 80,000 |
평균 응답 시간 | ✅ 1ms |
Throughput | ✅ 1,333/sec |
→ 완벽한 저장 + 빠른 응답
→ race condition 없음
📊 전체 비교 요약
구조 | 응답시간 | Throughput | 저장 결과 | 정합성 |
동기 + 객체 연산 | 1,404ms | 684.8/sec | 12,045 | ❌ |
동기 + SQL 쿼리 연산 | 6,570ms | 242.3/sec | 119,778 | ⚠️ |
비동기 + MQ | 1ms | 1333/sec | 120,000 | ✅ |
💡 실험을 통해 얻은 인사이트
- 동기 구조는 단순해 보이지만,
데이터 정합성에 치명적인 문제(race condition) 가 존재함 - SQL 레벨에서 연산을 처리하면 정합성은 어느 정도 개선되지만,
성능 병목은 여전 - 비동기 구조에서는 요청을 큐에 쌓아두고 순차 처리하기 때문에
정합성은 물론, 응답 속도도 압도적으로 개선됨
✅ 마무리
이번 실험을 통해
"빠르게 처리하는 것"보다
"정확하게 처리하는 구조가 더 중요하다" 는 걸 확실히 느꼈다.
RabbitMQ 기반의 비동기 구조는
실제 서비스에서 높은 트래픽, 데이터 정합성, 안정성을 모두 만족시키기 위한
실질적인 해법이 될 수 있다는 점을 배웠다.
'그냥 코딩' 카테고리의 다른 글
스프링 오픈소스 컨트리뷰트 하기 (1) | 2023.08.25 |
---|---|
[C언어] 문자열 뒤집기 (0) | 2019.01.24 |
[C언어] 문자열 공백제거 출력 (0) | 2019.01.24 |
[C언어] 문자열 대소문자 변환 (0) | 2019.01.24 |