You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- 사용자의 쿠폰 발급 요청이 애플리케이션 서버에 도달하는 즉시, 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
+
이처럼 쿠폰 발급 시스템의 전체 흐름에서 분산락은 가장 먼저 동시성 문제를 해결하고 데이터의 일관성을 유지하기 위한 핵심적인 방어선 역할을 수행합니다.
115
92
그렇기 때문에 이 분산락 메커니즘을 어떻게 구현하고 관리하느냐가 시스템 전체의 안정성에 큰 영향을 미치게 됩니다.
116
93
117
94
이제, 이 중요한 분산락 기능을 기존 `SETNX` 방식에서 어떻게 개선해 나갔는지 자세히 살펴보겠습니다.
118
95
119
-
120
96
---
121
97
122
98
## 기존 SETNX 직접 사용 방식의 고려사항
@@ -125,16 +101,18 @@ support/
125
101
126
102
하지만 이 방식은 다음과 같은 점들을 추가적으로 고려해야 했습니다.
127
103
128
-
1. 비즈니스 로직과 락 로직의 혼재:
129
-
쿠폰 발급과 같은 핵심 비즈니스 로직 내에 락을 획득하고 해제하는 코드가 직접 포함되어, 코드의 가독성과 유지보수성을 저해할 수 있었습니다. 핵심 로직에 집중하기 어려워지는 것이죠.
104
+
1. 비즈니스 로직과 락 로직의 혼재
105
+
쿠폰 발급과 같은 핵심 비즈니스 로직 내에 락을 획득하고 해제하는 코드가 직접 포함되어, 코드의 가독성과 유지보수성을 저해할 수 있었습니다. 핵심 로직에 집중하기 어려워지게 됩니다.
130
106
131
-
2. 반복적인 상용구 코드 (Boilerplate Code):
107
+
2. 반복적인 상용구 코드
132
108
모든 락 사용 지점마다 `try-catch-finally` 구문을 사용하여 락 해제를 보장해야 했고, 락 키를 생성하는 로직 또한 중복될 수 있었습니다.
133
109
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`만으로는 구현하기 복잡합니다.
136
114
137
-
이러한 불편함과 고려사항들을 개선하고, 보다 안정적이며 재사용 가능한 락킹 메커니즘을 구축하고자 Redisson 도입을 결정했습니다. 특히 Spring AOP를 활용하여 락킹 로직을 공통화하고 비즈니스 로직과 분리하는 것에 큰 매력을 느꼈습니다.
115
+
이러한 불편함과 고려사항들을 개선하고, 보다 안정적이며 재사용 가능한 락킹 메커니즘을 구축하고자 Redisson 도입을 결정했습니다. 특히 Spring AOP를 활용하여 락킹 로직을 공통화하고 비즈니스 로직과 분리하는 것에 큰 장점을 느꼈습니다.
0 commit comments