Skip to main content

Command Palette

Search for a command to run...

약 27분 이상 걸리는 출고 지시 Api를 3초만에 동작하도록 개선한 경험 공유

사용자 경험을 높이기 위한 기능 최적화

Updated
2 min read
약 27분 이상 걸리는 출고 지시 Api를 3초만에 동작하도록 개선한 경험 공유

1. 배경

회사 내부 시스템에서 제공하는 주문 처리 API가 실사용자에게 너무 긴 응답시간을 유발하고 있었습니다.

특히 약 300건의 주문을 처리하는 데 10~15분 이상이 소요되며, 이는 클라이언트의 **타임아웃(1분)**에 의해 실제로는 에러로 반환되고 있는 상황입니다.

문제의 핵심은 다음과 같습니다:

  • 주문 처리 시간이 길어 클라이언트에서 먼저 커넥션이 끊어짐

  • 상태머신 프레임워크가 bulk 처리 불가하고, 주문 1건씩 루프 처리

  • 단일 API 호출이 파드의 CPU를 100%까지 소모하여 다른 요청도 영향을 받는 심각한 성능 문제가 발생


2. 가설

상태머신 기반 처리를 API 요청 흐름 내에서 동기적으로 처리하기 때문에 병목이 발생하기에 로직을 Kafka 메시지를 통한 비동기 처리로 전환하면 성능 문제를 해결할 수 있다.”


3. 데이터 분석

기존 구조 (AS-IS)

  • 처리 방식 : 상태머신 호출을 API 흐름 내부에서 순차적으로 처리

  • 처리 단위 : 주문 1건씩 순회 처리

  • 병목 원인 : 상태머신 자체 처리 속도 + 연속 동기 호출 → 전체 API 응답 지연

  • 리소스 사용 : 요청 처리 중 단일 POD의 CPU 사용률 100% 도달

  • 결과 : 1분 이내 응답이 불가능 → API 실패

4. 결과

출고 지시 Flow Chart와 개선안

개선 방식 (TO-BE)

  • 상태머신 트리거를 Kafka 메시지 발행 방식으로 전환

  • 주문 수만큼 Kafka에 메시지를 전송하고, Consumer가 개별 처리

  • 처리 흐름은 Lazy하게 진행되며, 처리 결과에 대해서 API는 빠르게 응답

성능 비교

성능 개선 수치를 확인하기 위해서 다음과 같이 테스트 및 확인을 했습니다.

DEV 환경

  • 주문 수 : 300건

  • Kafka

    • partition : 1

    • concurrency : 2

  • API elapsed time : 약 28분 → 약 4초

  • 실제 처리 시간 : 약 27분 → 약 10~15초

PROD 환경

  • 주문 수 : 300건

  • Kafka

    • partition : 4

    • concurrency : 2

  • API elapsed time : 약 12분 → 약 1초

  • 실제 처리 시간 : 약 10~12분 → 약 3초

개선 전 API JVM

개선 후 API JVM

개선 후 Consumer JVM

자원 사용 비교

  • 기존 API 서버는 요청 처리 중 JVM 자원 (CPU 및 메모리) 급증

  • 개선 후 API 서버는 가볍게 메시지만 전송, Kafka Consumer에서 분산 처리

  • API 서버 안정성 향상, POD 장애 가능성 제거

  • Consumer의 자원 사용량은 일부 증가하였지만 Kafka 기능 특성상 Lazy하게 처리하기 때문에 수용가능한 수준임을 확인

5. 느낀 점

이번 개선은 단순한 성능 향상이 아니라, 비즈니스 흐름을 시스템 구조 측면에서 재설계한 경험이었습니다.

  • 새로운 기능을 만드는 것보다, 기존 로직을 유지하면서 구조 개선하는 것이 훨씬 어렵고 에너지 소모가 크다

  • 구조적으로 개선하지 않으면 일시적인 성능 개선은 한계가 있다는 점을 체감

  • Kafka 기반 아키텍처 설계와 병렬 처리 구조에 대한 확신과 이해를 얻는 계기가 되었다

  • 출고지시 기능의 개선함으로써 사용자 경험에도 긍적적 영향을 미쳤을 것으로 생각됩니다.

30 views

More from this blog

카프카 입문 시리즈 2편: 토픽, 파티션, 오프셋

이 글은 Apache Kafka 입문 시리즈의 두 번째 글입니다. 1편에서 살펴본 구성 요소들 위에서, 메시지가 실제로 어떤 구조로 저장되고 관리되는지 알아보겠습니다. 1편을 마치며 세 가지 질문을 남겼습니다. 메시지는 브로커 안에서 어떤 구조로 저장될까? 토픽과 파티션은 정확히 무엇이고, 왜 필요할까? 컨슈머의 오프셋은 어떻게 동작할까? 이번 편에서 이 질문들에 하나씩 답하겠습니다. Topic: 메시지의 논리적 분류 토픽(Topic)은...

Mar 19, 202612 min read7

Java GC의 진화 — Serial에서 Generational ZGC까지

Java가 약속한 것 중 하나는 "메모리는 내가 관리할게"였다. C/C++ 개발자들이 malloc과 free로 메모리와 씨름하던 시절, Java는 Garbage Collector(GC)라는 자동 메모리 관리자를 들고 나왔다. 개발자는 객체를 만들기만 하면 되고, 치우는 건 GC가 알아서 한다. 하지만 "알아서"라는 말에는 대가가 있었다. GC가 동작하는 동안 애플리케이션이 멈추는 것이다. 이 멈춤을 Stop-The-World(STW) 일시 정지...

Mar 16, 20269 min read1

Spring의 3대 철학 — DI, AOP, PSA가 만드는 코드의 품격

Spring을 처음 배울 때, 나는 어노테이션 수집가였다. @Autowired를 붙이면 객체가 알아서 들어오고, @Transactional을 붙이면 트랜잭션이 알아서 관리되고, @Cacheable을 붙이면 캐시가 알아서 동작했다. "알아서"라는 말 뒤에 숨은 원리를 몰랐다. 그냥 마법이라고 생각했다. 그러다 문제가 생겼다. @Transactional을 붙였는데 롤백이 안 됐다. 같은 클래스 안에서 메서드를 호출했기 때문이었다. 원인을 찾는 데 ...

Mar 16, 202611 min read9

Spring Boot Docker 이미지, 한 줄 한 줄에 담긴 고민

처음 Spring Boot 애플리케이션을 Docker로 배포했을 때, Dockerfile은 딱 세 줄이었다. FROM openjdk:17 COPY build/libs/app.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] 동작은 했다. 하지만 이미지 크기는 700MB를 넘겼고, 코드 한 줄 고칠 때마다 전체 JAR를 다시 빌드해야 했다. 프로덕션에 올릴 때는 root 권한으로 실행되고 있었다. "동작...

Mar 16, 202610 min read4

끄적끄적 테크 블로그

32 posts

물류 회사에 다니고 있는 개발자 블로그입니다. 개발을 너무 좋아해서 정신없이 작업하다가 중간에 끄적거리며 내용들을 몇개 적어봅니다 ㅎㅎ