728x90
포인트 시스템 구현 시 야기될 수 있는 주요 문제점
1. 동시성 문제 (Concurrency Issue)
- 상황: 여러 사용자의 활동 또는 한 사용자의 여러 활동이 거의 동시에 발생하여 같은 사용자의 포인트를 변경하려고 할 때 발생합니다.
- 문제: 데이터베이스의 '읽기-수정-쓰기' 과정에서 경쟁 상태(Race Condition)가 발생하여 포인트가 누락되거나 잘못 계산될 수 있습니다. (예: 현재 포인트가 100점일 때, 10점을 주는 두 개의 요청이 동시에 들어와 둘 다 110점으로 업데이트하여 최종 결과가 120점이 아닌 110점이 되는 경우)
2. 데이터 정합성 문제 (Data Consistency Issue)
- 상황: 포인트 적립/사용 로직이 여러 단계로 이루어질 때, 중간에 시스템 장애가 발생할 경우에 발생합니다.
- 문제: 예를 들어 '상품 구매'라는 행동에 대해 '주문 완료' 상태는 처리되었지만, '포인트 적립'은 실패하는 경우 데이터가 불일치하게 됩니다. 모든 작업이 하나의 원자적 단위(Atomic Unit)로 처리되지 않으면 시스템의 신뢰도가 떨어집니다.
3. 성능 저하 문제 (Performance Degradation)
- 상황: 포인트 적립/사용 요청이 발생할 때마다 동기적으로 데이터베이스에 접근하여 사용자 테이블의 포인트 필드를 직접 업데이트할 경우 발생합니다.
- 문제: 특히 사용자가 많은 서비스에서 '좋아요', '댓글 작성' 등 빈번한 행동에 포인트를 지급하면, 해당 로직이 주 애플리케이션의 성능에 병목이 될 수 있습니다. 사용자는 자신의 요청(댓글 작성 등)이 느리게 처리된다고 느낄 수 있습니다.
4. 포인트 변경 이력 관리 (Point History/Audit Trail)
- 상황: 단순히 사용자의 최종 포인트만 관리하고, 어떤 이유로 포인트가 변경되었는지 기록하지 않을 경우에 발생합니다.
- 문제: 고객 서비스(CS) 문의가 왔을 때 "왜 제 포인트가 150점이죠?"라는 질문에 답할 수 없습니다. 또한 어뷰징(Abusing)을 추적하거나 시스템 오류를 복구하기가 매우 어려워집니다.
5. 어뷰징 및 정책 관리 (Abuse & Policy Management)
- 상황: 사용자가 시스템의 허점을 이용해 비정상적으로 포인트를 획득하려는 시도(매크로, 반복 작업 등)가 있을 때 발생합니다.
- 문제: 특정 행동에 대한 포인트 지급 횟수를 제한(예: 하루에 댓글 작성으로 얻는 포인트는 최대 50점)하는 등의 정책이 없다면, 악의적인 사용자가 시스템의 재화를 무한정 탈취할 수 있습니다.
해결을 위한 기술 및 아키텍처 패턴
이러한 문제들을 해결하기 위해 다음과 같은 기술과 패턴을 적용할 수 있습니다.
1. 동시성 제어
- 낙관적 락 (Optimistic Lock): JPA의
@Version어노테이션을 사용하여 데이터 수정 시 버전 번호를 확인합니다. 동시 수정이 감지되면 예외를 발생시켜 재시도 로직을 구현할 수 있습니다. 경합이 적을 때 효율적입니다. - 비관적 락 (Pessimistic Lock): 데이터베이스의
SELECT ... FOR UPDATE구문을 사용하여 특정 레코드에 직접 락을 거는 방식입니다. 정합성이 매우 중요하고 경합이 잦을 것으로 예상될 때 사용하지만, 시스템 전반의 성능 저하를 유발할 수 있습니다. - Redis 활용: Redis의
INCR,DECR같은 원자적(Atomic) 연산을 활용하여 포인트를 관리하면 동시성 문제를 쉽게 해결할 수 있습니다.
2. 비동기 처리와 메시지 큐 (Asynchronous Processing & Message Queue)
- 개념: 포인트 지급 요청이 발생하면, 즉시 처리하지 않고 관련 데이터(사용자 ID, 지급 포인트, 사유 등)를 메시지 큐에 '이벤트' 형태로 발행합니다. 별도의 워커(Consumer)가 큐에서 이벤트를 가져와 안전하게 포인트를 처리합니다.
- 장점:
- 성능 향상: 사용자의 원래 요청(댓글 작성 등)은 큐에 메시지를 보내는 작업만으로 끝나므로 매우 빠릅니다.
- 안정성: 포인트 처리 시스템에 장애가 발생해도, 메시지 큐에 이벤트가 남아있으므로 복구 후 재처리가 가능하여 데이터 유실을 막습니다.
- 주요 기술: RabbitMQ, Apache Kafka, AWS SQS 등
3. 트랜잭션 관리 및 이력 테이블 설계
- 데이터베이스 트랜잭션 (
@Transactional): 포인트 처리 로직(예: 사용자 포인트 업데이트 + 포인트 이력 기록)을 하나의 트랜잭션으로 묶어 데이터 정합성을 보장합니다. - 포인트 이력 테이블 설계: 사용자의 최종 포인트만 저장하는 것이 아니라, 모든 포인트 변동 내역을 별도의 테이블에 기록합니다.
-- 예시: point_history 테이블 구조
CREATE TABLE point_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount INT NOT NULL, -- 변경된 포인트 (적립: +, 사용: -)
reason VARCHAR(255) NOT NULL, -- 변경 사유 (예: 'COMMENT_REWARD', 'PRODUCT_PURCHASE')
created_at DATETIME NOT NULL, -- 변경 시각
related_event_id BIGINT -- 관련된 이벤트 ID (예: 댓글 ID)
);
4. 어뷰징 방지 기술
- API Rate Limiting: 특정 시간 동안 동일 IP 또는 동일 사용자의 요청 횟수를 제한합니다. Spring Cloud Gateway, Resilience4j 등의 라이브러리를 활용할 수 있습니다.
- 정책 기반 로직 구현: 포인트 지급 로직 내에서 "오늘 이 사용자가 댓글 작성으로 포인트를 받은 횟수가 몇 번인가?"를 이력 테이블을 통해 확인하고, 정책을 초과하면 지급하지 않도록 구현합니다.
접근 방식
- 초기 단계: 우선 데이터베이스 트랜잭션과 포인트 이력 테이블을 설계하여 가장 기본적인 정합성과 추적 가능성을 확보합니다. 동시성 문제는 낙관적 락으로 시작하는 것이 일반적입니다.
- 확장 단계: 사용자가 늘고 포인트 지급 이벤트가 많아져 성능에 영향이 보이기 시작하면, 메시지 큐를 도입하여 비동기 처리 방식으로 전환하는 것을 적극적으로 고려해야 합니다. 이것이 가장 확장성 있고 안정적인 구조입니다.
기술 분석 및 아키텍처 선정: Kafka vs RabbitMQ
앞서 정의한 문제들의 가장 확실한 해결책은 비동기 처리를 위한 메시지 큐(Message Queue)의 도입입니다. 현재 저희 서비스는 마이크로서비스 간의 데이터 전파를 위해 아파치 카프카(Apache Kafka)를 표준 이벤트 버스로 사용하고 있습니다. 따라서 가장 먼저 고려할 수 있는 선택지는 기존의 Kafka 인프라를 그대로 활용하는 것입니다.
하지만 포인트 시스템은 '돈'과 유사한 민감 데이터를 다루므로, 이것이 과연 최선인지, 혹은 포인트 처리라는 특정 목적에 더 적합한 기술이 있는지 심층적으로 검토할 필요가 있습니다.
1. 메시징 기술 심층 비교: 이벤트 허브 vs 메시지 브로커
Kafka와 RabbitMQ는 둘 다 메시지를 전달하는 시스템이지만, 그 철학과 내부 동작 방식에는 근본적인 차이가 있습니다.
| 구분 | Apache Kafka (이벤트 스트리밍 플랫폼) | RabbitMQ (전통적 메시지 브로커) |
|---|---|---|
| 철학 및 모델 | 분산 커밋 로그 (Commit Log) 데이터를 '발생한 사건의 영구적 기록'으로 취급합니다. |
메시지 브로커 (Broker) 메시지를 '처리해야 할 일시적 작업(편지)'으로 취급합니다. |
| 패러다임 | Dumb Broker / Smart Consumer 브로커는 데이터를 순서대로 저장소에 쌓기만 할 뿐, 소비자가 자신의 읽을 위치(Offset)를 직접 관리하며 데이터를 당겨옵니다(Pull). |
Smart Broker / Dumb Consumer 브로커가 라우팅 규칙에 따라 소비자에게 메시지를 능동적으로 밀어줍니다(Push). 소비자는 전달받은 메시지를 처리하는 데 집중합니다. |
| 데이터 소비 | 비파괴적 읽기 (Non-destructive Read) 소비자가 메시지를 읽어도 데이터는 삭제되지 않습니다. 여러 다른 소비자 그룹이 동일한 데이터를 각자의 목적에 맞게 여러 번 읽을 수 있습니다. |
파괴적 읽기 (Destructive Read) 소비자가 메시지를 성공적으로 처리하고 확인(Ack) 신호를 보내면, 메시지는 큐에서 삭제됩니다. 하나의 메시지는 하나의 소비자에게만 전달되어 처리되는 것이 기본 원칙입니다. |
| 주요 기능 | • 높은 처리량 (Throughput) • 데이터 리플레이 (Replay) • 스트림 처리 (Kafka Streams) |
• 유연하고 복잡한 라우팅 • 메시지 처리 보장 (Ack/Nack) • Dead-Lettering (처리 실패 메시지 관리) • 메시지 우선순위 큐 |
| 적합한 용도 | 이벤트(Event) 전파 "주문이 발생했다"는 하나의 '사실'을 여러 시스템(재고, 배송, 분석)에 동시에 알릴 때 매우 강력합니다. |
명령(Command) 처리 "결제를 처리하라"는 '작업 지시'를 특정 시스템에 안정적으로 전달하여 정확히 한 번 처리해야 할 때 매우 적합합니다. |
2. 최종 결정: 포인트 시스템에 RabbitMQ를 도입하는 이유
위 비교 분석을 통해, 저희는 '포인트 지급'이라는 작업의 본질이 Kafka가 지향하는 '이벤트 스트리밍'보다는 RabbitMQ가 지향하는 '명령(Command) 처리' 에 훨씬 가깝다고 결론 내렸습니다.
- '이벤트'가 아닌 '명령'의 처리: "UserID: 123에게 5포인트를 지급하라"는 요청은 여러 시스템이 각자의 해석에 따라 구독해야 할 '사실'이 아닙니다. 이 요청은 포인트 시스템에 의해 단 한 번만 실행되어야 하는 명확한 '명령' 입니다. 만약 Kafka 토픽에 이 메시지를 넣는다면, 소비자 장애나 재조정(Rebalancing) 과정에서 동일한 메시지가 중복 처리될 위험을 방지하기 위한 복잡한 '멱등성(Idempotency)' 로직이 소비자 측에 반드시 추가로 구현되어야 합니다.
- 처리 보장 메커니즘의 중요성: RabbitMQ는 소비자가 메시지를 처리한 후 확인 응답(Acknowledgement) 을 보내야만 큐에서 메시지를 제거하는 메커니즘을 기본으로 제공합니다. 만약 소비자가 처리 중 실패하면(Nack을 보내거나 응답이 없으면), 브로커는 이 메시지를 다른 소비자에게 다시 전달하거나, 별도의 'Dead-Letter Queue' 로 보내 실패 원인을 분석하고 수동으로 복구할 기회를 제공합니다. 이는 금전적 데이터의 정합성을 보장하는 데 매우 강력하고 직관적인 기능입니다.
- 라우팅의 유연성: 지금은 단순한 포인트 지급이지만, 향후 "VIP 고객의 포인트 요청은 우선 처리" 하거나 "어뷰징 의심 요청은 별도의 큐로 분리"하는 등 비즈니스 규칙이 복잡해질 수 있습니다. RabbitMQ의 Exchange-Queue 바인딩과 다양한 라우팅 규칙은 이러한 요구사항 변화에 훨씬 유연하게 대처할 수 있는 구조를 제공합니다.
따라서 우리의 최종 아키텍처는 '최고의 도구를 올바른 문제에 사용한다' 는 원칙 아래 다음과 같이 결정되었습니다.
- Apache Kafka (기존 역할 유지): 서비스 전반의 상태 변화나 발생한 사실을 전파하는 '이벤트 허브' 로서의 역할을 계속 수행합니다. (
StudioCreated,UserRegistered등) - RabbitMQ (신규 도입): 포인트 지급/사용과 같이 반드시 한 번의 처리가 보장되어야 하고, 트랜잭션의 안정성이 중요한 '명령'을 처리하는 전용 브로커로 사용합니다.
이처럼 각 기술의 철학과 장점을 가장 잘 살릴 수 있는 곳에 배치하는 'Polyglot Messaging' 접근 방식을 통해, 시스템 전체의 안정성과 확장성을 모두 확보하고자 합니다.
728x90
'BindProject' 카테고리의 다른 글
| AOP 도입하기 (4) | 2025.08.01 |
|---|---|
| 포인트 시스템 구축기 : 이벤트 발행 시스템을 위한 플러그인 아키텍처 구축기 (3) | 2025.07.29 |
| 백엔드 아키텍처 : 바인드 서비스레포 구조(모노레포 VS 폴리레포) (1) | 2025.07.28 |
| 스튜디오 예약 시스템의 설계와 구현 : 유연한 운영 시간 설계와 MSA의 시너지 (5) | 2025.07.27 |
| 클린코드를 위한 여정 : 빈혈증에 걸린 도메인 모델(Anemic Domain Model) 처리하기: USER_PROFILE (3) | 2025.07.26 |