코드 저장소.

Kafka Exactly-Once, 실제로 버티나? 50VU 실패부터 에러율 0% 달성까지 본문

포폴/일정관리 프로젝트

Kafka Exactly-Once, 실제로 버티나? 50VU 실패부터 에러율 0% 달성까지

slown 2026. 4. 9. 00:40

목차

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의 정상 동작 및 응답 시간을 검증한다

시나리오

  1. JMeter로 일정 생성 API를 10회 반복 호출
  2. Grafana(Prometheus 연동)로 JVM Heap, GC, DB Connection Pool 등 내부 지표를 모니터링
  3. API 응답 시간, 에러율, 처리 결과 확인

성공 기준

  • 모든 요청에서 에러율 0%
  • 평균 응답 속도: 50ms~100ms 내외 유지
  • DB Connection Pool, GC, Heap 사용량에서 이상 징후 없음
  • Kafka Outbox → Consumer → DB 저장까지 전파가 정상적으로 완료됨

부하 테스트 

  • 동시 사용자 요청 증가(50명 → 100명) 상황에서 API → Kafka → Consumer → DB 저장까지의 전체 흐름에 병목 구간이 없는지 검증한다

시나리오

  1. JMeter
    • Virtual User 50명, 100명 시나리오로 테스트 진행
    • Ramp-up: 30초, Duration: 180초
    • 응답 시간(P95, P99), 에러율, TPS(Throughput) 측정
  2. Grafana (Prometheus + Kafka Exporter)
    • Consumer 처리량(Records/sec), Lag, 처리 지연시간 확인
    • JVM Heap/GC, DB Connection Pool 지표 수집
  3. 결과 분석
    • 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)하는지 검증한다

시나리오

  1. JMeter로 동일 이벤트(1만, 3만, 5만 건)를 일괄 publish
  2. Kafka Consumer가 이벤트를 처리하여 DB에 저장
  3. DB 레벨에서 eventId를 unique key로 사용, 중복 발생 시 409를 정상 처리로 간주
  4. 처리 과정에서 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회 반복을 해서 일정을 생성을 하는 방식입니다.

정상 실행시의 Jmeter 설정

 

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%”라는 지표를 얻으면서, 막연한 주장이 아니라 데이터로 신뢰를 주는 방법을 몸소 체험했습니다. 앞으로도 기능 구현 후에는 반드시 수치로 검증하는 습관을 유지하려 합니다.
개발자 태도의 전환
이번 과정은 단순한 기술 학습이 아니라, 저 스스로에게 “개발은 결국 문제 해결의 연속이고, 장애와 복원력을 고려하지 않는다면 반쪽짜리다”라는 기준을 심어주었습니다. 예전엔 ‘돌아가기만 하면 된다’는 태도였다면, 이제는 ‘운영 중에 문제가 생겨도 버틸 수 있어야 한다’는 관점으로 성장하게 되었습니다.