Skip to content

HPL-BE2/ecommerce-platform

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

17 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ›๏ธ E-Commerce Platform

์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ ์šฉํ•œ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์ด์ปค๋จธ์Šค ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค.

image

๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

Spring Boot 3 ๊ธฐ๋ฐ˜์˜ ํ”„๋กœ๋•์…˜ ๋ ˆ๋ฒจ ์ด์ปค๋จธ์Šค API ํ”Œ๋žซํผ์œผ๋กœ, MSA(Microservices Architecture) ์ „ํ™˜์„ ์œ„ํ•œ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์„ค๊ณ„๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ํŠน์ง•

  • โœ… ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ - Ports & Adapters ํŒจํ„ด์œผ๋กœ ๋„๋ฉ”์ธ๊ณผ ์ธํ”„๋ผ ๊ณ„์ธต ๋ถ„๋ฆฌ
  • โœ… ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์„ค๊ณ„ - Kafka๋ฅผ ํ™œ์šฉํ•œ ๋น„๋™๊ธฐ ๋ฉ”์‹œ์ง• ๋ฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ
  • โœ… ๋ถ„์‚ฐ ์‹œ์Šคํ…œ ํŒจํ„ด - Outbox ํŒจํ„ด, Saga ํŒจํ„ด, ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ
  • โœ… ๋™์‹œ์„ฑ ์ œ์–ด - Redis ๋ถ„์‚ฐ ๋ฝ, ๋‚™๊ด€์ /๋น„๊ด€์  ๋ฝ
  • โœ… ๋‹ค์ธต ์บ์‹ฑ - Redis ์บ์‹œ๋ฅผ ํ™œ์šฉํ•œ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • โœ… ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์„ค๊ณ„ - ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง•, Kafka ํŒŒํ‹ฐ์…”๋‹

๐Ÿ—๏ธ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜

์ „์ฒด ๊ตฌ์„ฑ๋„

graph TB
    subgraph "Client Layer"
        Client[ํด๋ผ์ด์–ธํŠธ<br/>Web/Mobile]
    end

    subgraph "Application Layer"
        API[REST API<br/>Controllers]
        Service[Application<br/>Services]
        Domain[Domain<br/>Models]
    end

    subgraph "Infrastructure Layer"
        JPA[JPA<br/>Adapters]
        RedisAdapter[Redis<br/>Client]
        KafkaAdapter[Kafka<br/>Producer]
    end

    subgraph "Data Layer"
        MySQL[(MySQL 8.0<br/>์ฃผ๋ฌธ/์ƒํ’ˆ/์ง€๊ฐ‘)]
        Redis[(Redis 7.0<br/>์บ์‹œ/๋ฝ/๋žญํ‚น)]
    end

    subgraph "Messaging Layer"
        Kafka[Apache Kafka<br/>3-Broker Cluster]
        Consumer1[Ranking<br/>Updater]
        Consumer2[Data<br/>Platform]
        Consumer3[Coupon<br/>Issuer]
    end

    Client --> API
    API --> Service
    Service --> Domain
    Domain --> JPA
    Domain --> RedisAdapter
    Domain --> KafkaAdapter

    JPA --> MySQL
    RedisAdapter --> Redis
    KafkaAdapter --> Kafka

    Kafka --> Consumer1
    Kafka --> Consumer2
    Kafka --> Consumer3

    Consumer1 --> Redis
    Consumer3 --> MySQL

    style Client fill:#e1f5ff
    style API fill:#fff4e1
    style Service fill:#ffe1f5
    style Domain fill:#f0e1ff
    style MySQL fill:#e1ffe1
    style Redis fill:#ffe1e1
    style Kafka fill:#fff9e1
Loading

ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์กฐ

graph LR
    subgraph "Inbound Adapters"
        REST[REST<br/>Controllers]
        Kafka_In[Kafka<br/>Consumers]
    end

    subgraph "Application Core"
        Ports_In[Inbound<br/>Ports]
        Services[Application<br/>Services]
        Domain[Domain<br/>Models]
        Ports_Out[Outbound<br/>Ports]
    end

    subgraph "Outbound Adapters"
        JPA[JPA<br/>Repositories]
        Redis[Redis<br/>Client]
        Kafka_Out[Kafka<br/>Producers]
    end

    REST --> Ports_In
    Kafka_In --> Ports_In
    Ports_In --> Services
    Services --> Domain
    Services --> Ports_Out
    Ports_Out --> JPA
    Ports_Out --> Redis
    Ports_Out --> Kafka_Out

    style REST fill:#e1f5ff
    style Kafka_In fill:#e1f5ff
    style Services fill:#ffe1f5
    style Domain fill:#f0e1ff
    style JPA fill:#e1ffe1
    style Redis fill:#ffe1e1
    style Kafka_Out fill:#fff9e1
Loading

๐Ÿ“ก ์ฃผ์š” API ์—”๋“œํฌ์ธํŠธ

๋ฉ”์„œ๋“œ ์—”๋“œํฌ์ธํŠธ ์„ค๋ช… ์ฃผ์š” ๊ธฐ๋Šฅ
GET /api/v1/products ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง•, ๊ฒ€์ƒ‰, Redis ์บ์‹ฑ (3๋ถ„)
GET /api/v1/products/{id} ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ Redis ์บ์‹ฑ (10๋ถ„)
POST /api/v1/wallets/{userId}/topups ์ง€๊ฐ‘ ์ถฉ์ „ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ, ๋น„๊ด€์  ๋ฝ
POST /api/v1/orders ์ฃผ๋ฌธ ์ƒ์„ฑ ์žฌ๊ณ  ๊ฒ€์ฆ, ์ฟ ํฐ ์ ์šฉ, ๋ถ„์‚ฐ ๋ฝ
PATCH /api/v1/orders/{orderId}/complete ์ฃผ๋ฌธ ์™„๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (Kafka)
POST /coupons/{id}/issue ์ฟ ํฐ ๋ฐœ๊ธ‰ (๋™๊ธฐ) ๋ถ„์‚ฐ ๋ฝ, Redis ์›์ž์  ์นด์šดํ„ฐ
POST /coupons/{id}/issue-async ์ฟ ํฐ ๋ฐœ๊ธ‰ (๋น„๋™๊ธฐ) Kafka ๊ธฐ๋ฐ˜, Lua ์Šคํฌ๋ฆฝํŠธ
GET /api/v1/rankings/products ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ Redis Sorted Set, ์‹ค์‹œ๊ฐ„/์ผ๋ณ„/์ฃผ๊ฐ„

๐Ÿ”„ API ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ

๊ฐ API์˜ ์ƒ์„ธํ•œ ์ฒ˜๋ฆฌ ํ๋ฆ„์„ ํ™•์ธํ•˜๋ ค๋ฉด ์•„๋ž˜ ํ•ญ๋ชฉ์„ ํด๋ฆญํ•˜์„ธ์š”.

๐Ÿ“ฆ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ API - GET /api/v1/products

์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ํ”Œ๋กœ์šฐ

  • Redis ์บ์‹œ ์šฐ์„  ์กฐํšŒ (3๋ถ„ TTL)
  • ์บ์‹œ ๋ฏธ์Šค ์‹œ DB ์กฐํšŒ ํ›„ ์บ์‹ฑ
  • ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง•์œผ๋กœ ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
