Early Express νλ«νΌμ μν κ΄λ¦¬λ₯Ό λ΄λΉνλ λ§μ΄ν¬λ‘μλΉμ€
Product Serviceλ μνμ λ±λ‘, μμ , μμ , μν κ΄λ¦¬ λ° μ¬κ³ μ°λμ λ΄λΉν©λλ€.
DDD(Domain-Driven Design) μν€ν
μ²λ₯Ό κΈ°λ°μΌλ‘ μ€κ³λμμΌλ©°, Inventory Serviceμμ μ΄λ²€νΈ κΈ°λ° ν΅μ μ ν΅ν΄ μ¬κ³ μνλ₯Ό λκΈ°νν©λλ€.
ꡬλΆ
κΈ°μ
Framework
Spring Boot 3.5.7, Spring Cloud 2025.0.0
Language
Java 21
Database
PostgreSQL + pgvector
ORM
Spring Data JPA, QueryDSL 5.1.0
Message Queue
Apache Kafka (Spring Cloud Stream)
Service Discovery
Netflix Eureka Client
Config
Spring Cloud Config
Security
Spring Security, OAuth 2.0 (Keycloak)
Service Communication
OpenFeign (User Service μ°λ)
Observability
Micrometer, Zipkin, Loki, Prometheus
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Product Service β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Presentation Layer β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β
β β AllProduct β β ProducerProduct β β InternalProduct β β
β β Controller β β Controller β β Controller β β
β β (κ³΅κ° μ‘°ν) β β (μμ°μ
체 μ μ©) β β (μλΉμ€ κ° ν΅μ ) β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Application Layer β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β ProductService (λΉμ¦λμ€ λ‘μ§ μ‘°μ¨) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Domain Layer β
β βββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ β
β β Product (AR) β β Value Objects β β
β β - ProductStatus β β - Price β β
β β - μν λ‘μ§ β β β β
β βββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Infrastructure Layer β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββ β
β β JPA Entity β β Repository β β UserServiceClient β β
β β ProductEntityβ β Impl β β (Feign) β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
μνμ μ 체 μλͺ
μ£ΌκΈ°λ₯Ό κ΄λ¦¬νλ ν΅μ¬ λλ©μΈ λͺ¨λΈμ
λλ€.
Product
βββ productId (μλ³μ)
βββ sellerId (νλ§€μ ID)
βββ companyId (μ
체 ID)
βββ name (μνλͺ
)
βββ description (μν μ€λͺ
)
βββ Price (κ°κ²© VO)
β βββ amount (κΈμ‘)
βββ ProductStatus (μν)
βββ isSellable (νλ§€ κ°λ₯ μ¬λΆ)
βββ hasEvent (μ΄λ²€νΈ μ μ© μ¬λΆ)
βββ minOrderQuantity (μ΅μ μ£Όλ¬Έ μλ)
βββ maxOrderQuantity (μ΅λ μ£Όλ¬Έ μλ)
βββ Audit Fields
βββ createdAt / createdBy
βββ updatedAt / updatedBy
βββ deletedAt / deletedBy / isDeleted
μν μν νλ¦ (ProductStatus)
ββββββββββββββββββββ
β DRAFT β μμ μ μ₯
ββββββββββ¬ββββββββββ
β activate()
βΌ
ββββββββββββββββββββ
βββββββ ACTIVE βββββββ
β β (νλ§€ μ€) β β
β ββββββββββ¬ββββββββββ β
β β β
suspend()β β βInventoryLowStockEvent
β β β (from Inventory Service)
βΌ β βΌ
βββββββββββββββ β βββββββββββββββββββ
β SUSPENDED β β β OUT_OF_STOCK β
β (νλ§€ μ€μ§) ββββββββββββ€ β (νμ ) β
βββββββββββββββ β ββββββββββ¬βββββββββ
β activate() β β
ββββββββββββββββ€ βInventoryRestockedEvent
β β (from Inventory Service)
βΌ β
ββββββββββββββββββββ β
β DISCONTINUED ββββββ (νμ ν΄μ μ ACTIVEλ‘)
β (λ¨μ’
) β
ββββββββββββββββββββ
Public API (λͺ¨λ μ¬μ©μ)
Method
Endpoint
μ€λͺ
GET
/v1/product/web/all/products
μν λͺ©λ‘ μ‘°ν (νμ΄μ§)
GET
/v1/product/web/all/products/{productId}
μν μμΈ μ‘°ν
GET
/v1/product/web/all/products/search
μν κ²μ (ν€μλ)
Producer API (μμ°μ
체 μ μ©)
Method
Endpoint
μ€λͺ
POST
/v1/product/web/producer/products
μν λ±λ‘
PUT
/v1/product/web/producer/products/{productId}
μν μμ
DELETE
/v1/product/web/producer/products/{productId}
μν μμ (λ¨μ’
)
GET
/v1/product/web/producer/products
λ΄ μν λͺ©λ‘ μ‘°ν
PUT
/v1/product/web/producer/products/{productId}/activate
μν νμ±ν
PUT
/v1/product/web/producer/products/{productId}/suspend
μν μΌμμ€μ§
Internal API (μλΉμ€ κ° ν΅μ )
Method
Endpoint
μ€λͺ
GET
/v1/product/internal/products/{productId}/validate
μν μ‘΄μ¬ νμΈ
GET
/v1/product/internal/products/{productId}
μν μ 보 μ‘°ν
POST
/v1/product/internal/products/validate-bulk
λλ μν κ²μ¦
GET
/v1/product/internal/sellers/{sellerId}/products
νλ§€μλ³ μν λͺ©λ‘
μν λ±λ‘ μμ² μμ
POST /v1/product/web/producer/products
X-User-Id: seller-uuid
{
"hubId" : " hub-uuid" ,
"companyId" : " company-uuid" ,
"name" : " ν리미μ λ
ΈνΈλΆ" ,
"description" : " κ³ μ±λ₯ λΉμ¦λμ€ λ
ΈνΈλΆ" ,
"price" : 1500000 ,
"minOrderQuantity" : 1 ,
"maxOrderQuantity" : 10
}
{
"productId" : " product-uuid" ,
"sellerId" : " seller-uuid" ,
"name" : " ν리미μ λ
ΈνΈλΆ" ,
"description" : " κ³ μ±λ₯ λΉμ¦λμ€ λ
ΈνΈλΆ" ,
"price" : 1500000 ,
"status" : " DRAFT" ,
"isSellable" : false ,
"hasEvent" : false ,
"minOrderQuantity" : 1 ,
"maxOrderQuantity" : 10 ,
"createdAt" : " 2025-01-15T10:30:00" ,
"updatedAt" : null
}
λλ μν κ²μ¦ μμ²/μλ΅
POST /v1/product/internal/products/validate-bulk
{
"productIds" : [" product-1" , " product-2" , " product-3" ]
}
{
"allValid" : false ,
"validProductIds" : [" product-1" , " product-2" ],
"invalidProductIds" : [" product-3" ],
"errors" : {
"product-3" : " μνμ μ°Ύμ μ μμ΅λλ€."
}
}
# Application
APP_PORT=4012
APP_NAME=product-service
APP_PROFILE=dev
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=default_db
DB_USERNAME=postgres
DB_PASSWORD=postgres123!
# Eureka
EUREKA_DEFAULT_ZONE=https://www.pinjun.xyz/eureka1/eureka/,https://www.pinjun.xyz/eureka2/eureka/
EUREKA_INSTANCE_HOSTNAME=192.168.0.42
# Config Server
CONFIG_SERVER_URL=https://www.pinjun.xyz/config
# Kafka
KAFKA_BOOTSTRAP_SERVERS=61.254.69.188:9092,61.254.69.188:9093,61.254.69.188:9094
KAFKA_CONSUMER_GROUP_ID=product-service-group
# Keycloak (OAuth 2.0)
KEYCLOAK_ISSUER_URI=https://www.pinjun.xyz/keycloak/realms/codefactory
KEYCLOAK_CLIENT_ID=user
KEYCLOAK_CLIENT_SECRET=user-password
# User Service (Feign)
USER_SERVICE_URL=http://user-service:8081
# Observability
ZIPKIN_ENABLED=true
ZIPKIN_BASE_URL=https://www.pinjun.xyz/zipkin
LOKI_ENABLED=true
LOKI_URL=https://www.pinjun.xyz/loki/api/v1/push
PROMETHEUS_PUSHGATEWAY_ENABLED=true
PROMETHEUS_PUSHGATEWAY_URL=https://www.pinjun.xyz/prometheus/pushgateway
# 1. νκ²½ λ³μ μ€μ
cp .env.example .env
# .env νμΌ μμ
# 2. Gradle λΉλ
./gradlew clean build
# 3. μ ν리μΌμ΄μ
μ€ν
./gradlew bootRun
# λλ JAR μ§μ μ€ν
java -jar build/libs/product-service-0.0.1-SNAPSHOT.jar
docker build -t product-service .
docker run -p 4012:4012 --env-file .env product-service
Product Serviceλ ν ν½ λΆλ¦¬ ν¨ν΄ μ μ¬μ©νμ¬ μ΄λ²€νΈλ₯Ό λ°ν/μμ ν©λλ€.
βββββββββββββββββββ βββββββββββββββββββββ
β Product Service β β Inventory Service β
ββββββββββ¬βββββββββ βββββββββββ¬ββββββββββ
β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Topic: product-created β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ProductCreatedEvent β β
ββββββββββββββββββββββββΌβββββββββββββββββββββββββΆβ
β β β μ¬κ³ μ΄κΈ°ν
β β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Topic: product-deleted β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ProductDeletedEvent β β
ββββββββββββββββββββββββΌβββββββββββββββββββββββββΆβ
β β β μ¬κ³ λΉνμ±ν
β β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Topic: inventory-low-stock β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β β
ββββββββββββββββββββββββΌββββββββββββββββββββββββββ
βInventoryLowStockEventβ β
β (νμ μ²λ¦¬) β β
β β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β Topic: inventory-restocked β β
β βββββββββββββββββββββββββββββββββββββββββββ β
β β β
ββββββββββββββββββββββββΌββββββββββββββββββββββββββ
βInventoryRestockedEvt β β
β (νμ ν΄μ ) β β
λ°ν μ΄λ²€νΈ (Publisher)
Topic
Event
μ€λͺ
λ°ν μμ
product-created
ProductCreatedEvent
μν μμ±
μν λ±λ‘ μ
product-updated
ProductUpdatedEvent
μν μμ
μν μ 보 λ³κ²½ μ
product-deleted
ProductDeletedEvent
μν μμ (λ¨μ’
)
μν λ¨μ’
μ
product-status-changed
ProductStatusChangedEvent
μν λ³κ²½
μν μν μ μ΄ μ
// ProductCreatedEvent μμ
{
"eventId" : " uuid" ,
"eventType" : " PRODUCT_CREATED" ,
"source" : " product-service" ,
"productId" : " product-uuid" ,
"sellerId" : " seller-uuid" ,
"hubId" : " hub-uuid" ,
"name" : " ν리미μ λ
ΈνΈλΆ" ,
"createdAt" : " 2025-01-15T10:30:00"
}
// ProductDeletedEvent μμ
{
"eventId" : " uuid" ,
"eventType" : " PRODUCT_DELETED" ,
"source" : " product-service" ,
"productId" : " product-uuid" ,
"sellerId" : " seller-uuid" ,
"deletedAt" : " 2025-01-15T15:00:00"
}
// ProductStatusChangedEvent μμ
{
"eventId" : " uuid" ,
"eventType" : " PRODUCT_STATUS_CHANGED" ,
"source" : " product-service" ,
"productId" : " product-uuid" ,
"oldStatus" : " DRAFT" ,
"newStatus" : " ACTIVE" ,
"changedAt" : " 2025-01-15T11:00:00"
}
μμ μ΄λ²€νΈ (Consumer)
Topic
Event
μ€λͺ
μ²λ¦¬
inventory-low-stock
InventoryLowStockEvent
μ¬κ³ λΆμ‘± μλ¦Ό
ProductService.markAsOutOfStock()
inventory-restocked
InventoryRestockedEvent
μ¬μ
κ³ μλ¦Ό
ProductService.restoreFromOutOfStock()
// InventoryLowStockEvent μμ
{
"eventId" : " uuid" ,
"eventType" : " INVENTORY_LOW_STOCK" ,
"source" : " inventory-service" ,
"inventoryId" : " inventory-uuid" ,
"productId" : " product-uuid" ,
"hubId" : " hub-uuid" ,
"currentQuantity" : 5 ,
"safetyStock" : 10 ,
"detectedAt" : " 2025-01-15T14:00:00"
}
// InventoryRestockedEvent μμ
{
"eventId" : " uuid" ,
"eventType" : " INVENTORY_RESTOCKED" ,
"source" : " inventory-service" ,
"inventoryId" : " inventory-uuid" ,
"productId" : " product-uuid" ,
"hubId" : " hub-uuid" ,
"restockedQuantity" : 100 ,
"currentQuantity" : 105 ,
"restockedAt" : " 2025-01-15T16:00:00"
}
spring :
kafka :
topic :
# λ°ν ν ν½
product-created : product-created
product-updated : product-updated
product-deleted : product-deleted
product-status-changed : product-status-changed
# μμ ν ν½
inventory-low-stock : inventory-low-stock
inventory-restocked : inventory-restocked
consumer :
group-id : product-service-group
enable-auto-commit : false # μλ ACK
User Service (Feign Client)
@ FeignClient (name = "user-service" )
public interface UserServiceClient {
@ GetMapping ("/api/v1/users/{userId}" )
UserInfoResponse getUserInfo (@ PathVariable String userId );
}
// UserInfoResponse
{
"userId" : " user-uuid" ,
"username" : " seller01" ,
"hubId" : " hub-uuid" ,
"companyId" : " company-uuid" ,
"role" : " SELLER"
}
OAuth 2.0 Resource Server (Keycloak μ°λ)
X-User-Id ν€λλ₯Ό ν΅ν μ¬μ©μ μλ³
Producer APIλ νλ§€μ κΆν κ²μ¦
Internal APIλ μλΉμ€ κ° ν΅μ μ μ© (Gateway λ―Έλ
ΈμΆ)
λꡬ
μ©λ
μλν¬μΈνΈ
Actuator
ν¬μ€μ²΄ν¬/λ©νΈλ¦
/actuator/health, /actuator/prometheus
Zipkin
λΆμ° μΆμ
Push to Zipkin Server
Loki
λ‘κ·Έ μμ§
Push via Logback Appender
Prometheus
λ©νΈλ¦ μμ§
Push to Pushgateway
src/main/java/com/early_express/product_service/
βββ domain/product/
β βββ application/
β β βββ service/
β β βββ ProductService.java
β βββ domain/
β β βββ model/
β β β βββ Product.java (Aggregate Root)
β β β βββ vo/
β β β βββ Price.java
β β β βββ ProductStatus.java
β β βββ exception/
β β βββ messaging/
β β β βββ ProductEventPublisher.java (Interface)
β β β βββ dto/
β β β βββ ProductCreatedEventData.java
β β β βββ ProductUpdatedEventData.java
β β β βββ ProductDeletedEventData.java
β β β βββ ProductStatusChangedEventData.java
β β βββ repository/
β βββ infrastructure/
β β βββ client/user/
β β β βββ UserServiceClient.java
β β β βββ dto/
β β β βββ UserInfoResponse.java
β β βββ messaging/
β β β βββ inventory/
β β β β βββ consumer/
β β β β β βββ InventoryEventConsumer.java
β β β β βββ event/
β β β β βββ InventoryLowStockEvent.java
β β β β βββ InventoryRestockedEvent.java
β β β βββ product/
β β β βββ producer/
β β β β βββ KafkaProductEventPublisher.java
β β β βββ event/
β β β βββ ProductCreatedEvent.java
β β β βββ ProductUpdatedEvent.java
β β β βββ ProductDeletedEvent.java
β β β βββ ProductStatusChangedEvent.java
β β βββ persistence/
β β βββ entity/
β β βββ ProductEntity.java
β βββ presentation/
β βββ web/
β β βββ AllProductController.java
β β βββ ProducerProductController.java
β β βββ dto/
β β βββ request/
β β β βββ CreateProductRequest.java
β β β βββ UpdateProductRequest.java
β β βββ response/
β β βββ ProductResponse.java
β βββ internal/
β βββ InternalProductController.java
β βββ dto/
β βββ request/
β β βββ ValidateProductsRequest.java
β βββ response/
β βββ InternalProductResponse.java
β βββ ProductValidationResponse.java
βββ global/
βββ common/
βββ config/
βββ infrastructure/
β βββ event/base/
β βββ BaseEvent.java
βββ presentation/
βββ dto/
βββ PageResponse.java