Skip to content

Commit 53355b8

Browse files
committed
Update SpringBoot Post " 쿠폰 시스템 개선기 "
1 parent 295b800 commit 53355b8

File tree

5 files changed

+25
-47
lines changed

5 files changed

+25
-47
lines changed

_posts/2025-06-03-SpringBoot-Coupon-System-Redisson.md

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -70,53 +70,29 @@ support/
7070

7171
## 쿠폰 발급 전체 시스템 흐름과 분산락의 역할
7272

73-
본격적으로 분산락 개선 과정에 대해 이야기하기에 앞서, 현재 제가 개발 중인 쿠폰 발급 시스템의 전체적인 처리 흐름을 간략히 소개해 드리겠습니다.
73+
본격적으로 분산락 개선 과정에 대해 이야기하기에 앞서, 이 분산락이 시스템의 어느 지점에서 왜 중요한 역할을 하는지 간략한 흐름을 통해 이해해 보겠습니다.
7474

75-
이 흐름을 통해 분산락이 어느 단계에서 왜 중요한 역할을 하는지 이해하는 데 도움이 될 것입니다.
76-
77-
![](/assets/img/coupon/coupon-issuance-processing-flow.png)
75+
![](/assets/img/coupon/coupon-success-flow.png)
7876

7977
사용자의 쿠폰 발급 요청은 다음과 같은 주요 단계를 거쳐 처리됩니다.
80-
1. **사용자 단위 분산락 획득** (본 포스팅의 핵심 주제)
81-
- 사용자의 쿠폰 발급 요청이 애플리케이션 서버에 도달하는 즉시, Redis 기반의 Redisson 분산락을 사용자 단위로 적용하여 `race condition`을 선제적으로 차단합니다.
82-
- **초기에는 SETNX 명령어를 직접 사용했지만, 현재는 Redisson의 RLock을 Spring AOP로 적용**하여 이 과정을 더욱 안정적으로 처리하고 있습니다.
83-
- 락 키는 `LOCK:coupon:{couponId}:user:{userId}` 형식으로 구성되며, AOP 기반 커스텀 어노테이션(`@DistributedLock`)을 통해 `issue()` 메서드 진입 전 자동으로 획득됩니다.
84-
- 해당 락은 지정된 대기 시간(`waitTime`)과 점유 시간(`leaseTime`)을 가지며, 획득 실패 시 즉시 예외를 반환하여 처리를 중단합니다. 이 락을 통해 동일 사용자의 중복 요청을 직렬화 처리하고, 다중 서버 환경에서도 쿠폰 발급 수량의 정합성을 보장합니다.
85-
86-
2. 중복 발급 방지 및 수량 제어 (Redis 활용):
87-
- 락 획득 후에는 Redis를 활용하여 두 가지 중요한 검증을 수행합니다.
88-
- 먼저, Redis의 `SADD` 연산을 사용하여 `userId`가 해당 쿠폰 발급 요청에 포함된 이력이 있는지를 체크합니다.
89-
- 최초 요청 시에만 `true`가 반환되며, 이후 요청은 중복으로 간주되어 즉시 차단됩니다. 중복이 아닌 경우에만 Redis의 `INCR` 연산으로 쿠폰 발급 수량을 1 증가시킵니다.
90-
- 이때 증가된 수량이 총 발급 가능 수량을 초과하면 `DECR`로 롤백되며, 발급 실패 응답을 반환합니다.
91-
- 이 단계에서 이미 중복 요청 또는 수량 초과가 선제적으로 차단되므로, 불필요한 데이터베이스 접근 없이 효율적인 자원 사용이 가능합니다.
92-
93-
3. 예외 응답 처리:
94-
- 락 획득 실패, 중복 발급, 수량 초과 등의 경우에는 적절한 예외 메시지와 함께 해당 요청의 처리를 중단합니다.
95-
- 특히, 수량 초과로 인해 `INCR``DECR`이 발생한 경우에는 Redis `Set(SADD)` 데이터도 롤백하여 시스템 상태 불일치를 방지합니다.
96-
97-
4. Kafka 기반 비동기 처리:
98-
- Redis 검증을 모두 통과한 유효한 요청만 Apache Kafka로 전송됩니다.
99-
- Kafka Producer는 `coupon.issue` 토픽으로 메시지를 발행하며, 이는 API 서버의 부하를 최소화하고 쿠폰 발급 흐름을 비동기적으로 유지하는 핵심적인 역할을 합니다.
100-
- API 서버는 Kafka로 메시지를 전송한 후 사용자에게 빠르게 응답하며, 실제 데이터베이스 저장과 같은 후속 처리는 Kafka Consumer에서 담당합니다. 이 `Kafka 전송 → Consumer → DB 저장` 구조를 통해 API 서버의 부하를 줄이고 전체적인 처리 속도를 개선하였습니다. 또한, MySQL RDBMS에는 `userId``couponId` 조합에 대해 복합 유니크 인덱스를 설정하여, Redis 레벨에서의 사전 체크를 우회한 비정상 요청에 대해서도 데이터베이스 차원에서 최종적으로 데이터 무결성을 보장합니다. (현재는 Kafka와 DB를 별도 트랜잭션으로 처리하지만, 추후 `KafkaTransactionManager``ChainedTransactionManager`를 적용하여 하나의 트랜잭션 경계로 묶는 구조로 확장할 계획입니다.)
101-
102-
5. DB 저장 실패 및 실패 이력 관리:
103-
- Kafka Consumer는 발급 메시지를 수신하여 관계형 데이터베이스에 발급 정보를 저장합니다.
104-
- 만약 Consumer에서의 DB 저장이 실패할 경우, 해당 발급 이력을 `FailedIssuedCoupon` 테이블에 저장하여 재처리를 위한 상태를 기록합니다.
105-
106-
6. 미해결 실패 건 재처리:
107-
- Spring의 `@Scheduled` 기반 스케줄러가 `FailedIssuedCoupon` 테이블의 미해결 실패 건을 주기적으로 조회합니다.
108-
- 재처리 대상 건은 다시 Kafka로 전송되어 쿠폰 발급 시도를 반복합니다. 발급 성공 시 `isResolved = true`로 업데이트되며, 재시도 실패 시 `retryCount`를 증가시켜 재시도 횟수를 누적합니다.
109-
110-
7. 최종 저장 및 DLQ 이관:
111-
- 재처리 결과, `retryCount`가 3회를 초과한 실패 건은 Dead Letter Queue(DLQ)로 이관될 예정입니다.
112-
- DLQ로 이관된 대상은 운영자의 수동 개입 또는 알림 기반의 후속 조치를 통해 관리됩니다.
113-
114-
이처럼 쿠폰 발급 시스템의 전체 흐름에서 분산락은 가장 먼저 동시성 문제를 해결하고 데이터의 일관성을 유지하기 위한 핵심적인 방어선 역할을 수행합니다.
78+
1. 선 검증 및 요청 접수 (API 서버)
79+
- 사용자의 요청이 들어오면, 가장 먼저 **분산락을 획득**하여 동시성을 제어합니다.
80+
- 이후 Redis의 Atomic 연산을 통해 중복 발급을 방지하고, INCR/DECR 명령으로 쿠폰 수량을 제어하는 빠른 검증을 수행합니다.
81+
82+
2. 비동기 전달 (Kafka)
83+
- 검증을 통과한 요청은 즉시 Kafka 토픽으로 발행됩니다.
84+
- 덕분에 API 서버는 DB 작업의 지연 시간 없이 사용자에게 빠르게 응답할 수 있습니다.
85+
86+
3. 최종 처리 (Consumer 서버)
87+
- 별도의 Consumer 서버가 Kafka 메시지를 받아 최종적으로 데이터베이스에 쿠폰 발급 내역을 저장합니다.
88+
89+
(이러한 아키텍처를 채택한 이유와 전체 시스템의 더 상세한 처리 흐름(실패 및 재처리 포함)은 깃허브에서 확인하실 수 있습니다.)
90+
91+
이처럼 쿠폰 발급 시스템의 전체 흐름에서 분산락은 가장 먼저 동시성 문제를 해결하고 데이터의 일관성을 유지하기 위한 핵심적인 방어선 역할을 수행합니다.
11592
그렇기 때문에 이 분산락 메커니즘을 어떻게 구현하고 관리하느냐가 시스템 전체의 안정성에 큰 영향을 미치게 됩니다.
11693

