| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- 일정관리 프로젝트
- S3
- 알고리즘
- LV.02
- 포트폴리오
- GIT
- spring boot
- 이것이 자바다
- Join
- 데이터 베이스
- mysql
- SQL
- 연습문제
- JPA
- CI/CD
- Redis
- LV01
- CoffiesVol.02
- Kafka
- 코테
- 일정관리프로젝트
- 디자인 패턴
- docker
- LV02
- LV03
- jvm
- 프로그래머스
- LV0
- Java
- Lv.0
- Today
- Total
코드 저장소.
Kafka Exactly-Once, 실제로 버티나? 50VU 실패부터 에러율 0% 달성까지 본문
목차
1. 테스트 환경 및 도구
2. 테스트 시나리오
3. 정상 테스트 결과
4. 부하 테스트 결과
5. 일괄 테스트 결과
6.후기
1. 테스트 환경 및 도구
테스트를 해볼 서버와 측정 도구에 대한 설명은 아래와 같습니다
서버 환경
- 서버: 2GB VM (JVM 힙 512m~1g, HikariCP max=20)
- API: 일정 생성 API (/api/schedule/)
- 인증: JWT 헤더 포함
측정 도구
- Jmeter : 요청 부하 발생 및 응답 시간 측정
- Grafana (Prometheus 연동): JVM Heap, GC, DB 커넥션 풀 등 서버 내부 지표 모니터링
2. 테스트 시나리오
정상 테스트
- 부하 조건이 없는 상황에서 일정 생성 API의 정상 동작 및 응답 시간을 검증한다
시나리오
- JMeter로 일정 생성 API를 10회 반복 호출
- Grafana(Prometheus 연동)로 JVM Heap, GC, DB Connection Pool 등 내부 지표를 모니터링
- API 응답 시간, 에러율, 처리 결과 확인
성공 기준
- 모든 요청에서 에러율 0%
- 평균 응답 속도: 50ms~100ms 내외 유지
- DB Connection Pool, GC, Heap 사용량에서 이상 징후 없음
- Kafka Outbox → Consumer → DB 저장까지 전파가 정상적으로 완료됨
부하 테스트
- 동시 사용자 요청 증가(50명 → 100명) 상황에서 API → Kafka → Consumer → DB 저장까지의 전체 흐름에 병목 구간이 없는지 검증한다
시나리오
- JMeter
- Virtual User 50명, 100명 시나리오로 테스트 진행
- Ramp-up: 30초, Duration: 180초
- 응답 시간(P95, P99), 에러율, TPS(Throughput) 측정
- Grafana (Prometheus + Kafka Exporter)
- Consumer 처리량(Records/sec), Lag, 처리 지연시간 확인
- JVM Heap/GC, DB Connection Pool 지표 수집
- 결과 분석
- JMeter 에러율 기준: 0% 유지 여부 확인
- Consumer Lag이 안정적으로 해소되는지 확인
- 특정 지점(API, Kafka, DB)에서 병목 현상 발생 여부 확인
성공 기준
- 동시 접속자 100명까지 에러율 0% 유지
- Kafka Consumer Lag이 단기간 내 해소되어 처리량이 정상화됨
- JVM/DB Pool에서 리소스 병목 없음
- 평균 응답속도 및 P95 지연이 허용 범위 내(예: <1s) 유지됨
일괄 처리 테스트
- Kafka Producer → Broker → Consumer → DB 저장까지의 전체 파이프라인에 대해
대량 이벤트(1만, 3만, 5만 건) 일괄 처리 시 유실 0, 중복 0을 보장(EOS)하는지 검증한다
시나리오
- JMeter로 동일 이벤트(1만, 3만, 5만 건)를 일괄 publish
- Kafka Consumer가 이벤트를 처리하여 DB에 저장
- DB 레벨에서 eventId를 unique key로 사용, 중복 발생 시 409를 정상 처리로 간주
- 처리 과정에서 DLQ/Retry가 정상적으로 동작하는지도 함께 관찰
성공 기준
- 전체 이벤트 건수 = DB 저장 건수 (유실 0건)
- 중복 저장 없음 (409 발생 시 “정상 차단”으로 집계)
- 장애 발생 시 DLQ로 이관 후 재처리 → 최종 정상 처리율 100%
- 처리율/지연시간/Consumer Lag 메트릭 기록
우선은 Jmeter로 측정을 할때 일정생성의 요청값으로 validation에 통과가 되게끔 설정을 아래와 같이 했습니다.
import org.apache.commons.lang3.RandomUtils
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
// 날짜 범위 (2025-09-19 ~ 2025-12-30)
def startDate = LocalDate.of(2025, 9, 19)
def endDate = LocalDate.of(2025, 12, 30)
def daysBetween = ChronoUnit.DAYS.between(startDate, endDate)
def randomOffset = RandomUtils.nextInt(0, (int)daysBetween + 1)
def date = startDate.plusDays(randomOffset)
// userId (1~50 랜덤)
def userId = RandomUtils.nextInt(1, 51)
// userId별 시간대 기본 배정 (0~23시)
def baseHour = userId % 24
// 같은 시간대라도 분 단위 랜덤 오프셋 추가 → 충돌 최소화
def offsetMin = RandomUtils.nextInt(0, 60)
// 시작/종료 시간 (1시간 일정)
def startTime = LocalDateTime.of(date, LocalTime.of(baseHour, offsetMin))
def endTime = startTime.plusHours(1)
// 포맷
def formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
// JMeter 변수 저장
vars.put("startTime", startTime.format(formatter))
vars.put("endTime", endTime.format(formatter))
vars.put("scheduleDays", String.valueOf(date.getDayOfMonth()))
vars.put("contents", "테스트-" + System.nanoTime())
vars.put("userId", String.valueOf(userId))
3. 정상 테스트 결과
정상 테스트를 실행한 결과는 아래와 같습니다. 10회 반복을 해서 일정을 생성을 하는 방식입니다.