sequenceDiagram
    participant Client as ํด๋ผ์ด์–ธํŠธ
    participant Controller as ProductsController
    participant Service as ProductService
    participant Redis as Redis Cache
    participant DB as MySQL

    Client->>Controller: GET /api/v1/products?q=๊ฒ€์ƒ‰์–ด&limit=20
    Controller->>Service: getProducts(query, limit, cursor)
    Service->>Redis: ์บ์‹œ ์กฐํšŒ (products:key)

    alt ์บ์‹œ ํžˆํŠธ
        Redis-->>Service: ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜
    else ์บ์‹œ ๋ฏธ์Šค
        Service->>DB: SELECT * FROM products WHERE...
        DB-->>Service: ์ƒํ’ˆ ๋ชฉ๋ก
        Service->>Redis: ์บ์‹œ ์ €์žฅ (TTL: 3๋ถ„)
    end

    Service-->>Controller: ProductListResponse
    Controller-->>Client: 200 OK + ์ƒํ’ˆ ๋ชฉ๋ก
Loading
๐Ÿ“ฆ ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ API - GET /api/v1/products/{id}

์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ํ”Œ๋กœ์šฐ

  • Redis ์บ์‹œ ์šฐ์„  ์กฐํšŒ (10๋ถ„ TTL)
  • ์ƒํ’ˆ ์ •๋ณด + ๊ฐ€๊ฒฉ ์ •๋ณด ํฌํ•จ
sequenceDiagram
    participant Client as ํด๋ผ์ด์–ธํŠธ
    participant Controller as ProductsController
    participant Service as ProductService
    participant Redis as Redis Cache
    participant DB as MySQL

    Client->>Controller: GET /api/v1/products/{productId}
    Controller->>Service: getProductDetail(productId)
    Service->>Redis: ์บ์‹œ ์กฐํšŒ (product-detail:{id})

    alt ์บ์‹œ ํžˆํŠธ
        Redis-->>Service: ์บ์‹œ๋œ ์ƒํ’ˆ ์ •๋ณด
    else ์บ์‹œ ๋ฏธ์Šค
        Service->>DB: SELECT * FROM products WHERE id=?
        DB-->>Service: ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด
        Service->>Redis: ์บ์‹œ ์ €์žฅ (TTL: 10๋ถ„)
    end

    Service-->>Controller: ProductDetailResponse
    Controller-->>Client: 200 OK + ์ƒํ’ˆ ์ƒ์„ธ
Loading
๐Ÿ’ฐ ์ง€๊ฐ‘ ์ถฉ์ „ API - POST /api/v1/wallets/{userId}/topups

์ง€๊ฐ‘ ์ถฉ์ „ ํ”Œ๋กœ์šฐ

  • ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ (Idempotency-Key ํ—ค๋”)
  • ๋น„๊ด€์  ๋ฝ(SELECT FOR UPDATE)์„ ํ†ตํ•œ ๋™์‹œ์„ฑ ์ œ์–ด
  • ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ถ”์  ๋ฐ ์ž”์•ก ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๋ฐฉ์ง€
sequenceDiagram
    participant Client as ํด๋ผ์ด์–ธํŠธ
    participant Controller as WalletController
    participant Service as WalletService
    participant DB as MySQL

    Client->>Controller: POST /api/v1/wallets/{userId}/topups<br/>Header: Idempotency-Key<br/>Body: {amount, refType, refId}
    Controller->>Service: topup(userId, amount, idempotencyKey)

    Service->>DB: ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ<br/>SELECT * FROM wallet_transactions<br/>WHERE user_id=? AND idempotency_key=?

    alt ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์š”์ฒญ
        DB-->>Service: ๊ธฐ์กด ๊ฑฐ๋ž˜ ๋‚ด์—ญ
        Service-->>Controller: {idempotent: true, ๊ธฐ์กด ๊ฒฐ๊ณผ}
    else ์‹ ๊ทœ ์š”์ฒญ
        Service->>DB: BEGIN TRANSACTION
        Service->>DB: ๋น„๊ด€์  ๋ฝ ํš๋“<br/>SELECT * FROM wallets<br/>WHERE user_id=? FOR UPDATE
        DB-->>Service: Wallet ์ •๋ณด

        Service->>Service: ์ž”์•ก ๊ณ„์‚ฐ (Math.addExact)<br/>์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๊ฒ€์ฆ

        Service->>DB: INSERT INTO wallet_transactions
        Service->>DB: UPDATE wallets SET balance = balance + ?
        Service->>DB: COMMIT

        DB-->>Service: ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ
        Service-->>Controller: {idempotent: false, ์ƒˆ ์ž”์•ก}
    end

    Controller-->>Client: 200 OK + ๊ฑฐ๋ž˜ ๊ฒฐ๊ณผ
Loading
๐Ÿ›’ ์ฃผ๋ฌธ ์ƒ์„ฑ API - POST /api/v1/orders

์ฃผ๋ฌธ ์ƒ์„ฑ ํ”Œ๋กœ์šฐ

  • ๋ณ‘๋ ฌ ๊ฒ€์ฆ: ์žฌ๊ณ , ์ฟ ํฐ, ์ง€๊ฐ‘ ์ž”์•ก
  • Redis ๋ถ„์‚ฐ ๋ฝ ๊ธฐ๋ฐ˜ ์žฌ๊ณ  ์˜ˆ์•ฝ
  • ์‹คํŒจ ์‹œ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰ (์žฌ๊ณ  ๋ณต์›, ์ฟ ํฐ ํ•ด์ œ, ์ง€๊ฐ‘ ํ™˜๋ถˆ)
