| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 트랜잭션아웃박스패턴
- 버퍼풀
- 외부API연동
- Redis아키텍처
- 루스 인덱스 스캔
- 실행 사이클
- 멱등성
- 개발자성장기
- 인덱스 풀 스캔
- 1년차개발자
- 세컨더리 인덱스
- 잠금없는읽기
- 테스트컨테이너
- 프라이머리 키
- Snapshot Read
- Undo Log
- 인덱스 레인지 스캔
- 데이터 무결성
- 공유락
- 비식별자
- 외부API
- 언두로그
- 백그라운드스레드
- 레코드 락
- CPU
- 갭 락
- 인덱스 스킵 스캔
- InnoDB 격리수준
- 인덱스 구조
- 배타락
- Today
- Total
DS log
계약마다 다른 쿼터 정책을 어떻게 설계할까 본문
이번 글에서는 회사에서 개발하면서 겪었던, 저에게 꽤 의미 있던 경험을 정리해보려고 합니다. 저는 B2B 솔루션을 개발하는 팀에 속해 있습니다. 이번에 그 솔루션에 계약마다 다른 쿼터 정책을 적용해야 하는 기능을 개발하게 됐습니다.
기본적인 CRUD 개발이 아니라서 확실히 흥미롭긴 했습니다. 그런데, '계약에 관한 정책이 위에서 정해지지 않았는데, 내 마감기한은 그대로네 이대로라면 야근 확정이네 ~ ' 하는 자포자기의 심정도 들었습니다. 뭐 아무튼 기쁜(?) 마음으로 야근을 하며 기한 내에 기능 개발을 완료할 수 있었습니다. ^^
포스팅으로 돌아와서 기능 요구사항을 간단히 정리하면 아래와 같습니다.
기능마다 서로 다른 쿼터 정책이 필요하고, 그 정책은 계약에 따라 달라진다.
처음에는 "기능마다 if-else로 나누면 되지 않을까?"라고 생각했습니다. 그런데 조금만 더 생각해 보니 이 방식은 금방 한계가 보였습니다. 정책이 늘어날수록 서비스 메서드는 비대해지고, 동시 요청이 들어오기 시작하면 검증과 차감 로직이 여기저기 흩어질 가능성이 컸습니다.
또 비기능 요구사항도 있었습니다. 자원 사용량이 계약에 정해진 범위를 정확히 지켜야 한다는 점입니다. 결국 동시성 문제입니다.
이 글은 그 문제를 어떻게 바라봤고, 왜 정책 분리와 원자적 갱신을 중심으로 설계했는지에 대한 회고입니다.
문제는 정책이 계약마다 바뀐다는 것!
처음에는 쿼터 시스템을 단순히 "남은 횟수를 확인하고 차감하는 기능" 정도로 생각했습니다. 하지만 실제로 어려웠던 것은 차감 자체가 아니라, 정책의 기준이 고정돼 있지 않다는 점이었습니다.
예를 들면 이런 식입니다.
- 기능 A는 사용자 기준으로만 제한한다.
- 기능 B는 조직 기준으로만 제한한다.
- 기능 C는 사용자와 조직 조건을 동시에 만족해야 한다.
즉, "사용량을 추적한다"는 행위는 같지만, 어떻게 검증할지는 계약에 따라 달라집니다.
이 상황에서 가장 피하고 싶었던 것은 정책 분기문이 비즈니스 로직 전체로 퍼지는 구조였습니다. 새로운 계약 타입이 추가될 때마다 핵심 서비스 코드에 조건문을 덧붙이는 방식은, 처음에는 빠를 수 있어도 결국 유지보수 비용이 커질 수밖에 없습니다.
그래서 초반에 세운 기준은 하나였습니다.
바뀌는 것은 정책이고, 바뀌지 않는 것은 "사용량을 검증하고 기록한다"는 흐름이다.
이 기준을 잡고 나니 설계 방향도 자연스럽게 정리됐습니다.
정책을 동적으로 선택할 수 있게 하자.
제가 택한 방식은 정책별로 사용량 처리 전략을 분리하는 것이었습니다. 핵심 아이디어는 단순합니다. 또 전략패턴입니다.. ㅎㅎ
- 서비스는 "이번 요청에 어떤 정책을 적용해야 하는지"만 결정한다.
- 실제 검증과 차감 방식은 정책별 전략이 담당한다.
이렇게 하면 얻을 수 있는 장점은 분명합니다.
첫째, 정책 변경의 영향을 좁힐 수 있습니다. 새로운 계약 정책이 추가되더라도 기존 서비스 메서드를 계속 뜯어고칠 필요가 없습니다. 정책 하나가 늘면 그 정책에 맞는 처리 전략을 하나 추가하면 됩니다.
둘째, 코드가 문제의 구조를 더 잘 드러냅니다. 쿼터 시스템의 핵심은 "어떤 기준으로 제한할 것인가"인데, 이를 전략으로 분리해 두면 코드 수준에서도 정책이 독립된 개념으로 보입니다.
셋째, 테스트가 쉬워집니다. 정책별 검증 규칙을 각각 분리해서 검증할 수 있기 때문에, 복잡한 분기문 하나를 통째로 테스트하는 것보다 훨씬 명확합니다.
물론 전략 패턴이 만능은 아닙니다. 정책 조합이 아주 복잡해지면 resolver나 chain 같은 다른 구조가 필요할 수 있습니다. 하지만 적어도 이 단계에서는 "정책 선택"과 "정책 실행"을 분리하는 것만으로도 설계가 상당히 단순해졌습니다.
제일 중요한 부분은 동시성 문제 해결!
정책을 분리하고 나니 다음 문제가 남았습니다. "동시에 여러 요청이 들어오면 한도를 어떻게 정확하게 지킬 것인가?"
쿼터 시스템에서는 이 문제가 생각보다 빨리 드러납니다. 예를 들어 남은 횟수가 1회일 때 요청 두 개가 거의 동시에 들어오면, 둘 다 조회 시점에는 "아직 가능하다"고 판단할 수 있습니다. 그 상태에서 각각 차감까지 성공하면 한도를 초과하게 됩니다.
처음에는 사용 이력을 한 테이블에 계속 insert 하면서 집계로 처리하는 방식을 떠올렸습니다. 그러나 이 방식은 현재 인프라 구조상 아래와 같은 한계를 가집니다.
MySQL InnoDB의 격리 수준을 Repeatable Read로 설정했기 때문에 '조회하는 시점(스냅샷) '의 기능별 사용 횟수와 '현재' 기능별 사용 횟수가 다를 수 있습니다.
그래서, 락을 사용할 수 있는 구조로 테이블을 재설계했습니다. 그리고, 사용 가능한 락을 검토했습니다.
- 낙관적 락
- 비관적 락
- 분산 락
- 조건부 원자적 update
1. 낙관적 락
낙관적 락은 충돌이 드물다는 전제에서는 깔끔한 선택지입니다. 다만 충돌이 발생했을 때 재시도나 실패 처리가 필요합니다.
문제는 쿼터 차감이 사용자의 핵심 요청 흐름 안에 있다는 점이었습니다. 이 상황에서 "잠시 후 다시 시도해 주세요"를 자주 반환하는 구조는 사용자 경험 측면에서 부담이 컸습니다.
즉, 기술적으로는 가능하지만 이 문제에 가장 잘 맞는 방식은 아니라고 판단했습니다.
2. 비관적 락
비관적 락은 정합성을 지키기에는 확실한 방식입니다. 대신 락 경합이 늘어나면 처리량이 떨어질 수 있고, 트랜잭션 설계가 길어질수록 데드락 가능성도 고려해야 합니다.
가장 큰 고민은, 이 문제를 해결하기 위해 락 자체를 시스템의 중심으로 두고 싶지 않았다는 점이었습니다. 쿼터 차감은 자주 일어나는 일인데, 그때마다 긴 락 대기를 감수하는 방향은 부담스러웠습니다.
3. 분산 락
분산 락도 검토했습니다. 여러 인스턴스가 동시에 접근하는 환경에서 통제 지점을 외부로 빼는 방식은 분명 매력적입니다.
다만 이 방식은 새로운 인프라 의존성을 가져옵니다. 락 저장소, 만료 시간, 장애 시 복구 방식까지 함께 설계해야 합니다. 쿼터 차감 하나를 위해 시스템 복잡도를 그렇게까지 올릴 필요가 있는지는 의문이었습니다.
4. 조건부 원자적 update
마지막으로 본 것이 조건부 update였습니다. 핵심은 검증과 갱신을 하나의 SQL 문 안에서 끝내는 것입니다.
UPDATE quota_counter
SET used_count = used_count + 1
WHERE id = ?
AND used_count < quota_limit;
이 방식의 장점은 명확합니다.
- 한도 검증과 증가가 분리되지 않는다.
- DB가 보장하는 원자성 안에서 문제를 해결할 수 있다.
- 짧은 row-level lock만으로 처리된다.
- 애플리케이션에서 별도 락 프로토콜을 만들지 않아도 된다.
제가 이 방식을 좋게 본 이유는, 쿼터 시스템의 핵심 문제를 가장 직접적으로 풀기 때문입니다. 이 문제에서 정말 필요한 것은 "검증과 갱신을 절대 분리하지 않는 것"이지, 더 화려한 락 전략이 아니었습니다.
최종적으로 택한 방향
결론적으로 저는 아래 두 가지를 중심으로 설계했습니다.
- 정책은 전략으로 분리한다.
- 카운터 증가는 조건부 원자적 update로 처리한다.
정책의 복잡성과 동시성의 복잡성을 같은 레이어에서 한 번에 풀려고 하지 않고, 이 둘을 분리해서 생각을 하였습니다.
이 선택의 trade-off
이 구조의 장단점은 무엇일까 고민해봤습니다.
1. 카운터 모델이 단순한 대신 유연성은 제한됩니다
정책이 더 복잡해지면 단일 카운터 모델만으로는 표현이 어려워질 수 있습니다. 예를 들어 시간대별 제한, 우선순위 차감, 여러 조건의 조합 같은 요구가 생기면 정책 해석 계층을 더 세분화해야 합니다.
이때는 계약 타입별로 카운터 모델을 각각 다르게 설계할 예정입니다.
2. 특정 카운터로 트래픽이 몰리면 병목이 생길 수 있습니다
조건부 update는 단순하고 실용적이지만, 결국 같은 row에 요청이 집중되면 해당 지점이 병목이 될 수 있다고 생각합니다.
그래도, 현재의 상황에서 제가 할 수 있는 가장 나은 선택이지 않을까 싶습니다.
지금 다시 돌아봐도 남는 배움
이 경험을 통해 얻은 것을 아래와 같습니다.
첫째, 기술을 잘 선택하기 위해서는 기술에 대해서 잘 알고 있어야합니다.
둘째, 계약 이라는 도메인과 관련된 기능을 개발하며 도메인 경험이 중요하구나 라는 거를 알았습니다.
셋째, 설계에서는 "가장 유연한 구조"보다 "현재 문제를 가장 단순하게 푸는 구조"가 더 좋은 선택이구나를 다시 한 번 느꼈습니다.
솔직히, 요즘 AI 덕분에(?) 공부의 방향? 의지?를 잃고 있었는데 그래도 지금까지 공부한 게 헛된 게 아니었네 라는 생각이 드는 경험이었습니다. 조금만 더 힘내봐야지 하는 생각이 들었습니다.
감사합니다!
'트러블 슈팅' 카테고리의 다른 글
| 외부 API 연동 환경에서 안정적인 통계 집계 배치 개발하기 (0) | 2026.04.08 |
|---|---|
| 내가 테스트를 작성하며 고민한 것들 (0) | 2026.04.06 |
| 트랜잭션 아웃박스 패턴을 이용하여 데이터 정합성 확보하기 (회원탈퇴) (0) | 2026.04.05 |
| DDD의 Entity와 JPA의 Entity를 구분해야하는 것인가? (1) | 2024.08.15 |