11794
이제, 이 중요한 분산락 기능을 기존 `SETNX` 방식에서 어떻게 개선해 나갔는지 자세히 살펴보겠습니다.
11895

119-
12096
---
12197

12298
## 기존 SETNX 직접 사용 방식의 고려사항
@@ -125,16 +101,18 @@ support/
125101

126102
하지만 이 방식은 다음과 같은 점들을 추가적으로 고려해야 했습니다.
127103

128-
1. 비즈니스 로직과 락 로직의 혼재:
129-
쿠폰 발급과 같은 핵심 비즈니스 로직 내에 락을 획득하고 해제하는 코드가 직접 포함되어, 코드의 가독성과 유지보수성을 저해할 수 있었습니다. 핵심 로직에 집중하기 어려워지는 것이죠.
104+
1. 비즈니스 로직과 락 로직의 혼재
105+
쿠폰 발급과 같은 핵심 비즈니스 로직 내에 락을 획득하고 해제하는 코드가 직접 포함되어, 코드의 가독성과 유지보수성을 저해할 수 있었습니다. 핵심 로직에 집중하기 어려워지게 됩니다.
130106

131-
2. 반복적인 상용구 코드 (Boilerplate Code):
107+
2. 반복적인 상용구 코드
132108
모든 락 사용 지점마다 `try-catch-finally` 구문을 사용하여 락 해제를 보장해야 했고, 락 키를 생성하는 로직 또한 중복될 수 있었습니다.
133109