sequenceDiagram
    participant Client as ํด๋ผ์ด์–ธํŠธ
    participant Controller as OrderController
    participant OrderService as OrderService
    participant InventoryService as InventoryService
    participant CouponService as CouponService
    participant WalletService as WalletService
    participant Redis as Redis
    participant DB as MySQL

    Client->>Controller: POST /api/v1/orders<br/>Header: Idempotency-Key<br/>Body: {userId, items, couponCode, expectedTotal}

    Controller->>OrderService: createOrder(request)

    OrderService->>DB: ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ<br/>SELECT * FROM orders WHERE request_key=?

    alt ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์ฃผ๋ฌธ
        DB-->>OrderService: ๊ธฐ์กด ์ฃผ๋ฌธ ์ •๋ณด
        OrderService-->>Controller: ๊ธฐ์กด ์ฃผ๋ฌธ ๋ฐ˜ํ™˜
    else ์‹ ๊ทœ ์ฃผ๋ฌธ

        par ๋ณ‘๋ ฌ ๊ฒ€์ฆ
            OrderService->>InventoryService: validateStock(items)
            InventoryService->>Redis: ์žฌ๊ณ  ์บ์‹œ ์กฐํšŒ
            InventoryService->>DB: SELECT stock FROM inventory
            InventoryService-->>OrderService: ์žฌ๊ณ  OK
        and
            OrderService->>CouponService: validateCoupon(couponCode, userId)
            CouponService->>DB: ์ฟ ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
            CouponService-->>OrderService: ์ฟ ํฐ ์ •๋ณด + ํ• ์ธ์•ก
        and
            OrderService->>WalletService: getBalance(userId)
            WalletService->>DB: SELECT balance FROM wallets
            WalletService-->>OrderService: ์ž”์•ก ์ •๋ณด
        end

        OrderService->>OrderService: ์ด์•ก ๊ณ„์‚ฐ ๋ฐ ๊ฒ€์ฆ<br/>(expectedTotal ๋งค์นญ)

        OrderService->>DB: BEGIN TRANSACTION

        loop ๊ฐ ์ƒํ’ˆ๋ณ„
            OrderService->>Redis: ๋ถ„์‚ฐ ๋ฝ ํš๋“<br/>product:{id}:order:lock
            OrderService->>InventoryService: reserve(productId, qty)
            InventoryService->>DB: UPDATE inventory<br/>SET stock = stock - ?<br/>WHERE version = ? (๋‚™๊ด€์  ๋ฝ)
            alt ๋‚™๊ด€์  ๋ฝ ์ถฉ๋Œ
                DB-->>InventoryService: ์—…๋ฐ์ดํŠธ ์‹คํŒจ
                InventoryService->>InventoryService: ์žฌ์‹œ๋„ (3ํšŒ, ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„)
            end
            OrderService->>Redis: ๋ฝ ํ•ด์ œ
        end

        OrderService->>CouponService: useCoupon(couponCode, userId)
        CouponService->>DB: UPDATE coupon_issuances SET used=true

        OrderService->>WalletService: debit(userId, total)
        WalletService->>DB: SELECT * FROM wallets FOR UPDATE
        WalletService->>DB: UPDATE wallets SET balance = balance - ?

        OrderService->>DB: INSERT INTO orders
        OrderService->>DB: INSERT INTO order_items

        OrderService->>DB: COMMIT

        OrderService-->>Controller: ์ฃผ๋ฌธ ์ƒ์„ฑ ์™„๋ฃŒ
    end

    Controller-->>Client: 201 Created + ์ฃผ๋ฌธ ์ •๋ณด

    Note over Client,DB: ์‹คํŒจ ์‹œ ๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ์‹คํ–‰<br/>(์žฌ๊ณ  ๋ณต์›, ์ฟ ํฐ ํ•ด์ œ, ์ง€๊ฐ‘ ํ™˜๋ถˆ)
Loading
โœ… ์ฃผ๋ฌธ ์™„๋ฃŒ API - PATCH /api/v1/orders/{orderId}/complete

์ฃผ๋ฌธ ์™„๋ฃŒ ๋ฐ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ํ”Œ๋กœ์šฐ

  • Spring Application Event๋กœ ์ฆ‰์‹œ ๋ฐœํ–‰
  • Outbox ํŒจํ„ด์œผ๋กœ ์ด๋ฒคํŠธ ์˜์†์„ฑ ๋ณด์žฅ
  • Kafka๋กœ ๋‹ค์ค‘ ์ปจ์Šˆ๋จธ์—๊ฒŒ ์ „๋‹ฌ (๋žญํ‚น ์—…๋ฐ์ดํŠธ, ๋ฐ์ดํ„ฐ ํ”Œ๋žซํผ)
sequenceDiagram
    participant Client as ํด๋ผ์ด์–ธํŠธ
    participant Controller as OrderController
    participant OrderService as OrderService
    participant DB as MySQL
    participant EventBus as Spring Event Bus
    participant RankingListener as RankingListener
    participant DataListener as DataListener
    participant OutboxDispatcher as OutboxDispatcher
    participant Kafka as Kafka

    Client->>Controller: PATCH /api/v1/orders/{orderId}/complete

    Controller->>OrderService: complete(orderId)

    OrderService->>DB: BEGIN TRANSACTION
    OrderService->>DB: UPDATE orders<br/>SET status='COMPLETED', completed_at=NOW()<br/>WHERE order_id=? AND status='RESERVED'

    OrderService->>DB: INSERT INTO outbox_events<br/>(event_type, payload, status='PENDING')

    OrderService->>DB: COMMIT

    OrderService->>EventBus: publish(OrderCompletedDomainEvent)

    par ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ
        EventBus->>RankingListener: onOrderCompleted(event)
        RankingListener->>Kafka: produce(ecommerce.order.events)<br/>Key: orderId
        Kafka-->>RankingListener: ACK

    and
        EventBus->>DataListener: onOrderCompleted(event)
        DataListener->>Kafka: produce(ecommerce.order.events)<br/>๋ฐ์ดํ„ฐ ํ”Œ๋žซํผ์šฉ
        Kafka-->>DataListener: ACK

    and
        EventBus->>OutboxDispatcher: onOrderCompleted(event)
        Note over OutboxDispatcher: ์ด๋ฏธ DB์— ์ €์žฅ๋จ<br/>์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ 5์ดˆ๋งˆ๋‹ค ํด๋ง
    end

    OrderService-->>Controller: ์™„๋ฃŒ ์‘๋‹ต
    Controller-->>Client: 200 OK + ์ฃผ๋ฌธ ์™„๋ฃŒ ์ •๋ณด

    Note over OutboxDispatcher,Kafka: ๋ณ„๋„ ์Šค์ผ€์ค„๋Ÿฌ (5์ดˆ๋งˆ๋‹ค)
    OutboxDispatcher->>DB: SELECT * FROM outbox_events<br/>WHERE status='PENDING' LIMIT 20
    DB-->>OutboxDispatcher: ๋Œ€๊ธฐ ์ค‘์ธ ์ด๋ฒคํŠธ๋“ค

    loop ๊ฐ ์ด๋ฒคํŠธ (๋ฐฐ์น˜ 20๊ฐœ)
        OutboxDispatcher->>Kafka: produce(ํ† ํ”ฝ, ์ด๋ฒคํŠธ)
        alt Kafka ๋ฐœํ–‰ ์„ฑ๊ณต
            Kafka-->>OutboxDispatcher: ACK
            OutboxDispatcher->>DB: UPDATE outbox_events SET status='SENT'
        else Kafka ๋ฐœํ–‰ ์‹คํŒจ
            Kafka-->>OutboxDispatcher: ERROR
            OutboxDispatcher->>DB: UPDATE retry_count++, next_retry_at=...
        end
    end
Loading
๐ŸŽŸ๏ธ ์ฟ ํฐ ๋ฐœ๊ธ‰ API (๋™๊ธฐ) - POST /coupons/{id}/issue

๋™๊ธฐ ์ฟ ํฐ ๋ฐœ๊ธ‰ ํ”Œ๋กœ์šฐ

  • Redis ๋ถ„์‚ฐ ๋ฝ์œผ๋กœ ๋™์‹œ ์š”์ฒญ ์ œ์–ด
  • Redis INCR๋กœ ์›์ž์  ์นด์šดํ„ฐ ์ฆ๊ฐ€
  • ์„ ์ฐฉ์ˆœ ์ œํ•œ ๊ฒ€์ฆ ๋ฐ DB ์ €์žฅ
