1️⃣ 문제 상황
JMeter로 동시 사용자 300명 / 500명 환경에서 Enroll(신청) API 동시성 테스트를 진행하던 중, 03_enroll 요청이 빨간색으로 표시되며 다음과 같은 응답이 발생했다.
{
"timestamp": "2026-02-14T07:10:23.474+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/lectures/1/enroll"
}
IntelliJ 서버 로그에는 다음과 같은 예외가 출력되었다.
java.lang.IllegalStateException: 정원이 마감되었습니다.
at studying.blog.service.EnrollService.enroll(EnrollService.java:37)
2️⃣ 처음에는 이렇게 생각했다
- “동시 요청이 몰려서 서버가 터진 건가?”
- “Row Lock이 잘못된 건가?”
- “데이터가 깨진 건가?”
하지만 DB를 확인해보니:
SELECT COUNT(*) FROM ENROLLMENT;
결과는 정확히 100건이었다.
또한:
SELECT ENROLLED_COUNT, CAPACITY FROM LECTURE WHERE ID=1;
ENROLLED_COUNT = 100
CAPACITY = 100
즉, 정원 초과 저장은 발생하지 않았다.
데이터 정합성은 정상적으로 유지되고 있었다.
3️⃣ 그럼 왜 500이 발생한 걸까?
핵심은 이 코드였다.
if (!lecture.hasSeat()) {
throw new IllegalStateException("정원이 마감되었습니다.");
}
동시 사용자 300명 / 500명이 신청하면:
- 앞의 100명은 정상적으로 신청 성공
- 나머지는 "정원이 마감되었습니다." 예외 발생
이건 정상적인 비즈니스 실패였다.
하지만 Spring은 예외가 컨트롤러 밖으로 그대로 전달되면, 기본적으로 500 Internal Server Error로 응답한다.
즉,
정상적인 정원 마감 상황을
서버 내부 오류(500)로 잘못 표현하고 있었던 것이다.
4️⃣ 문제의 본질
- 정원 마감은 "서버가 터진 것"이 아니라
- 클라이언트 요청이 비즈니스 조건에 맞지 않은 상황이다.
HTTP 관점에서 보면 이는:
- 500 ❌ (서버 내부 오류 아님)
- 409 Conflict ⭕ (자원 상태 충돌)
- 또는 403/400 계열
이 더 적절하다.
5️⃣ 해결 방법: 전역 예외 처리 도입
비즈니스 예외를 적절한 HTTP 상태 코드로 내려주기 위해 @RestControllerAdvice를 추가했다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalStateException.class)
public ResponseEntity<?> handleIllegalState(IllegalStateException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of(
"status", 409,
"error", e.getMessage()
));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<?> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of(
"status", 400,
"error", e.getMessage()
));
}
}
이후 다시 JMeter 테스트를 진행했다.
6️⃣ 개선 결과
동시 사용자 500명 테스트 시:
- 신청 성공: 100명 → 200 OK
- 정원 초과: 다수 → 409 Conflict
- 500 Internal Server Error → 0건
응답 예시 :
{
"error": "정원이 마감되었습니다.",
"status": 409
}
DB 확인 결과:
SELECT COUNT(*) FROM ENROLLMENT;
100
정합성도 유지되고, 상태 코드도 정상적으로 구분되었다.
7️⃣ 배운 점
① HTTP 200/500만 보면 안 된다
JMeter에서 빨간색이 나와도,
그게 서버 오류인지, 의도된 비즈니스 실패인지 구분해야 한다.
② 비즈니스 실패와 서버 오류는 반드시 구분해야 한다
- 정원 마감은 500이 아니다.
- 500은 진짜 서버 버그일 때만 사용해야 한다.
③ 동시성 테스트는 “에러 코드 설계”까지 포함해야 한다
단순히 DB에 100건만 저장되는지만 확인하는 것이 아니라,
- 실패 응답이 의미 있는 상태 코드로 내려가는지
- 클라이언트가 구분 가능한지
까지 고려해야 완성된 API 설계라고 느꼈다.
8️⃣ 정리
이번 경험을 통해 단순히 동시성 제어를 구현하는 것에서 끝나는 것이 아니라,
- 부하 상황을 직접 재현하고
- 문제를 발견하고
- 예외 설계를 개선하고
- 다시 검증하는 과정
이 중요하다는 것을 배웠다.
정원 초과 상황을 500에서 409로 개선함으로써
API의 의미적 정확성과 안정성을 함께 확보할 수 있었다.
'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
| ⏰🚨 선착순 강의 신청 페이지에서 버튼이 동작하지 않던 문제 해결 (0) | 2026.02.23 |
|---|---|
| ⏰💡 소셜 로그인 + 선착순 강의 신청 시스템 통합 (JWT 기반 userId 전환) (1) | 2026.02.23 |
| ⏰🚨 Enroll 동시성 테스트에서 신청이 거의 발생하지 않은 문제(JMeter) (0) | 2026.02.15 |
| ⏰💡 Enroll 동시성 테스트 + 폴링 API 부하테스트 진행 방법(JMeter) (0) | 2026.02.15 |
| ⏰💡 폴링 API 부하 테스트 (500명 실험) (0) | 2026.02.15 |