Early Express 물류 플랫폼의 재고 관리 서비스입니다. 허브별 재고 현황 관리, 재고 예약/해제/출고 확정, 안전 재고 모니터링 등 재고 관련 모든 비즈니스 로직을 담당합니다.
Inventory Service는 마이크로서비스 아키텍처 기반의 재고 관리 시스템으로, 다음 핵심 기능을 제공합니다:
- 재고 입출고 관리: 재입고, 재고 조정, 출고 확정
- 재고 예약 시스템: 주문 시 재고 예약 → 취소 시 해제 → 배송 시 확정
- 안전 재고 모니터링: 안전 재고 이하 감지 및 알림 이벤트 발행
- 허브별 재고 관리: 물류 허브 단위의 재고 추적
- 이벤트 기반 동기화: Product Service와 Kafka 이벤트로 연동
| 구분 | 기술 |
|---|---|
| Framework | Spring Boot 3.5.7 |
| Language | Java 21 |
| Database | PostgreSQL |
| Messaging | Apache Kafka |
| Service Discovery | Netflix Eureka |
| Security | OAuth 2.0 Resource Server (Keycloak) |
| Build Tool | Gradle |
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Inventory Service │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Presentation Layer │
│ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ ProducerController │ │ AdminController │ │ InternalController │ │
│ │ /web/producer │ │ /web/admin │ │ /internal │ │
│ └─────────────────────┘ └─────────────────────┘ └─────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Application Layer │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ InventoryService │ │
│ │ • restock() • reserve() • confirmShipment() │ │
│ │ • adjustInventory() • releaseReservation() • checkAvailability() │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Domain Layer │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ Inventory (Aggregate Root) │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ StockQuantity │ │ StockQuantity │ │ StockQuantity │ │ │
│ │ │ (quantityInHub)│ │ (reserved) │ │ (safetyStock) │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ PostgreSQL │ │ Kafka Producer │ │ Kafka Consumer │ │
│ │ Repository │ │ (6 토픽 발행) │ │ (2 토픽 수신) │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────┐ ┌─────────────────┐
│ Product │ ───── Kafka ─────▶ │ Inventory │
│ Service │ product-created │ Service │
│ │ product-deleted │ │
└─────────────────┘ └─────────────────┘
│
┌──────────────────────────────────────┤
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Product │ ◀───────────────── │ Order │
│ Service │ inventory-low-stock │ Service │
│ │ inventory-restocked │ │
└─────────────────┘ └─────────────────┘
▲
│
inventory-reserved ─────────┤
stock-decreased ────────────┤
stock-restored ─────────────┘
public class Inventory {
// 식별자
private String inventoryId; // 재고 고유 ID
private String productId; // 상품 ID (FK)
private String hubId; // 허브 ID (FK)
// 수량 정보 (Value Objects)
private StockQuantity quantityInHub; // 허브 내 전체 수량
private StockQuantity reservedQuantity; // 예약된 수량 (주문 처리 중)
private StockQuantity safetyStock; // 안전 재고
private StockQuantity reorderPoint; // 재주문 시점
// 위치 및 시간
private String location; // 허브 내 물리적 위치 (형식: A-1-3)
private LocalDateTime lastRestockedAt; // 마지막 입고 시간
// 낙관적 락
private Long version;
// Audit 필드
private LocalDateTime createdAt;
private String createdBy;
private LocalDateTime updatedAt;
private String updatedBy;
private LocalDateTime deletedAt;
private String deletedBy;
private boolean isDeleted;
}불변 객체로 설계되어 수량의 유효성을 보장합니다.
public class StockQuantity {
private Integer value;
// 팩토리 메서드
public static StockQuantity of(Integer value);
public static StockQuantity zero();
// 연산
public StockQuantity increase(Integer amount);
public StockQuantity decrease(Integer amount); // 음수 결과 시 예외
// 비교
public boolean isGreaterThan(StockQuantity other);
public boolean isLessThanOrEqual(StockQuantity other);
public boolean isZero();
}┌─────────────────────────────────────────────────────────────────────┐
│ quantityInHub (전체 수량) │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────────────┐ ┌────────────────────────────┐ │ │
│ │ │ reservedQuantity │ │ availableQuantity │ │ │
│ │ │ (예약된 수량) │ │ (판매 가능 수량) │ │ │
│ │ │ │ │ = 전체 - 예약 │ │ │
│ │ └──────────────────────┘ └────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────┐ │
│ │ safetyStock │ ← 이하로 떨어지면 LowStock 이벤트 발행 │
│ │ (안전 재고) │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
[주문 생성] [주문 취소] [배송 시작]
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ reserve │ │ release │ │ confirm │
│ 예약 │ │ 해제 │ │ 출고확정 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────────┐
│ reservedQty +N │ │ reservedQty -N │ │ reservedQty -N │
│ availableQty-N │ │ availableQty+N │ │ quantityInHub -N │
└────────────────┘ └────────────────┘ └────────────────────┘
Base Path: /v1/inventory/web/producer
| Method | Endpoint | 설명 |
|---|---|---|
POST |
/restock |
재입고 |
GET |
/products/{productId}/inventories |
내 상품의 재고 현황 (전체 허브) |
GET |
/hubs/{hubId}/inventories |
특정 허브의 내 재고 조회 |
PUT |
/inventories/{inventoryId}/adjust |
재고 조정 |
PUT |
/inventories/{inventoryId}/safety-stock |
안전 재고 설정 |
PUT |
/inventories/{inventoryId}/location |
위치 변경 |
GET |
/inventories/{inventoryId} |
재고 상세 조회 |
POST /v1/inventory/web/producer/restock
X-User-Id: seller-001
Content-Type: application/jsonRequest
{
"productId": "prod-001",
"hubId": "hub-seoul-001",
"quantity": 500
}Response (200 OK)
{
"inventoryId": "inv-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"totalQuantity": 1500,
"availableQuantity": 1200,
"reservedQuantity": 300,
"safetyStock": 100,
"location": "A-3-5",
"isOutOfStock": false,
"isBelowSafetyStock": false,
"lastRestockedAt": "2025-01-15T10:30:00",
"createdAt": "2024-12-01T09:00:00",
"updatedAt": "2025-01-15T10:30:00"
}PUT /v1/inventory/web/producer/inventories/{inventoryId}/adjust
X-User-Id: seller-001
Content-Type: application/jsonRequest
{
"adjustmentQuantity": -50,
"reason": "파손으로 인한 재고 차감"
}Response (200 OK)
{
"inventoryId": "inv-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"previousQuantity": 1500,
"adjustmentQuantity": -50,
"currentQuantity": 1450,
"reason": "파손으로 인한 재고 차감"
}Base Path: /v1/inventory/web/admin
| Method | Endpoint | 설명 |
|---|---|---|
GET |
/inventories |
전체 재고 조회 (페이징) |
GET |
/hubs/{hubId}/inventories |
허브별 재고 현황 |
GET |
/out-of-stock |
품절 상품 목록 |
GET |
/low-stock |
안전 재고 이하 상품 목록 |
GET |
/inventories/{inventoryId} |
재고 상세 조회 |
GET |
/products/{productId}/inventories |
상품별 재고 현황 |
GET /v1/inventory/web/admin/out-of-stockResponse (200 OK)
[
{
"inventoryId": "inv-003",
"productId": "prod-003",
"hubId": "hub-busan-001",
"totalQuantity": 0,
"availableQuantity": 0,
"reservedQuantity": 0,
"safetyStock": 50,
"location": "B-2-1",
"isOutOfStock": true,
"isBelowSafetyStock": true,
"lastRestockedAt": "2025-01-10T14:00:00",
"createdAt": "2024-11-01T09:00:00",
"updatedAt": "2025-01-14T16:30:00"
}
]GET /v1/inventory/web/admin/low-stockResponse (200 OK)
[
{
"inventoryId": "inv-002",
"productId": "prod-002",
"hubId": "hub-seoul-001",
"totalQuantity": 80,
"availableQuantity": 30,
"reservedQuantity": 50,
"safetyStock": 100,
"location": "A-1-2",
"isOutOfStock": false,
"isBelowSafetyStock": true,
"lastRestockedAt": "2025-01-08T11:00:00",
"createdAt": "2024-10-15T09:00:00",
"updatedAt": "2025-01-14T09:00:00"
}
]Base Path: /v1/inventory/internal
| Method | Endpoint | 설명 |
|---|---|---|
GET |
/products/{productId}/hubs/{hubId}/availability |
재고 가용성 확인 |
POST |
/products/check-availability |
대량 재고 확인 |
POST |
/reservations |
재고 예약 |
DELETE |
/reservations/{orderId} |
예약 해제 |
POST |
/reservations/{orderId}/confirm |
출고 확정 |
GET |
/products/{productId}/inventories |
상품별 전체 재고 조회 |
POST |
/products/{productId}/initialize |
초기 재고 생성 |
GET |
/inventories/{inventoryId}/exists |
재고 존재 확인 |
GET /v1/inventory/internal/products/{productId}/hubs/{hubId}/availabilityResponse (200 OK)
{
"productId": "prod-001",
"hubId": "hub-seoul-001",
"isAvailable": true,
"availableQuantity": 1200,
"reservedQuantity": 300,
"totalQuantity": 1500
}POST /v1/inventory/internal/products/check-availability
Content-Type: application/jsonRequest
{
"hubId": "hub-seoul-001",
"items": [
{ "productId": "prod-001", "quantity": 10 },
{ "productId": "prod-002", "quantity": 5 },
{ "productId": "prod-003", "quantity": 20 }
]
}Response (200 OK)
{
"hubId": "hub-seoul-001",
"allAvailable": false,
"results": [
{
"productId": "prod-001",
"requiredQuantity": 10,
"availableQuantity": 1200,
"isAvailable": true
},
{
"productId": "prod-002",
"requiredQuantity": 5,
"availableQuantity": 30,
"isAvailable": true
},
{
"productId": "prod-003",
"requiredQuantity": 20,
"availableQuantity": 0,
"isAvailable": false
}
]
}POST /v1/inventory/internal/reservations
Content-Type: application/jsonRequest
{
"orderId": "order-001",
"items": [
{
"productId": "prod-001",
"hubId": "hub-seoul-001",
"quantity": 10
},
{
"productId": "prod-002",
"hubId": "hub-seoul-001",
"quantity": 5
}
]
}Response (200 OK / 206 Partial Content)
{
"reservationId": "res-uuid-001",
"orderId": "order-001",
"allSuccess": true,
"reservedItems": [
{
"productId": "prod-001",
"hubId": "hub-seoul-001",
"quantity": 10,
"success": true,
"errorMessage": null
},
{
"productId": "prod-002",
"hubId": "hub-seoul-001",
"quantity": 5,
"success": true,
"errorMessage": null
}
]
}DELETE /v1/inventory/internal/reservations/{orderId}?productId=prod-001&hubId=hub-seoul-001&quantity=10Response (200 OK)
{
"orderId": "order-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"quantity": 10,
"released": true
}POST /v1/inventory/internal/reservations/{orderId}/confirm?productId=prod-001&hubId=hub-seoul-001&quantity=10Response (200 OK)
{
"orderId": "order-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"quantity": 10,
"confirmed": true
}POST /v1/inventory/internal/products/{productId}/initialize
Content-Type: application/jsonRequest
{
"productId": "prod-001",
"sellerId": "seller-001"
}Response (201 Created)
{
"productId": "prod-001",
"inventories": [
{
"inventoryId": "inv-001",
"hubId": "hub-seoul-001",
"totalQuantity": 0
},
{
"inventoryId": "inv-002",
"hubId": "hub-busan-001",
"totalQuantity": 0
}
]
} Inventory Service
┌────────────────────────────────────┐
│ │
Product Service │ │ Order Service
┌───────────┐ │ ┌────────────────────────┐ │ ┌───────────┐
│ │─────┼───▶│ ProductEventConsumer │ │ │ │
│ product- │ │ │ │ │ │ │
│ created │ │ │ • createInitialInventory() │ │ │
│ │ │ └────────────────────────┘ │ │ │
└───────────┘ │ │ │ │
│ │ │ │
┌───────────┐ │ ┌────────────────────────┐ │ │ │
│ │─────┼───▶│ ProductEventConsumer │ │ │ │
│ product- │ │ │ │ │ │ │
│ deleted │ │ │ • deleteInventoriesByProduct() │ │ │
│ │ │ └────────────────────────┘ │ │ │
└───────────┘ │ │ │ │
│ │ │ │
│ ┌────────────────────────┐ │ │ │
┌───────────┐ │ │ KafkaInventoryEvent │ │ │ │
│ │◀────┼────│ Publisher │─────┼────▶│ inventory │
│ inventory │ │ │ │ │ │ -reserved │
│ -low-stock│ │ │ 6개 토픽 발행 │ │ └───────────┘
└───────────┘ │ └────────────────────────┘ │
│ │ │ ┌───────────┐
┌───────────┐ │ │ │ │ │
│ │◀────┼──────────────┤ ├────▶│ stock- │
│ inventory │ │ │ │ │ decreased │
│ -restocked│ │ │ │ └───────────┘
└───────────┘ │ │ │
│ │ │ ┌───────────┐
│ │ │ │ │
│ └─────────────────────┼────▶│ stock- │
│ │ │ restored │
│ │ └───────────┘
└────────────────────────────────────┘
상품 생성 시 해당 허브에 초기 재고 레코드를 생성합니다.
토픽: product-created
{
"eventId": "evt-uuid-001",
"eventType": "PRODUCT_CREATED",
"source": "product-service",
"timestamp": "2025-01-15T09:00:00",
"productId": "prod-001",
"sellerId": "seller-001",
"hubId": "hub-seoul-001",
"name": "유기농 사과 1kg",
"createdAt": "2025-01-15T09:00:00"
}처리 로직:
inventoryService.createInitialInventory(event.getProductId(), event.getHubId());상품 삭제(단종) 시 해당 상품의 모든 재고를 소프트 삭제합니다.
토픽: product-deleted
{
"eventId": "evt-uuid-002",
"eventType": "PRODUCT_DELETED",
"source": "product-service",
"timestamp": "2025-01-15T10:00:00",
"productId": "prod-001",
"sellerId": "seller-001",
"deletedAt": "2025-01-15T10:00:00"
}처리 로직:
inventoryService.deleteInventoriesByProduct(event.getProductId());재고 레코드가 새로 생성되었을 때 발행됩니다.
토픽: inventory-created
{
"eventId": "evt-uuid-003",
"eventType": "INVENTORY_CREATED",
"source": "inventory-service",
"timestamp": "2025-01-15T09:00:05",
"inventoryId": "inv-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"quantity": 0,
"createdAt": "2025-01-15T09:00:05"
}가용 재고가 안전 재고 이하로 떨어졌을 때 발행됩니다.
토픽: inventory-low-stock
{
"eventId": "evt-uuid-004",
"eventType": "INVENTORY_LOW_STOCK",
"source": "inventory-service",
"timestamp": "2025-01-15T14:30:00",
"inventoryId": "inv-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"currentQuantity": 45,
"safetyStock": 100,
"detectedAt": "2025-01-15T14:30:00"
}Product Service 처리: markAsOutOfStock() 호출 (상품 품절 상태로 변경)
재입고가 완료되었을 때 발행됩니다.
토픽: inventory-restocked
{
"eventId": "evt-uuid-005",
"eventType": "INVENTORY_RESTOCKED",
"source": "inventory-service",
"timestamp": "2025-01-15T10:30:00",
"inventoryId": "inv-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"restockedQuantity": 500,
"currentQuantity": 1500,
"restockedAt": "2025-01-15T10:30:00"
}Product Service 처리: restoreFromOutOfStock() 호출 (품절 해제)
주문에 대한 재고 예약이 완료되었을 때 발행됩니다.
토픽: inventory-reserved
메시지 키: orderId (Order Service 파티셔닝)
{
"eventId": "evt-uuid-006",
"eventType": "INVENTORY_RESERVED",
"source": "inventory-service",
"timestamp": "2025-01-15T11:00:00",
"inventoryId": "inv-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"orderId": "order-001",
"reservedQuantity": 10,
"availableQuantity": 1190,
"reservedAt": "2025-01-15T11:00:00"
}출고 확정으로 실제 재고가 차감되었을 때 발행됩니다.
토픽: stock-decreased
메시지 키: orderId
{
"eventId": "evt-uuid-007",
"eventType": "STOCK_DECREASED",
"source": "inventory-service",
"timestamp": "2025-01-15T12:00:00",
"inventoryId": "inv-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"orderId": "order-001",
"decreasedQuantity": 10,
"remainingQuantity": 1490,
"decreasedAt": "2025-01-15T12:00:00"
}주문 취소로 예약된 재고가 복원되었을 때 발행됩니다.
토픽: stock-restored
메시지 키: orderId
{
"eventId": "evt-uuid-008",
"eventType": "STOCK_RESTORED",
"source": "inventory-service",
"timestamp": "2025-01-15T13:00:00",
"inventoryId": "inv-001",
"productId": "prod-001",
"hubId": "hub-seoul-001",
"orderId": "order-001",
"restoredQuantity": 10,
"currentQuantity": 1200,
"restoredAt": "2025-01-15T13:00:00"
}# 서버 설정
APP_PORT=4015
# 데이터베이스
SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/inventory_db
SPRING_DATASOURCE_USERNAME=postgres
SPRING_DATASOURCE_PASSWORD=password
# Eureka
EUREKA_DEFAULT_ZONE=http://localhost:8761/eureka/
# Kafka
KAFKA_BOOTSTRAP_SERVERS=localhost:9092
# Keycloak
KEYCLOAK_ISSUER_URI=http://localhost:8080/realms/early-express
# Kafka Topics
KAFKA_TOPIC_PRODUCT_CREATED=product-created
KAFKA_TOPIC_PRODUCT_DELETED=product-deleted
KAFKA_TOPIC_INVENTORY_CREATED=inventory-created
KAFKA_TOPIC_INVENTORY_LOW_STOCK=inventory-low-stock
KAFKA_TOPIC_INVENTORY_RESTOCKED=inventory-restocked
KAFKA_TOPIC_INVENTORY_RESERVED=inventory-reserved
KAFKA_TOPIC_STOCK_DECREASED=stock-decreased
KAFKA_TOPIC_STOCK_RESTORED=stock-restoredspring:
application:
name: inventory-service
kafka:
consumer:
group-id: inventory-service-group
enable-auto-commit: false # 수동 ACK
auto-offset-reset: earliest
producer:
acks: all
eureka:
client:
service-url:
defaultZone: ${EUREKA_DEFAULT_ZONE}- Java 21
- PostgreSQL 15+
- Apache Kafka
- Eureka Server
- Keycloak
# 1. 데이터베이스 생성
createdb inventory_db
# 2. 환경 변수 설정
export $(cat .env | xargs)
# 3. 애플리케이션 실행
./gradlew bootRundocker-compose up -d inventory-servicecurl http://localhost:4015/actuator/healthsrc/main/java/com/early_express/inventory_service/
├── domain/inventory/
│ ├── application/
│ │ ├── dto/
│ │ │ ├── command/ # RestockCommand, AdjustCommand, ReservationCommand 등
│ │ │ └── result/ # AvailabilityInfo, ReservationInfo 등
│ │ └── service/
│ │ └── InventoryService.java
│ │
│ ├── domain/
│ │ ├── exception/
│ │ │ ├── InventoryErrorCode.java
│ │ │ └── InventoryException.java
│ │ ├── messaging/
│ │ │ ├── InventoryEventPublisher.java # 도메인 인터페이스 (Port)
│ │ │ └── dto/ # EventData DTOs
│ │ │ ├── InventoryCreatedEventData.java
│ │ │ ├── InventoryLowStockEventData.java
│ │ │ ├── InventoryRestockedEventData.java
│ │ │ ├── InventoryReservedEventData.java
│ │ │ ├── StockDecreasedEventData.java
│ │ │ └── StockRestoredEventData.java
│ │ ├── model/
│ │ │ ├── Inventory.java # Aggregate Root
│ │ │ └── vo/
│ │ │ └── StockQuantity.java # Value Object
│ │ └── repository/
│ │ └── InventoryRepository.java
│ │
│ ├── infrastructure/
│ │ ├── messaging/
│ │ │ ├── inventory/
│ │ │ │ ├── event/ # Kafka 이벤트 클래스
│ │ │ │ │ ├── InventoryCreatedEvent.java
│ │ │ │ │ ├── InventoryLowStockEvent.java
│ │ │ │ │ ├── InventoryRestockedEvent.java
│ │ │ │ │ ├── InventoryReservedEvent.java
│ │ │ │ │ ├── StockDecreasedEvent.java
│ │ │ │ │ └── StockRestoredEvent.java
│ │ │ │ └── producer/
│ │ │ │ └── KafkaInventoryEventPublisher.java # Adapter
│ │ │ └── product/
│ │ │ ├── consumer/
│ │ │ │ └── ProductEventConsumer.java
│ │ │ └── event/
│ │ │ ├── ProductCreatedEvent.java
│ │ │ └── ProductDeletedEvent.java
│ │ └── persistence/
│ │ ├── InventoryEntity.java
│ │ ├── InventoryJpaRepository.java
│ │ └── InventoryRepositoryImpl.java
│ │
│ └── presentation/
│ ├── internal/
│ │ ├── InternalInventoryController.java
│ │ └── dto/
│ │ ├── request/
│ │ │ ├── CheckAvailabilityRequest.java
│ │ │ ├── InitializeInventoryRequest.java
│ │ │ └── ReserveStockRequest.java
│ │ └── response/
│ │ ├── AvailabilityResponse.java
│ │ ├── BulkAvailabilityResponse.java
│ │ ├── ConfirmResponse.java
│ │ ├── ExistsResponse.java
│ │ ├── InitializeInventoryResponse.java
│ │ ├── InternalInventoryResponse.java
│ │ ├── ReleaseResponse.java
│ │ └── ReservationResponse.java
│ └── web/
│ ├── AdminInventoryController.java
│ ├── ProducerInventoryController.java
│ └── dto/
│ ├── request/
│ │ ├── AdjustInventoryRequest.java
│ │ ├── RestockRequest.java
│ │ ├── UpdateLocationRequest.java
│ │ └── UpdateSafetyStockRequest.java
│ └── response/
│ ├── AdjustmentResponse.java
│ └── InventoryResponse.java
│
└── global/
├── common/
│ └── utils/
│ └── PageUtils.java
├── infrastructure/
│ └── event/
│ └── base/
│ └── BaseEvent.java
└── presentation/
└── dto/
└── PageResponse.java
- OAuth 2.0 Resource Server: Keycloak JWT 토큰 검증
- 역할 기반 접근 제어:
- Producer API:
X-User-Id헤더로 판매자 식별 - Admin API: ADMIN 권한 필요 (TODO)
- Internal API: 서비스 간 통신 (인증 우회 또는 서비스 토큰)
- Producer API:
- 낙관적 락:
@Version필드로 동시성 제어
- Actuator:
/actuator/health,/actuator/info - Zipkin: 분산 추적
- Loki: 로그 수집
- Prometheus Pushgateway: 메트릭 수집
| 서비스 | 연동 방식 | 역할 |
|---|---|---|
| Product Service | Kafka (양방향) | 상품 생성/삭제 이벤트 수신, 재고 상태 이벤트 발행 |
| Order Service | Internal API + Kafka | 재고 예약/해제/확정 API, 재고 이벤트 발행 |
| Hub Service | 참조 | 허브 ID 참조 |
- Admin 권한 검증 구현
- 재고 이동 (허브 간) 기능:
POST /admin/inventories/transfer - 재고 이력 조회 기능
- 대량 재입고 API
- 재고 알림 설정 (이메일/Slack)