134-
3. 락 임대 시간(Lease Time) 및 유실 문제:
135-
`SETNX`로 락을 설정할 때 `EXPIRE` 명령으로 TTL(Time To Live)을 설정하여 락 유실(Lock Leak)을 방지해야 합니다. 하지만 이 TTL 관리가 수동적이어서, 만약 락을 획득한 클라이언트가 예상보다 오래 작업을 수행할 경우 락이 먼저 해제되어 다른 클라이언트가 락을 중복 획득할 위험이 있었습니다. 반대로, 클라이언트 장애 시 락이 너무 길게 남아있을 수도 있고요. Redisson이 제공하는 Watchdog과 같은 Lease Time 자동 연장 기능은 `SETNX`만으로는 구현하기 복잡합니다.
110+
3. 락 임대 시간(Lease Time) 및 유실 문제
111+
- `SETNX`로 락을 설정할 때 `EXPIRE` 명령으로 TTL(Time To Live)을 설정하여 락 유실(Lock Leak)을 방지해야 합니다.
112+
- 하지만 이 TTL 관리가 수동적이어서, 만약 락을 획득한 클라이언트가 예상보다 오래 작업을 수행할 경우 락이 먼저 해제되어 다른 클라이언트가 락을 중복 획득할 위험이 있었습니다.
113+
- 반대로, 클라이언트 장애 시 락이 너무 길게 남아있을 수도 있고요. Redisson이 제공하는 Watchdog과 같은 Lease Time 자동 연장 기능은 `SETNX`만으로는 구현하기 복잡합니다.
136114

137-
이러한 불편함과 고려사항들을 개선하고, 보다 안정적이며 재사용 가능한 락킹 메커니즘을 구축하고자 Redisson 도입을 결정했습니다. 특히 Spring AOP를 활용하여 락킹 로직을 공통화하고 비즈니스 로직과 분리하는 것에 큰 매력을 느꼈습니다.
115+
이러한 불편함과 고려사항들을 개선하고, 보다 안정적이며 재사용 가능한 락킹 메커니즘을 구축하고자 Redisson 도입을 결정했습니다. 특히 Spring AOP를 활용하여 락킹 로직을 공통화하고 비즈니스 로직과 분리하는 것에 큰 장점을 느꼈습니다.
138116

139117

140118
---
@@ -537,7 +515,7 @@ Kafka Consumer 서버에서는 API 서버에서 전송한 `CouponIssueMessage`
537515
![](/assets/img/coupon/coupon-issue-system-Grafana-Kafka-Consumer-Server-Before.png)
538516

539517

540-
### Redisson RLock 및 AOP 도입 후
518+
### After: Redisson RLock 및 AOP 도입 후
541519

542520
> K6
543521
79 KB
Loading
-157 KB
Binary file not shown.
55.8 KB
Loading
47.9 KB
Loading

0 commit comments

Comments
 (0)