sequenceDiagram
    participant Client as ํด๋ผ์ด์–ธํŠธ
    participant Controller as CouponController
    participant Service as CouponService
    participant Redis as Redis
    participant DB as MySQL

    Client->>Controller: POST /coupons/{couponId}/issue?userId={userId}

    Controller->>Service: issueCoupon(couponId, userId)

    Service->>Redis: ๋ถ„์‚ฐ ๋ฝ ํš๋“<br/>coupon:{couponId}:lock<br/>waitTime: 2s, leaseTime: 5s

    alt ๋ฝ ํš๋“ ์‹คํŒจ
        Redis-->>Service: ๋ฝ ํƒ€์ž„์•„์›ƒ
        Service-->>Controller: 429 Too Many Requests
        Controller-->>Client: ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„
    else ๋ฝ ํš๋“ ์„ฑ๊ณต

        Service->>Redis: INCR coupon:{couponId}:issued
        Redis-->>Service: ํ˜„์žฌ ๋ฐœ๊ธ‰ ์ˆ˜

        Service->>Service: ๋ฐœ๊ธ‰ ํ•œ๋„ ๊ฒ€์ฆ<br/>(issued <= maxIssuance)

        alt ๋ฐœ๊ธ‰ ํ•œ๋„ ์ดˆ๊ณผ
            Service->>Redis: DECR coupon:{couponId}:issued
            Service->>Redis: ๋ฝ ํ•ด์ œ
            Service-->>Controller: 409 Conflict - ์ฟ ํฐ ์†Œ์ง„
        else ๋ฐœ๊ธ‰ ๊ฐ€๋Šฅ
            Service->>DB: BEGIN TRANSACTION

            Service->>DB: SELECT * FROM coupons WHERE coupon_id=?
            DB-->>Service: ์ฟ ํฐ ์ •๋ณด

            Service->>Service: ์œ ํšจ์„ฑ ๊ฒ€์ฆ<br/>(์œ ํšจ ๊ธฐ๊ฐ„, ์ค‘๋ณต ๋ฐœ๊ธ‰ ์ฒดํฌ)

            Service->>DB: INSERT INTO coupon_issuances<br/>(coupon_id, user_id, issued_at)

            Service->>DB: COMMIT

            Service->>Redis: ๋ฝ ํ•ด์ œ
            Service-->>Controller: ๋ฐœ๊ธ‰ ์„ฑ๊ณต
        end
    end

    Controller-->>Client: 200 OK + ์ฟ ํฐ ๋ฐœ๊ธ‰ ์ •๋ณด
Loading
๐ŸŽŸ๏ธ ์ฟ ํฐ ๋ฐœ๊ธ‰ API (๋น„๋™๊ธฐ) - POST /coupons/{id}/issue-async

๋น„๋™๊ธฐ ์ฟ ํฐ ๋ฐœ๊ธ‰ ํ”Œ๋กœ์šฐ (Kafka)

  • Kafka Producer๋กœ ์ฆ‰์‹œ ์‘๋‹ต (202 Accepted)
  • Kafka Consumer๊ฐ€ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ
  • Redis Lua Script๋กœ ์›์ž์  ๊ฒ€์ฆ ๋ฐ ์ฆ๊ฐ€
  • ๊ฒฐ๊ณผ๋Š” ๋ณ„๋„ ํ† ํ”ฝ์œผ๋กœ ์ „์†ก
sequenceDiagram
    participant Client as ํด๋ผ์ด์–ธํŠธ
    participant Controller as CouponController
    participant Producer as CouponKafkaProducer
    participant Redis as Redis
    participant Kafka as Kafka
    participant Consumer as CouponKafkaConsumer
    participant Service as AsyncCouponService
    participant DB as MySQL

    Client->>Controller: POST /coupons/{couponId}/issue-async?userId={userId}

    Controller->>Producer: sendIssueRequest(couponId, userId)

    Producer->>Producer: requestId ์ƒ์„ฑ (UUID)

    Producer->>Kafka: produce(coupon.issue.requests)<br/>Key: couponId<br/>Value: {requestId, couponId, userId}

    Kafka-->>Producer: ACK (Idempotent Producer)

    Producer-->>Controller: requestId ๋ฐ˜ํ™˜
    Controller-->>Client: 202 Accepted<br/>{requestId, message: "์ฒ˜๋ฆฌ ์ค‘"}

    Note over Kafka,Consumer: Kafka Consumer (3 threads)

    Kafka->>Consumer: poll(coupon.issue.requests)
    Consumer->>Service: processIssueRequest(couponId, userId)

    Service->>Redis: Lua Script ์‹คํ–‰<br/>์›์ž์  ๊ฒ€์ฆ ๋ฐ INCR

    alt Lua Script - ๋ฐœ๊ธ‰ ๊ฐ€๋Šฅ
        Redis-->>Service: SUCCESS + issued count

        Service->>DB: BEGIN TRANSACTION
        Service->>DB: INSERT INTO coupon_issuances<br/>(coupon_id, user_id, request_id, ...)
        Service->>DB: COMMIT

        Service->>Kafka: produce(coupon.issue.results)<br/>{requestId, status: SUCCESS}

        Service->>Consumer: manual ACK
        Consumer->>Kafka: commit offset

    else Lua Script - ๋ฐœ๊ธ‰ ๋ถˆ๊ฐ€
        Redis-->>Service: FAIL - ๋ฐœ๊ธ‰ ํ•œ๋„ ์ดˆ๊ณผ

        Service->>Kafka: produce(coupon.issue.results)<br/>{requestId, status: FAILED, reason: SOLD_OUT}

        Service->>Consumer: manual ACK
        Consumer->>Kafka: commit offset
    end

    Note over Client: ํด๋ผ์ด์–ธํŠธ๋Š” ํด๋ง ๋˜๋Š”<br/>WebSocket์œผ๋กœ ๊ฒฐ๊ณผ ์ˆ˜์‹ 
Loading
๐Ÿ† ์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ API - GET /api/v1/rankings/products

์ƒํ’ˆ ๋žญํ‚น ์กฐํšŒ ํ”Œ๋กœ์šฐ

  • Redis Sorted Set ์กฐํšŒ (O(log n) ์„ฑ๋Šฅ)
  • ์‹ค์‹œ๊ฐ„/์ผ๋ณ„/์ฃผ๊ฐ„ ๋žญํ‚น ์ง€์›
  • Graceful degradation (Redis ์žฅ์•  ์‹œ ๋นˆ ๊ฒฐ๊ณผ)
sequenceDiagram
    participant Client as ํด๋ผ์ด์–ธํŠธ
    participant Controller as RankingController
    participant Service as ProductRankingService
    participant Redis as Redis Sorted Set
    participant DB as MySQL

    Client->>Controller: GET /api/v1/rankings/products?period=DAILY&limit=10

    Controller->>Service: getTopProducts(period, referenceDate, limit)

    Service->>Service: ranking key ์ƒ์„ฑ<br/>์˜ˆ: ranking:daily:2025-12-14

    Service->>Redis: ZREVRANGE ranking:daily:2025-12-14 0 9<br/>WITHSCORES

    alt Redis ๋ฐ์ดํ„ฐ ์กด์žฌ
        Redis-->>Service: [(productId, score), ...]

        Service->>DB: SELECT id, name, thumbnail_url, unit_price<br/>FROM products WHERE id IN (?, ?, ...)

        DB-->>Service: ์ƒํ’ˆ ์ •๋ณด ๋ชฉ๋ก

        Service->>Service: score์™€ ์ƒํ’ˆ ์ •๋ณด ๋งคํ•‘
        Service-->>Controller: RankingResponse<br/>{items: [{productId, name, score}, ...]}

    else Redis ๋ฐ์ดํ„ฐ ์—†์Œ
        Redis-->>Service: ๋นˆ ๋ฐฐ์—ด
        Service-->>Controller: ๋นˆ ๋žญํ‚น (graceful degradation)
    end

    Controller-->>Client: 200 OK + ๋žญํ‚น ์ •๋ณด

    Note over Redis: ๋žญํ‚น ์—…๋ฐ์ดํŠธ๋Š”<br/>์ฃผ๋ฌธ ์™„๋ฃŒ ์ด๋ฒคํŠธ ์ˆ˜์‹  ์‹œ<br/>ZINCRBY๋กœ ์‹คํ–‰