10회 반복시의 결과는 아래와 같이 볼 수 있습니다.
JMeter 결과 (API 응답 관점)
- 평균 응답속도: 약 74ms
- P95 지연시간: 57ms
- 최대 응답시간: 268ms
- 에러율: 0% (모든 요청 성공)
- Throughput: 약 13 req/s
Outbox Publisher 메트릭
- 발행 건수(rate): 약 0.33 msg/s → 10건 발행된 게 그래프에 반영됨.
- 평균 발행 지연시간: 0.005 ~ 0.035초
- 최대 발행 지연시간: 순간적으로 0.28초까지 튐.(대체로 수 ms 수준으로 빠르지만, 일부 피크에서 수백 ms까지 튀는 케이스가 있었습니다.)
Kafka Consumer (NotificationEventConsumer)
- 평균 소비 지연시간: 0.04 ~ 0.05초
- 최대 소비 지연시간: 0.2초 근처에서 피크 발생.
- 처리량: 10건 모두 정상적으로 소비 확인.(소비자도 정상 처리했고, EOS/멱등 처리 구조상 중복/유실 없음. 다만, 순간적으로 처리 지연이 살짝 발생을 했습니다.)
위의 내용을 토대로 정상 테스트는 성공적으로 마친것을 검증을 했습니다.
4. 부하 테스트 결과
4-1. 50VU 1차 테스트 ( 커넥션 풀 포화로 인한 실패)


Kafka / Outbox / DLQ
Kafka Consumer 처리량: 피크 ~30msg/s, backlog 없이 모두 소화.
Consumer Latency: 평균 0.2s 내외, Max 지연 없음.
Outbox Publish Rate: 초당 0.3~0.35msg/s 유지.
Outbox Avg Latency: 부하 구간(15:24~15:25)에서 2~3초까지 튐 → DB 처리 지연 영향.
DLQ Retry: 거의 0, 재처리율 정상.
EOS 관점: 유실/중복 없이 처리 정상.
JVM / GC
Heap 사용량: 200MB 내외에서 안정적, Old Gen 증가 없음.
GC Pause: 최대 6ms 수준, 애플리케이션에 영향 없음.
결론: JVM 튜닝 상태 양호, 부하에 영향 미미.
DB (HikariCP)
Active Connections: 피크 시 maxPoolSize=20 전부 사용
Idle Connections: 0으로 떨어지며 풀 포화 상태 발생
Pending Threads: 잠깐 튀었지만 0으로 회복 → 대기 큐 생겼다 해소된 상황
결론: DB Connection Pool이 성능 한계 지점
HTTP API (JMeter + Grafana)
Request Rate: 초당 최대 120req/s까지 도달
Average Latency: /api/schedule 호출 시 0.4~0.6s 수준으로 상승
Error Rate: 피크 시 초당 120건 이상 5xx 발생
원인 1: DB 풀 포화로 인한 트랜잭션 처리 지연
원인 2: 일정 충돌 같은 Validation 실패가 5xx로 분류됨 → 실제보다 에러율 과장
문제점
- DB Connection Pool 포화
- maxPoolSize=20 제한으로 인해 Outbox 지연과 5xx 에러율 증가 발생
- 테스트 데이터 유효성 부족
- 랜덤 데이터로 인해 일정 충돌 빈번 → Fail 응답 발생
- Validation 실패가 5xx로 잡히면서 Error Rate 지표 왜곡
1차 개선
HikariCP maximum-pool-size를 20에서 50으로 조정
userId 범위 확대, 시간 슬롯 제한 -> Jmeter에 JSR PreProcessor 수정으로 충돌 최소화
Validation 실패는 4xx로 내려서 모니터링 정확성 확보
50명 부하 2차 테스트





