목표: 대규모 트래픽을 처리할 수 있는 쿠폰 발급 시스템 설계 및 구현
제한된 서버 리소스 환경에서 최대한의 효율을 이끌어내기 위한 최적화 작업을 중점으로 진행했습니다.
- EC2: t3.medium (2vCPU, 4GB RAM)
- RDS: db.t4g.micro (2vCPU, 1GB RAM)
- 동시성 이슈 해결: 10,000명 동시 요청 환경에서 발생한 Race Condition(초과 발급)을 100% 제어
- 성능 최적화:
DB Lock→Redis→Kafka순차적 고도화를 통해 응답 속도 개선 및 시스템 안정성 확보
- 문제:
Java synchronized사용 시 다중 서버 환경에서 동시성 제어 불가 및 성능 저하 발생 - 해결: DB 비관적 락(Pessimistic Lock) 도입으로 데이터 정합성 보장
- 결과: 동시성 문제는 해결했으나, DB 커넥션 풀 고갈(Pending Threads 폭증)로 인한 병목 발생
- 문제: DB 커넥션 대기 시간 증가(최대 1초) 및 CPU 사용률 100% 도달
- 해결: Redis Lua Script를 활용해 원자적 연산 처리 및 DB 접근 최소화
- 결과: Latency 62% 감소. 단, 성공 건에 대한 동기식 DB 저장(Insert) 작업이 새로운 병목지점이 됨
- 문제: 동기식 DB 저장 구조로 인한 전체 처리량의 한계 확인
- 해결: Kafka 기반 Event-Driven 아키텍처로 전환하여 비동기 Batch Insert 처리
- 결과: DB 부하를 격리하고 소비(Consume) 속도를 Throttling하여 시스템 안정성 및 최종 처리량 향상
| Before | After | |
|---|---|---|
![]() |
→ | ![]() |
| Before | After | |
|---|---|---|
![]() |
→ | ![]() |
| Before | After | |
|---|---|---|
![]() |
→ | ![]() |
| Before | After | |
|---|---|---|
![]() |
→ | ![]() |
| 분류 | 사용한 기술 |
|---|---|
| Backend | Java 21, Kotlin, Spring Boot 3.4.1 |
| Database | MySQL 8.0.41, Redis, InfluxDB |
| Messaging & Streaming | Kafka 3.x, KRaft, Kafka-UI |
| Load Balancer | Nginx |
| Containerization | Docker |
| Build & Tool | Gradle |
| Testing | k6 |
| Monitoring | Prometheus, Grafana, Micrometer (JVM, HikariCP), Spring Boot Actuator |
| Exporters | mysql-exporter, redis-exporter, kafka-exporter |
.
├── HELP.md
├── README.md # 프로젝트 전체 설명
├── build.gradle.kts # 루트 프로젝트 빌드 설정 (멀티 모듈 공통 설정 등)
├── coupon-api # [Module] API 서버 (트래픽 접수, 검증, 발행 요청)
│ ├── Dockerfile # API 서버 컨테이너 이미지 빌드 명세
│ ├── build.gradle.kts # API 모듈 의존성 설정
│ └── src
│ ├── main
│ │ ├── kotlin
│ │ │ └── com
│ │ │ └── woong2e
│ │ │ └── couponsystem
│ │ │ ├── CouponsystemApiApplication.kt # API 서버 실행 진입점
│ │ │ ├── coupon
│ │ │ │ ├── api
│ │ │ │ │ ├── controller
│ │ │ │ │ │ └── CouponController.kt # 쿠폰 발급/조회 HTTP 요청 처리
│ │ │ │ │ └── request
│ │ │ │ │ ├── CouponCreateRequest.kt
│ │ │ │ │ ├── CouponIssueRequest.kt # 쿠폰 발급 요청 DTO
│ │ │ │ │ └── CouponStockInitRequest.kt
│ │ │ │ ├── application
│ │ │ │ │ ├── event
│ │ │ │ │ │ ├── CouponIssueDltEvent.kt # 실패 메시지(DLT) 처리 이벤트
│ │ │ │ │ │ └── CouponIssueEvent.kt # 쿠폰 발급 이벤트
│ │ │ │ │ ├── port
│ │ │ │ │ │ └── out
│ │ │ │ │ │ └── CouponIssueEventPublisher.kt # 이벤트 발행 인터페이스
│ │ │ │ │ ├── response
│ │ │ │ │ │ ├── CouponIssueResponse.kt
│ │ │ │ │ │ └── CouponResponse.kt
│ │ │ │ │ └── service
│ │ │ │ │ ├── CouponIssueService.kt # 쿠폰 발급 서비스 인터페이스
│ │ │ │ │ ├── CouponService.kt # 쿠폰 CRUD 기본 서비스
│ │ │ │ │ └── impl # 동시성 제어 전략별 구현체 모음
│ │ │ │ │ ├── AsyncLuaCouponIssueService.kt # 비동기 + Redis Lua Script 전략
│ │ │ │ │ ├── AtomicCouponIssueService.kt # Atomic 연산 활용 전략
│ │ │ │ │ ├── AtomicQueryCouponIssueService.kt
│ │ │ │ │ ├── DistributedLockCouponIssueService.kt # Redisson 분산락 전략
│ │ │ │ │ ├── LuaCouponIssueService.kt # Redis Lua Script 전략
│ │ │ │ │ ├── NoLockCouponIssueService.kt # 락 없는 상태 (Race Condition 발생)
│ │ │ │ │ ├── PessimisticLockCouponIssueService.kt # DB 비관적 락 전략
│ │ │ │ │ ├── ReentrantLockCouponIssueService.kt # Java ReentrantLock (단일 인스턴스)
│ │ │ │ │ ├── SemaphoreCouponIssueService.kt # Java Semaphore 활용 전략
│ │ │ │ │ ├── Synchronized2CouponIssueService.kt
│ │ │ │ │ └── SynchronizedCouponIssueService.kt # Java synchronized 키워드 전략
│ │ │ │ ├── domain
│ │ │ │ │ ├── entity
│ │ │ │ │ │ ├── Coupon.kt # 쿠폰 도메인 엔티티
│ │ │ │ │ │ └── IssuedCoupon.kt # 발급된 쿠폰 내역 엔티티
│ │ │ │ │ └── repository
│ │ │ │ │ ├── AppliedUserRepository.kt
│ │ │ │ │ ├── CouponRepository.kt
│ │ │ │ │ └── IssuedCouponRepository.kt
│ │ │ │ ├── infra
│ │ │ │ │ ├── persistence
│ │ │ │ │ │ ├── CouponJpaRepository.kt
│ │ │ │ │ │ └── IssuedCouponJpaRepository.kt
│ │ │ │ │ ├── producer
│ │ │ │ │ │ └── IssuedCouponProducer.kt # Kafka로 발급 요청 메시지 전송 (Producer)
│ │ │ │ │ └── redis
│ │ │ │ │ ├── AppliedUserRedisRepository.kt # 중복 발급 방지용 Redis Set 저장소
│ │ │ │ │ └── CouponRedisRepository.kt
│ │ │ │ ├── status
│ │ │ │ │ └── CouponErrorStatus.kt
│ │ │ │ └── value
│ │ │ │ └── DltSource.kt
│ │ │ ├── global
│ │ │ │ ├── annotaion
│ │ │ │ │ └── Bulkhead.kt # 벌크헤드 패턴 적용을 위한 어노테이션
│ │ │ │ ├── aop
│ │ │ │ │ └── BulkheadAspect.kt # 벌크헤드 패턴 AOP 구현 (장애 격리)
│ │ │ │ ├── exception
│ │ │ │ │ ├── CustomException.kt
│ │ │ │ │ └── GlobalExceptionHandler.kt # 전역 예외 처리기
│ │ │ │ ├── jpa
│ │ │ │ │ ├── JpaConfig.kt
│ │ │ │ │ └── PrimaryKeyEntity.kt # 공통 Base Entity
│ │ │ │ └── response
│ │ │ │ ├── ApiResponse.kt # 공통 응답 Wrapper
│ │ │ │ ├── code
│ │ │ │ │ ├── BaseCode.kt
│ │ │ │ │ └── BaseErrorStatus.kt
│ │ │ │ └── status
│ │ │ │ ├── ErrorStatus.kt
│ │ │ │ └── SuccessStatus.kt
│ │ │ ├── infra
│ │ │ │ ├── kafka
│ │ │ │ │ ├── KafkaProducerConfig.kt # Kafka Producer 설정 (직렬화 등)
│ │ │ │ │ └── KafkaTopicConfig.kt # 토픽 생성 및 설정
│ │ │ │ ├── lock
│ │ │ │ │ ├── DistributedLockExecutor.kt # 분산락 실행기 인터페이스
│ │ │ │ │ └── impl
│ │ │ │ │ ├── LettuceLockExecutor.kt # Lettuce 기반 락 (Spin Lock 직접 구현 시)
│ │ │ │ │ └── RedissonLockExecutor.kt # Redisson 기반 분산락
│ │ │ │ └── redis
│ │ │ │ ├── RedisConfig.kt
│ │ │ │ └── RedissonConfig.kt
│ │ │ └── user
│ │ │ └── domin
│ │ │ └── User.kt
│ │ └── resources
│ │ ├── application-local.yml # 로컬 개발 환경 설정
│ │ ├── application-prod.yml # 운영 환경 설정
│ │ ├── application.yml # 공통 설정
│ │ ├── sql
│ │ │ ├── data.sql # 초기 데이터 (더미 데이터 등)
│ │ │ └── schema.sql # 테이블 스키마 정의
│ │ ├── static
│ │ └── templates
│ └── test
│ ├── kotlin
│ │ └── com
│ │ └── woong2e
│ │ └── couponsystem
│ │ └── CouponsystemApiApplicationTests.kt # 통합 테스트 등
│ └── resources
│ └── application-test.yml
├── coupon-consumer # [Module] 컨슈머 서버 (비동기 처리, DB 적재)
│ ├── Dockerfile # 컨슈머 서버 컨테이너 이미지 빌드 명세
│ ├── build.gradle.kts # 컨슈머 모듈 의존성 (Kafka Consumer, JDBC 등)
│ ├── coupon-consumer.iml
│ └── src
│ ├── main
│ │ ├── kotlin
│ │ │ └── com
│ │ │ └── woong2e
│ │ │ └── couponsystem
│ │ │ ├── CouponsystemConsumerApplication.kt # 컨슈머 서버 실행 진입점
│ │ │ ├── coupon
│ │ │ │ ├── application
│ │ │ │ │ ├── port
│ │ │ │ │ │ └── out
│ │ │ │ │ │ └── CouponIssueDltPublisher.kt
│ │ │ │ │ └── service
│ │ │ │ │ └── CouponIssueWorkerService.kt # 실제 발급 로직 처리
│ │ │ │ ├── consumer
│ │ │ │ │ ├── event
│ │ │ │ │ │ ├── CouponIssueDltEvent.kt
│ │ │ │ │ │ └── CouponIssueEvent.kt
│ │ │ │ │ └── listener
│ │ │ │ │ └── CouponIssueConsumer.kt # Kafka 메시지 수신 (Listener)
│ │ │ │ ├── domain
│ │ │ │ │ ├── entity
│ │ │ │ │ │ ├── Coupon.kt
│ │ │ │ │ │ └── IssuedCoupon.kt
│ │ │ │ │ └── repository
│ │ │ │ │ ├── IssuedCouponBatchRepository.kt # Bulk Insert용 레포지토리
│ │ │ │ │ └── IssuedCouponRepository.kt
│ │ │ │ ├── infra
│ │ │ │ │ ├── kafka
│ │ │ │ │ │ └── KafkaCouponIssueDltPublisher.kt # 실패 메시지 재발행/DLT 전송
│ │ │ │ │ └── persistence
│ │ │ │ │ ├── IssuedCouponJdbcRepository.kt # JDBC Batch Update 구현
│ │ │ │ │ └── IssuedCouponJpaRepository.kt
│ │ │ │ ├── status
│ │ │ │ │ └── CouponErrorStatus.kt
│ │ │ │ └── value
│ │ │ │ └── DltSource.kt
│ │ │ ├── global
│ │ │ │ ├── exception
│ │ │ │ │ ├── CustomException.kt
│ │ │ │ │ └── GlobalExceptionHandler.kt
│ │ │ │ ├── jpa
│ │ │ │ │ ├── JpaConfig.kt
│ │ │ │ │ └── PrimaryKeyEntity.kt
│ │ │ │ └── response
│ │ │ │ ├── ApiResponse.kt
│ │ │ │ ├── code
│ │ │ │ │ ├── BaseCode.kt
│ │ │ │ │ └── BaseErrorStatus.kt
│ │ │ │ └── status
│ │ │ │ ├── ErrorStatus.kt
│ │ │ │ └── SuccessStatus.kt
│ │ │ ├── infra
│ │ │ │ └── kafka
│ │ │ │ └── KafkaConsumerConfig.kt # Kafka Consumer 설정 (Offset 전략, Batch Listener 등)
│ │ │ └── user
│ │ │ └── domin
│ │ │ └── User.kt
│ │ └── resources
│ │ ├── application-local.yml
│ │ ├── application-prod.yml
│ │ ├── application.yml
│ │ ├── static
│ │ └── templates
│ └── test
│ ├── kotlin
│ │ └── com
│ │ └── woong2e
│ │ └── couponsystem
│ │ └── CouponsystemConsumerApplicationTests.kt
│ └── resources
│ └── application-test.yml
├── deploy
│ └── deploy.sh # 배포 스크립트 (CI/CD 파이프라인 연동용)
├── docker-compose
│ ├── docker-compose-database.yml # DB (Redis, exporter 등) 컨테이너 구성
│ ├── docker-compose-kafka.yml # Kafka(Kraft) 컨테이너 구성
│ └── docker-compose.yml # 전체 서비스
├── nginx
│ └── nginx.conf # 로드밸런싱 또는 리버스 프록시 설정
└── settings.gradle.kts # 멀티 모듈 관리 설정