Loading

๐Ÿ’พ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„

๐Ÿ“Š ERD (Entity Relationship Diagram)

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ERD

erDiagram
    USERS ||--|| WALLETS : has
    USERS ||--o{ WALLET_TRANSACTIONS : creates
    USERS ||--o{ ORDERS : places
    USERS ||--o{ COUPON_ISSUANCES : receives

    PRODUCTS ||--|| INVENTORY : has
    PRODUCTS ||--o{ ORDER_ITEMS : contains
    PRODUCTS ||--o{ STOCK_MOVEMENTS : tracks

    ORDERS ||--|{ ORDER_ITEMS : contains
    ORDERS ||--o| PAYMENTS : has

    COUPONS ||--o{ COUPON_ISSUANCES : issues

    USERS {
        bigint user_id PK
        string username
        string email
        timestamp created_at
    }

    WALLETS {
        bigint wallet_id PK
        bigint user_id FK
        bigint balance
        timestamp updated_at
    }

    WALLET_TRANSACTIONS {
        bigint transaction_id PK
        bigint user_id FK
        string transaction_type
        bigint amount
        bigint balance_after
        string idempotency_key UK
        timestamp created_at
    }

    PRODUCTS {
        bigint product_id PK
        string sku UK
        string name
        decimal unit_price
        string thumbnail_url
        timestamp created_at
    }

    INVENTORY {
        bigint inventory_id PK
        bigint product_id FK
        int stock
        int version
        timestamp updated_at
    }

    STOCK_MOVEMENTS {
        bigint movement_id PK
        bigint product_id FK
        string movement_type
        int quantity
        string reference_type
        bigint reference_id
        timestamp created_at
    }

    ORDERS {
        bigint order_id PK
        bigint user_id FK
        string status
        decimal subtotal
        decimal discount
        decimal total
        string request_key UK
        timestamp completed_at
        timestamp created_at
    }

    ORDER_ITEMS {
        bigint order_item_id PK
        bigint order_id FK
        bigint product_id FK
        string product_name
        decimal unit_price
        int quantity
        decimal line_total
    }

    PAYMENTS {
        bigint payment_id PK
        bigint order_id FK
        string payment_method
        decimal amount
        string status
        timestamp created_at
    }

    COUPONS {
        bigint coupon_id PK
        string code UK
        string discount_type
        decimal discount_value
        decimal min_purchase_amount
        decimal max_discount_amount
        int max_issuance
        timestamp starts_at
        timestamp ends_at
    }

    COUPON_ISSUANCES {
        bigint issuance_id PK
        bigint coupon_id FK
        bigint user_id FK
        boolean used
        timestamp issued_at
        timestamp used_at
    }
Loading

์ฃผ์š” ํ…Œ์ด๋ธ” ์„ค๋ช…

  • USERS: ์‚ฌ์šฉ์ž ๊ณ„์ • ์ •๋ณด
  • WALLETS: ์‚ฌ์šฉ์ž๋ณ„ ์ง€๊ฐ‘ ์ž”์•ก (1:1)
  • WALLET_TRANSACTIONS: ์ง€๊ฐ‘ ๊ฑฐ๋ž˜ ๋‚ด์—ญ (์ถฉ์ „/์ฐจ๊ฐ), ๋ฉฑ๋“ฑ์„ฑ ํ‚ค ํฌํ•จ
  • PRODUCTS: ์ƒํ’ˆ ์นดํƒˆ๋กœ๊ทธ
  • INVENTORY: ์žฌ๊ณ  ๊ด€๋ฆฌ (๋‚™๊ด€์  ๋ฝ version ํ•„๋“œ)
  • STOCK_MOVEMENTS: ์žฌ๊ณ  ๋ณ€๋™ ์ด๋ ฅ (๊ฐ์‚ฌ ์ถ”์ )
  • ORDERS: ์ฃผ๋ฌธ ์ •๋ณด (request_key๋กœ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ)
  • ORDER_ITEMS: ์ฃผ๋ฌธ ์ƒํ’ˆ ๋ชฉ๋ก
  • COUPONS: ์ฟ ํฐ ์ •์˜ (ํ• ์ธ์œจ, ํ•œ๋„, ์œ ํšจ๊ธฐ๊ฐ„)
  • COUPON_ISSUANCES: ์‚ฌ์šฉ์ž๋ณ„ ์ฟ ํฐ ๋ฐœ๊ธ‰ ๋‚ด์—ญ

๐Ÿ“จ Kafka ์ด๋ฒคํŠธ ์•„ํ‚คํ…์ฒ˜

๐Ÿ”„ Kafka ํ† ํ”ฝ ๋ฐ ์ปจ์Šˆ๋จธ ๊ตฌ์กฐ

Kafka ํ† ํ”ฝ ๋ฐ ์ปจ์Šˆ๋จธ ๊ทธ๋ฃน

graph TB
    subgraph "Producers"
        OrderService[OrderService<br/>์ฃผ๋ฌธ ์™„๋ฃŒ]
        OutboxDispatcher[OutboxDispatcher<br/>Outbox ํด๋ง]
        CouponProducer[CouponKafkaProducer<br/>์ฟ ํฐ ๋ฐœ๊ธ‰ ์š”์ฒญ]
    end

    subgraph "Kafka Cluster (3 Brokers)"
        Topic1[ecommerce.order.events<br/>ํŒŒํ‹ฐ์…˜: 3๊ฐœ<br/>๋ฆฌํ”Œ๋ฆฌ์ผ€์ด์…˜: 1]
        Topic2[coupon.issue.requests<br/>ํŒŒํ‹ฐ์…˜: 3๊ฐœ<br/>๋ฆฌํ”Œ๋ฆฌ์ผ€์ด์…˜: 1]
        Topic3[coupon.issue.results<br/>ํŒŒํ‹ฐ์…˜: 3๊ฐœ<br/>๋ฆฌํ”Œ๋ฆฌ์ผ€์ด์…˜: 1]
    end

    subgraph "Consumer Groups"
        RankingConsumer[Ranking Updater<br/>๊ทธ๋ฃน: ranking-updater<br/>์Šค๋ ˆ๋“œ: 3๊ฐœ]
        DataConsumer[Data Platform<br/>๊ทธ๋ฃน: data-platform<br/>์Šค๋ ˆ๋“œ: 3๊ฐœ]
        CouponConsumer[Coupon Issuer<br/>๊ทธ๋ฃน: coupon-issuer<br/>์Šค๋ ˆ๋“œ: 3๊ฐœ]
    end

    subgraph "Processing"
        Redis[(Redis<br/>๋žญํ‚น ์—…๋ฐ์ดํŠธ)]
        Analytics[(Analytics<br/>๋ฐ์ดํ„ฐ ๋ถ„์„)]
        MySQL[(MySQL<br/>์ฟ ํฐ ๋ฐœ๊ธ‰)]
    end

    OrderService --> Topic1
    OutboxDispatcher --> Topic1
    CouponProducer --> Topic2

    Topic1 --> RankingConsumer
    Topic1 --> DataConsumer
    Topic2 --> CouponConsumer

    RankingConsumer --> Redis
    DataConsumer --> Analytics
    CouponConsumer --> MySQL
    CouponConsumer --> Topic3

    style Topic1 fill:#fff9e1
    style Topic2 fill:#e1f5ff
    style Topic3 fill:#f0e1ff
    style RankingConsumer fill:#e1ffe1
    style DataConsumer fill:#ffe1e1
    style CouponConsumer fill:#ffe1f5
Loading

Kafka ์„ค์ • ์š”์•ฝ

Producer ์„ค์ •:

  • Acks: all (๋ชจ๋“  ๋ณต์ œ๋ณธ ํ™•์ธ)
  • Idempotence: enabled (์ค‘๋ณต ๋ฐฉ์ง€)
  • Compression: snappy (์••์ถ•)
  • Retries: 3

Consumer ์„ค์ •:

  • Auto Offset Reset: earliest
  • Enable Auto Commit: false (์ˆ˜๋™ ACK)
  • Max Poll Records: 500
  • Concurrency: 3 (๊ฐ ์ปจ์Šˆ๋จธ ๊ทธ๋ฃน๋‹น)

๐Ÿš€ ์บ์‹ฑ ์ „๋žต

โšก Redis ์บ์‹ฑ ์ „๋žต (Cache-Aside Pattern)

์บ์‹œ ์ฝ๊ธฐ ํ”Œ๋กœ์šฐ

flowchart TD
    Start([API ์š”์ฒญ]) --> CheckCache{Redis<br/>์บ์‹œ ํ™•์ธ}

    CheckCache -->|์บ์‹œ ํžˆํŠธ| ReturnCache[์บ์‹œ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜]
    CheckCache -->|์บ์‹œ ๋ฏธ์Šค| QueryDB[MySQL DB ์กฐํšŒ]

    QueryDB --> SaveCache[Redis์—<br/>๋ฐ์ดํ„ฐ ์ €์žฅ<br/>TTL ์„ค์ •]
    SaveCache --> ReturnDB[DB ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜]

    ReturnCache --> End([์‘๋‹ต])
    ReturnDB --> End

    style CheckCache fill:#fff9e1
    style ReturnCache fill:#e1ffe1
    style QueryDB fill:#ffe1e1
    style SaveCache fill:#e1f5ff
Loading

์บ์‹œ ์˜์—ญ ๋ฐ TTL

์บ์‹œ ์˜์—ญ ํ‚ค ํŒจํ„ด TTL ์šฉ๋„
products products:* 3๋ถ„ ์ƒํ’ˆ ๋ชฉ๋ก
product-detail product-detail:{id} 10๋ถ„ ์ƒํ’ˆ ์ƒ์„ธ
coupon-info coupon-info:{id} 5๋ถ„ ์ฟ ํฐ ์ •๋ณด
user-orders user-orders:{userId} 10๋ถ„ ์‚ฌ์šฉ์ž ์ฃผ๋ฌธ ๋ชฉ๋ก
product:{id}:stock - 30์ดˆ ์‹ค์‹œ๊ฐ„ ์žฌ๊ณ 
ranking:* ranking:{period}:{date} 1์‹œ๊ฐ„ ์ƒํ’ˆ ๋žญํ‚น (Sorted Set)

๋ถ„์‚ฐ ๋ฝ ํ‚ค

๋ฝ ํƒ€์ž… ํ‚ค ํŒจํ„ด Wait Time Lease Time
์ฟ ํฐ ๋ฐœ๊ธ‰ coupon:{id}:lock 2์ดˆ 5์ดˆ
์žฌ๊ณ  ์˜ˆ์•ฝ product:{id}:order:lock 2์ดˆ 10์ดˆ

๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ

๋ฐฑ์—”๋“œ ํ”„๋ ˆ์ž„์›Œํฌ

  • Spring Boot 3.4.1
  • Spring Cloud 2024.0.0
  • Java 17

๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ

  • MySQL 8.0 - ์ฃผ๋ฌธ, ์ƒํ’ˆ, ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ
  • Redis 7.0 - ์บ์‹ฑ, ๋ถ„์‚ฐ ๋ฝ, ๋žญํ‚น

๋ฉ”์‹œ์ง•

  • Apache Kafka 7.6.0 - ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ
  • 3-broker ํด๋Ÿฌ์Šคํ„ฐ
  • 4๊ฐœ ํ† ํ”ฝ (์ฃผ๋ฌธ ์ด๋ฒคํŠธ, ์ฟ ํฐ ๋ฐœ๊ธ‰ ์š”์ฒญ/๊ฒฐ๊ณผ)

์ฃผ์š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

  • Spring Data JPA - ORM
  • Redisson - ๋ถ„์‚ฐ ๋ฝ
  • Lettuce - Redis ํด๋ผ์ด์–ธํŠธ
  • Spring Kafka - Kafka ํ†ตํ•ฉ
  • SpringDoc OpenAPI - API ๋ฌธ์„œ (Swagger UI)
  • Testcontainers - ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ
  • Lombok - ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ ์ œ๊ฑฐ

โœจ ์ฃผ์š” ๊ธฐ๋Šฅ

1. ์ƒํ’ˆ ์นดํƒˆ๋กœ๊ทธ

  • โœ… ์ปค์„œ ๊ธฐ๋ฐ˜ ํŽ˜์ด์ง• (๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ตœ์ ํ™”)
  • โœ… Redis ์บ์‹ฑ (3๋ถ„ TTL)
  • โœ… ์ „์ฒด ํ…์ŠคํŠธ ๊ฒ€์ƒ‰ ๋ฐ ์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ๋ง
  • โœ… ์ •๋ ฌ ์˜ต์…˜ (ID, ์ด๋ฆ„, ๊ฐ€๊ฒฉ ๋“ฑ)

2. ์ง€๊ฐ‘ ๊ด€๋ฆฌ

  • โœ… ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ (Idempotency-Key ํ—ค๋”)
  • โœ… ๋น„๊ด€์  ๋ฝ(SELECT FOR UPDATE)์„ ํ†ตํ•œ ๋™์‹œ์„ฑ ์ œ์–ด
  • โœ… ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์ถ”์ 
  • โœ… ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๋ฐฉ์ง€ (Math.addExact)
  • โœ… ํ™˜๋ถˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜ (๋ณด์ƒ ํŠธ๋žœ์žญ์…˜)

3. ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ

  • โœ… ํฌ๊ด„์ ์ธ ๊ฒ€์ฆ:
    • ์žฌ๊ณ  ๊ฐ€์šฉ์„ฑ ์ฒดํฌ (๋‚™๊ด€์  ๋ฝ)
    • ์ฟ ํฐ ์ ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ๊ฒ€์ฆ
    • ์ง€๊ฐ‘ ์ž”์•ก ํ™•์ธ
    • ์ด์•ก ๋งค์นญ (์ถฉ๋Œ ๊ฐ์ง€)
  • โœ… ์ฃผ๋ฌธ ์™„๋ฃŒ ์‹œ ์ด๋ฒคํŠธ ๋ฐœํ–‰
  • โœ… ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ (request_key)
  • โœ… ๋‹ค์ค‘ ์ƒํ’ˆ ์ฃผ๋ฌธ ์ง€์›
  • โœ… ํ• ์ธ ๊ณ„์‚ฐ (๋น„์œจ/๊ณ ์ • ๊ธˆ์•ก)
  • โœ… ์ฟ ํฐ ์‚ฌ์šฉ ์ถ”์ 

4. ์ฟ ํฐ ์‹œ์Šคํ…œ

  • โœ… ๋‘ ๊ฐ€์ง€ ๋ฐœ๊ธ‰ ๋ฐฉ์‹:
    • ๋™๊ธฐ: ์ฆ‰์‹œ ๋ฐœ๊ธ‰ (๋ถ„์‚ฐ ๋ฝ ์‚ฌ์šฉ)
    • ๋น„๋™๊ธฐ: Kafka ๊ธฐ๋ฐ˜ (Lua ์Šคํฌ๋ฆฝํŠธ ๊ฒ€์ฆ)
  • โœ… ๊ธฐ๋Šฅ:
    • ๋น„์œจ ํ• ์ธ ๋ฐ ๊ณ ์ • ๊ธˆ์•ก ํ• ์ธ
    • ์ตœ์†Œ ๊ตฌ๋งค ๊ธˆ์•ก ์กฐ๊ฑด
    • ์ตœ๋Œ€ ํ• ์ธ ๊ธˆ์•ก ์ œํ•œ
    • ์œ ํšจ ๊ธฐ๊ฐ„ (์‹œ์ž‘์ผ/์ข…๋ฃŒ์ผ)
    • ๋ฐœ๊ธ‰ ํ•œ๋„ (์„ ์ฐฉ์ˆœ)
    • Redis ์›์ž์  ์นด์šดํ„ฐ๋กœ ๋™์‹œ์„ฑ ์ œ์–ด
    • ์ฟ ํฐ ํ•ด์ œ (๋ณด์ƒ ์ฒ˜๋ฆฌ)

5. ์žฌ๊ณ  ๊ด€๋ฆฌ

  • โœ… ์ด์ค‘ ์ €์žฅ์†Œ:
    • MySQL (์˜๊ตฌ ์ €์žฅ)
    • Redis (๋น ๋ฅธ ์กฐํšŒ)
  • โœ… ๋‚™๊ด€์  ๋ฝ (version ํ•„๋“œ)์œผ๋กœ DB ์ถฉ๋Œ ๋ฐฉ์ง€
  • โœ… ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜ (3ํšŒ, ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„)
  • โœ… ์žฌ๊ณ  ๋ณ€๋™ ๊ฐ์‚ฌ ์ถ”์ 
  • โœ… ์ƒํ’ˆ๋ณ„ ๋ถ„์‚ฐ ๋ฝ์œผ๋กœ ์•ˆ์ „ํ•œ ์˜ˆ์•ฝ
  • โœ… Cache-Aside ํŒจํ„ด์œผ๋กœ ๋น ๋ฅธ ์ฝ๊ธฐ
  • โœ… ์ฃผ๋ฌธ ์ทจ์†Œ ์‹œ ์žฌ๊ณ  ๋ณต์›

6. ์ƒํ’ˆ ๋žญํ‚น ์‹œ์Šคํ…œ

  • โœ… ๋‹ค์ค‘ ๋žญํ‚น ๊ธฐ๊ฐ„:
    • REALTIME - ์ฃผ๋ฌธ ์™„๋ฃŒ ์‹œ ์ฆ‰์‹œ ์—…๋ฐ์ดํŠธ
    • DAILY - ์ผ๋ณ„ ์ง‘๊ณ„ ๋žญํ‚น
    • WEEKLY - ์ฃผ๊ฐ„ ์ง‘๊ณ„ ๋žญํ‚น
  • โœ… Redis Sorted Set์œผ๋กœ O(log n) ์„ฑ๋Šฅ
  • โœ… Spring Application Events + Kafka๋กœ ์—…๋ฐ์ดํŠธ
  • โœ… ์ ์ˆ˜ ๊ธฐ์ค€:
    • ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰
    • ํŒ๋งค ๊ธˆ์•ก
    • ๊ณ ๊ฐ ์ฐธ์—ฌ ์ง€ํ‘œ
  • โœ… ๊ณผ๊ฑฐ ๋‚ ์งœ ์กฐํšŒ ์ง€์›
  • โœ… Graceful degradation (Redis ์žฅ์•  ์‹œ ๋นˆ ๊ฒฐ๊ณผ)

๐Ÿ”’ ๋™์‹œ์„ฑ ์ œ์–ด ์ „๋žต

๊ธฐ๋Šฅ ์ œ์–ด ๋ฐฉ์‹ ์„ค๋ช…
์ง€๊ฐ‘ ์ถฉ์ „/์ฐจ๊ฐ ๋น„๊ด€์  ๋ฝ SELECT ... FOR UPDATE๋กœ ํŠธ๋žœ์žญ์…˜ ๊ฒฉ๋ฆฌ
์žฌ๊ณ  ์˜ˆ์•ฝ ๋ถ„์‚ฐ ๋ฝ (Redisson) product:{id}:order:lock์œผ๋กœ ์ƒํ’ˆ๋ณ„ ์ง๋ ฌํ™”
์ฟ ํฐ ๋ฐœ๊ธ‰ ๋ถ„์‚ฐ ๋ฝ + Redis INCR coupon:{id}:lock + ์›์ž์  ์นด์šดํ„ฐ
์žฌ๊ณ  ์—…๋ฐ์ดํŠธ ๋‚™๊ด€์  ๋ฝ version ํ•„๋“œ๋กœ ์ถฉ๋Œ ๊ฐ์ง€ ๋ฐ ์žฌ์‹œ๋„
๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ DB Unique ์ œ์•ฝ (user_id, idempotency_key) ๋ณตํ•ฉ ์œ ๋‹ˆํฌ ํ‚ค

Deadlock ๋ฐฉ์ง€

  • ์ •๋ ฌ๋œ ์ˆœ์„œ๋กœ ๋ฝ ํš๋“ (product_id ์˜ค๋ฆ„์ฐจ์ˆœ)
  • Timeout ์„ค์ • (2์ดˆ wait, 5-10์ดˆ lease)
  • ์‹คํŒจ ์‹œ ๋น ๋ฅธ ์‹คํŒจ ์ „๋žต (Fail-Fast)

๐Ÿ›๏ธ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด

1. ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ (Ports & Adapters)

  • Inbound Ports: ์œ ์ฆˆ์ผ€์ด์Šค ์ธํ„ฐํŽ˜์ด์Šค (application.port.in)
  • Outbound Ports: ๋„๋ฉ”์ธ ํฌํŠธ ์ธํ„ฐํŽ˜์ด์Šค (domain.port.out)
  • Adapters: JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๊ฐ€ Outbound Ports ๊ตฌํ˜„

2. CQRS (Command Query Responsibility Segregation)

  • ์ฝ๊ธฐ ์ž‘์—… ๋ถ„๋ฆฌ (ProductReadPort, ProductDetailReadPort)
  • ์“ฐ๊ธฐ ์ž‘์—… ๋ถ„๋ฆฌ (OrderWritePort, WalletReadWritePort)

3. ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜

  • ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ์„œ๋น„์Šค์—์„œ ๋ฐœํ–‰
  • ๋‹ค์ค‘ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์ฒ˜๋ฆฌ
  • Outbox ํŒจํ„ด์œผ๋กœ ๋ณด์žฅ๋œ ์ „๋‹ฌ
  • Kafka๋กœ ๋ถ„์‚ฐ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ

4. Saga ํŒจํ„ด (๋ณด์ƒ ํŠธ๋žœ์žญ์…˜)

  • ์ฃผ๋ฌธ ์ƒ์„ฑ ์‹คํŒจ โ†’ ์ง€๊ฐ‘ ํ™˜๋ถˆ
  • ์ฟ ํฐ ๋ฐœ๊ธ‰ ์‹คํŒจ โ†’ ๋ฐœ๊ธ‰ ๋‚ด์—ญ ํ•ด์ œ
  • ์žฌ๊ณ  ์˜ˆ์•ฝ ์‹คํŒจ โ†’ ์žฌ๊ณ  ๋ณต์›

5. ๋ฉฑ๋“ฑ์„ฑ ํŒจํ„ด

  • Idempotency-Key ํ—ค๋”๋กœ API ์š”์ฒญ ์‹๋ณ„
  • DB Unique ์ œ์•ฝ (user_id, idempotency_key)
  • ์žฌ์‹œ๋„ ์‹œ ๊ธฐ์กด ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜

6. Cache-Aside ํŒจํ„ด

  • Redis ์บ์‹œ ๋จผ์ € ํ™•์ธ
  • ๋ฏธ์Šค ์‹œ DB ์กฐํšŒ
  • TTL ๊ธฐ๋ฐ˜ ์บ์‹œ ๋งŒ๋ฃŒ
  • ์ƒํ’ˆ, ์ฟ ํฐ, ์žฌ๊ณ ์— ์ ์šฉ

7. Outbox Event Dispatcher ํŒจํ„ด

  • ์ด๋ฒคํŠธ๋ฅผ DB์— ํŠธ๋žœ์žญ์…˜ ๋‚ด ์ €์žฅ
  • ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ 5์ดˆ๋งˆ๋‹ค ํด๋ง
  • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ (20๊ฐœ์”ฉ)
  • ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„ ์žฌ์‹œ๋„ (30์ดˆ * retry_count)
  • ์ตœ์ข… ์ƒํƒœ: PENDING, SENT, FAILED

๐Ÿ’ป ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ •

1. ์ธํ”„๋ผ ์‹คํ–‰

Docker Compose๋กœ MySQL, Redis, Kafka๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

docker-compose up -d

์‹คํ–‰๋˜๋Š” ์„œ๋น„์Šค:

  • MySQL 8.0 (ํฌํŠธ: 3306)
  • Redis 7.0 (ํฌํŠธ: 6379)
  • Apache Kafka 3-broker ํด๋Ÿฌ์Šคํ„ฐ (ํฌํŠธ: 9092, 9093, 9094)
  • Zookeeper (ํฌํŠธ: 2181)

2. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰

./gradlew bootRun
  • local ํ”„๋กœํ•„๋กœ ์‹คํ–‰ (๊ธฐ๋ณธ๊ฐ’)
  • ์ž๋™ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ ๋ฐ ๋ฐ์ดํ„ฐ ์‹œ๋“œ (schema.sql, data.sql)
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํฌํŠธ: 8080

3. Swagger UI ์ ‘์†

http://localhost:8080/swagger-ui.html
  • local ํ”„๋กœํ•„์—์„œ๋งŒ ํ™œ์„ฑํ™”
  • ๋ชจ๋“  API ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ

4. ํ…Œ์ŠคํŠธ ์‹คํ–‰

./gradlew test
  • Testcontainers๋กœ ๊ฒฉ๋ฆฌ๋œ MySQL ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰
  • ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ž๋™ ์‹คํ–‰
  • UserFlowIntegrationTest๋กœ ์ „์ฒด ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ ๊ฒ€์ฆ

๐Ÿ“‚ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

src/
โ”œโ”€โ”€ main
โ”‚   โ”œโ”€โ”€ java/kr/hhplus/be/server
โ”‚   โ”‚   โ”œโ”€โ”€ interfaces/web           # REST Controllers
โ”‚   โ”‚   โ”œโ”€โ”€ application/service      # Application Services (์œ ์ฆˆ์ผ€์ด์Šค)
โ”‚   โ”‚   โ”œโ”€โ”€ domain/model             # Domain Models (๋ถˆ๋ณ€ Records)
โ”‚   โ”‚   โ””โ”€โ”€ infrastructure           # Persistence, Cache, Kafka Adapters
โ”‚   โ”‚       โ”œโ”€โ”€ persistence/adapter  # JPA Adapters
โ”‚   โ”‚       โ”œโ”€โ”€ kafka/               # Kafka Producers/Consumers
โ”‚   โ”‚       โ”œโ”€โ”€ lock/                # Distributed Lock
โ”‚   โ”‚       โ”œโ”€โ”€ outbox/              # Outbox Event Dispatcher
โ”‚   โ”‚       โ””โ”€โ”€ config/              # Configuration Classes
โ”‚   โ””โ”€โ”€ resources
โ”‚       โ”œโ”€โ”€ application.yml          # Spring ์„ค์ •
โ”‚       โ”œโ”€โ”€ schema.sql               # DDL (ํ…Œ์ด๋ธ” ์ƒ์„ฑ)
โ”‚       โ””โ”€โ”€ data.sql                 # ์‹œ๋“œ ๋ฐ์ดํ„ฐ
โ””โ”€โ”€ test
    โ””โ”€โ”€ java/kr/hhplus/be/server
        โ””โ”€โ”€ interfaces/web           # ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (Testcontainers)

๐Ÿ“š ์ถ”๊ฐ€ ๋ฌธ์„œ


๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘

# 1. ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํด๋ก  ๋ฐ Java 17 ์„ค์น˜
git clone <repository-url>
cd ecommerce-platform

# 2. ์ธํ”„๋ผ ์‹คํ–‰ (MySQL, Redis, Kafka)
docker-compose up -d

# 3. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰
./gradlew bootRun

# 4. Swagger UI์—์„œ API ํ…Œ์ŠคํŠธ
open http://localhost:8080/swagger-ui.html

# 5. ํ…Œ์ŠคํŠธ ์‹คํ–‰
./gradlew test

๐Ÿ“ž ๋ฌธ์˜ ๋ฐ ๊ธฐ์—ฌ

์ด์Šˆ๋‚˜ ๊ฐœ์„  ์ œ์•ˆ์€ GitHub Issues๋ฅผ ํ†ตํ•ด ๋“ฑ๋กํ•ด์ฃผ์„ธ์š”.


Built with โค๏ธ using Spring Boot 3, Kafka, Redis, and MySQL

About

๐Ÿšš๐Ÿ›œ ์ด์ปค๋จธ์Šค ํ”Œ๋žซํผ - ๋ฐฑ์—”๋“œ

Resources

Stars

Watchers

Forks

Contributors 2

  •  
  •  

Languages