1. JMeter 결과
- 샘플 수: 3,801건 (동시접속 50명 기준)
- 평균 응답시간: 187ms
- P90/P95/P99: 317ms / 376ms / 606ms
- 최대 응답시간: 1,036ms
- 에러율: 0.00% (409도 정상 처리로 잡힘)
- Throughput: 21.2 req/sec
2. JVM & DB Connection (HikariCP)
- JVM Heap Usage: 약 150MB 선에서 플랫
- GC Pause: 0.01 ~ 0.017초
- HikariCP Active Connections: 순간적으로 40개 근접, 이후 20개 안쪽 유지.
- Idle Connections: 10개 이상 항상 유지.
- Pending Threads: 0
3. Kafka & Outbox
- Consumer 처리량(msg/s): 피크 때 30 msg/s 정도
- Consumer 평균 지연(latency): 0.02 ~ 0.08초
- Outbox Publish Rate: 0.3 msg/s 근처에서 안정적으로 유지.
- DLQ Retry: 0 (실패 없음)
- Outbox 평균 지연: 초기 1.2초 → 곧 0.1초 이내로 수렴
4.HTTP
- Request Rate: 약 250~300 req/s에서 안정
- HTTP Avg Latency: 0.12 ~ 0.18초
- HTTP Error Rate(5xx): 없음 → 에러율 0%.
50명 동시 접속 시 평균 응답속도 160~180ms, 에러율 0%, Kafka/DB/GC 모두 정상이고 Kafka 메시징과 Outbox 스케줄러는 warm-up 후 안정화된다는 것을 알 수 있으며 DB 커넥션 풀은 순간 부하에도 병목 없이 잘 동작이 되는것을 알 수 있습니다.
다음은 100VU로 해서 테스트를 한 결과입니다.




- 평균 응답 시간: 약 1155ms
- Median: 1069ms
- 95% Line: 2187ms
- 최대값: 4454ms
- Throughput: ~14.6/sec
- 에러율: 0% (409 충돌은 정상 처리로 분류)
2. JVM Heap / GC & DB
- Heap 사용량 안정적 (150MB 근처 유지).
- GC Pause < 0.01s으로 메모리에는 문제가 없습니다.
- Active/Idle이 풀사이즈(50)에 꽉 차는 순간이 있긴 합니다.
- Pending Threads는 0 으로 풀 대기열 병목이 없다는 것을 알 수 있습니다..
3. kafka & Outbox
- Kafka Consumer 처리량: 순간적으로 30~40 msg/s까지 올라갔다가 정상적으로 떨어짐 → 컨슈머 레벨 병목 없음.
- Outbox Avg Latency: 초반에 8~9초까지 치솟았으나 빠르게 안정화되어 1초 미만으로 내려옴 → Outbox → Kafka Publish 구간은 스파이크만 있었음.
4. HTTP
- HTTP Avg Latency: 평균 0.8~1.0초 → JMeter와 일관됨.
- HTTP Error Rate(5xx): 없음 → 에러율 0%.
마찬가지로 동시 접속 100명도 충분히 버틸수 있다는것을 검증했습니다.
5. 일괄 테스트 결과
5-1. 3만건 일괄 테스트


1. JMeter 결과 (일괄 30,000건 삽입)
- 평균 응답 시간: 39ms
- Median: 36ms
- 95% Line: 59ms
- 최대값: ~1,037ms
- Throughput: ~24.4/sec
- 에러율: 0% (409 충돌은 정상 처리로 분류)


2. JVM Heap / GC & DB
- Heap 사용량 안정적 (150MB 내외 유지)
- GC Pause: < 0.01s → 메모리 병목 없음
- HikariCP Active/Idle 커넥션: 정상 범위 유지 (최대 풀을 쓰더라도 Pending Threads 없음-> 대기열 병목 없음)
- DB Insert: schedules, processed_event, outbox_event_entity, notification 테이블 모두 13,884건 일치-> EOS 보장 확인
3. Kafka & Outbox
- Kafka Consumer 처리량: 초반 20~25 msg/s, 이후 안정화 → 컨슈머 병목 없음
- Outbox Publish Rate: ~0.3 msg/s 안정적으로 유지 (스케줄러 기반 publish)
- Outbox Avg Latency: 초반 스파이크 있었으나 0.05~0.1s 수준으로 안정화
4.HTTP
- HTTP Avg Latency: ~38~39ms (JMeter 결과와 일관)
- HTTP Error Rate(5xx): 없음 → 에러율 0%
3만건 일괄 요청 중 DB에 들어간 것은 13,884건이고 나머지는 비즈니스 로직으로 (409충돌) 정상 거절을 했으며 디비에 저장된 내역으로 보아서 중복은 없었고, 유실은 없었으며 구현하고자 한 EOS가 보장이 되었다는것을 알 수 있습니다. Grafana에서 JVM,Kafka,DB 모두 병목 없이 안정적으로 처리가 되므로 안정성 검증이 완료가 되었습니다.
5-2. 5만건 일괄 테스트


1.JMeter 결과 (5만건 일괄)
- 평균 응답 시간: 39ms (엄청 안정적)
- Median: 38ms
- 95% Line: 59ms
- 최대값: 2044ms (일부 스파이크 있지만 전체 영향 없음)
- Throughput: ~23.9/sec
- 에러율: 0% (409 충돌은 정상 처리로 제외)


2. JVM & DB
- Heap: 100~150MB 선에서 안정 → GC도 문제 없음
- HikariCP: Active/Idle 적절히 움직였고, Pending Threads 0 → DB 커넥션 병목 없음
- Avg Latency: 0.02~0.05s → 안정적
3.Kafka & Outbox
- Consumer 처리량: 초반에 20~25 msg/s 치솟다가 점점 하향 안정 → 컨슈머 지연 없음
- Outbox Publish Rate: 0.3 msg/s 근처에서 꾸준 → 안정적인 발행
- Outbox Latency: 초반 스파이크 후 <0.1s 안정화
- DLQ Retry: 주기적으로 찍히지만 대부분 빠르게 정상화됨
4.Http
- HTTP Avg Latency: ~20~50ms (JMeter 결과와 일관)
- HTTP Error Rate(5xx): 없음 → 에러율 0%
5만건 요청에도 유실 0건, 중복 0건 최종적으로 디비에 1.7만건을 정상 저장을 했고 응답 시간 평균은 39ms,에러율은 0%로 Kafka EOS 멱등 처리(evnetId 기반) 보장이 검증이 된것을 알 수가 있습니다.
6.후기
‘구현’에서 ‘운영’으로의 시야 확장
과거에는 코드를 작성하고 테스트가 통과하면 끝이라고 생각했습니다. 단순히 기능이 동작하는 것만으로는 충분하지 않다는 사실을 깨달았습니다. DB 커넥션 풀 지표, JVM Heap, Kafka Consumer 처리량까지 전부 확인해야만 EOS 보장이 실제로 의미가 있다는 걸 알게 되었죠. 이 경험을 통해 코드 한 줄보다 운영 환경에서의 검증과 관찰이 훨씬 더 중요하다는 점을 배우게 되었습니다.
수치로 증명하는 힘
“유실 0건, 중복 0건, 평균 응답속도 39ms, 에러율 0%”라는 지표를 얻으면서, 막연한 주장이 아니라 데이터로 신뢰를 주는 방법을 몸소 체험했습니다. 앞으로도 기능 구현 후에는 반드시 수치로 검증하는 습관을 유지하려 합니다.
개발자 태도의 전환
이번 과정은 단순한 기술 학습이 아니라, 저 스스로에게 “개발은 결국 문제 해결의 연속이고, 장애와 복원력을 고려하지 않는다면 반쪽짜리다”라는 기준을 심어주었습니다. 예전엔 ‘돌아가기만 하면 된다’는 태도였다면, 이제는 ‘운영 중에 문제가 생겨도 버틸 수 있어야 한다’는 관점으로 성장하게 되었습니다.
'포폴 > 일정관리 프로젝트' 카테고리의 다른 글
| 일정관리 프로젝트 (0) | 2025.10.26 |
|---|---|
| Exception 처리 (Checked vs Unchecked) (0) | 2025.09.06 |
| Kafka Exactly-Once 보장하기 Outbox + EventId 기반 멱등 처리 (0) | 2025.09.01 |
| 일정추천 기능 고도화- OpenFeign에서 WebClient 전환기 (0) | 2025.07.11 |
| 프로젝트 배포3- CI부분에 캐싱 적용 (1) | 2025.07.06 |
