Kotlin + Spring Boot ๊ธฐ๋ฐ์ ์ด๋ฒคํธ ๋๋ฆฌ๋ธ ๋ถ์ฐ ์์คํ ํฌํธํด๋ฆฌ์ค ํ๋ก์ ํธ
- ํ๋ก์ ํธ ๊ฐ์
- ๊ธฐ์ ์คํ
- ์์คํ ์ํคํ ์ฒ
- ํ๋ก์ ํธ ๊ตฌ์กฐ
- ํต์ฌ ๊ธฐ๋ฅ
- ์์ํ๊ธฐ
- ์๋น์ค ์๊ฐ
- ์ฃผ์ ์์ ๋ด์ฉ
์ด ํ๋ก์ ํธ๋ ๋ถ์ฐ ์์คํ ์ ํต์ฌ ๊ณผ์ (ํธ๋์ญ์ , ๋ฐ์ดํฐ ์ ํฉ์ฑ, ์ฅ์ ์ฒ๋ฆฌ)๋ฅผ ํ์ตํ๊ณ ์คํํ๊ธฐ ์ํด ๋ง๋ ํฌํธํด๋ฆฌ์ค์ ๋๋ค.
์๋ฒฝํ ์ ํ์ ๋ง๋๋ ๊ฒ๋ณด๋ค, ๋ถ์ฐ ํ๊ฒฝ์์ ๋ฐ์ํ๋ ์ค์ ๋ฌธ์ ๋ค์ ์ง์ ๊ฒฝํํ๊ณ ํด๊ฒฐํ๋ ๊ณผ์ ์ ์ง์คํ์ต๋๋ค.
-
์ด๋ฒคํธ ๋๋ฆฌ๋ธ ์ํคํ ์ฒ ๊ตฌํ
- Kafka๋ฅผ ํ์ฉํ ์๋น์ค ๊ฐ ๋น๋๊ธฐ ํต์
- CloudEvents ํ์ค ์ค์
- Outbox ํจํด + CDC๋ฅผ ํตํ ์ด๋ฒคํธ-ํธ๋์ญ์ ์์์ฑ ๋ณด์ฅ
-
๋ถ์ฐ ํธ๋์ญ์ ์ฒ๋ฆฌ
- Saga ํจํด์ ํ์ฉํ ์ฃผ๋ฌธ ํ๋ก์ธ์ค ๊ตฌํ
- ๋ณด์ ํธ๋์ญ์ (Compensating Transaction) ์ฒ๋ฆฌ
- ์ด๋ฒคํธ ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ
-
์ฑ๋ฅ ๋ฐ ํ์ฅ์ฑ
- Redis๋ฅผ ํ์ฉํ ์ฌ๊ณ ๊ด๋ฆฌ ์ฑ๋ฅ ์ต์ ํ
- Kubernetes ํ๊ฒฝ์์์ ์ํ ํ์ฅ
- k6 ๊ธฐ๋ฐ ๋ถํ ํ ์คํธ ๋ฐ ์ฑ๋ฅ ์ธก์
-
๊ด์ธก์ฑ(Observability)
- Prometheus + Grafana๋ฅผ ํตํ ๋ฉํธ๋ฆญ ์์ง ๋ฐ ์๊ฐํ
- ๋ถ์ฐ ์ถ์ (correlationId, causationId)
- ๋ถํ ํ ์คํธ ๊ฒฐ๊ณผ ๋ถ์
- Language: Kotlin 1.9.25, Java 21
- Framework: Spring Boot 3.5.8, Spring Cloud 2025.0.0
- Build: Gradle (Kotlin DSL), Multi-module
- Database: MariaDB (JPA, QueryDSL)
- Cache: Redis (AOF persistence)
- Migration: JPA DDL Auto (๊ฐ๋ฐ), Flyway reference
- Message Broker: Apache Kafka (KRaft mode)
- CDC: Debezium (Outbox pattern)
- Event Spec: CloudEvents 1.0
- Container: Docker, Docker Compose
- Orchestration: Kubernetes (k3s/k3d)
- Monitoring: Prometheus, Grafana, Node Exporter
- Load Testing: k6 (JavaScript)
- Formatting: Spotless (ktlint 1.5.0)
- Architecture: Clean Architecture
- Testing: JUnit 5, Testcontainers
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ API Gateway โ
โ (Kubernetes Ingress) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโผโโโโโ โโโโโโผโโโโโ โโโโโโผโโโโโ
โ auth โ โ user โ โ catalog โ
โ service โ โ service โ โ service โ
โ :8089 โ โ :8081 โ โ :8084 โ
โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโผโโโโโ โโโโโโผโโโโโ โโโโโโผโโโโโ
โ order โ โinventoryโ โ payment โ
โ service โ โ service โ โ service โ
โ :8085 โ โ :8083 โ โ :8087 โ
โโโโโโฌโโโโโ โโโโโโฌโโโโโ โโโโโโฌโโโโโ
โ โ โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโ
โ Kafka Cluster โ
โ (KRaft mode) โ
โ :9092 โ
โโโโโโโโโโฌโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโ
โ Debezium โ
โ (CDC Outbox) โ
โโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Infrastructure Layer โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ MariaDB โ Redis โ Prometheus โ Grafana โ
โ :3306 โ :6379 โ :9090 โ :3000 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ api (Presentation) โ
โ - Controllers, DTOs, Exception Handlers โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ depends on
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ application (Use Cases) โ
โ - Commands, Results, Ports (interfaces) โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ depends on
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ domain (Entities) โ
โ - Entities, Value Objects, Domain Events โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โฒ
โ implemented by
โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ infra (Adapters & Ports) โ
โ - Repositories, Kafka, Redis, External APIs โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
mono/
โโโ common/ # ๊ณตํต ๋ชจ๋
โ โโโ common-core/ # ์๋ต ๋ชจ๋ธ, ์์ธ, ์ด๋ฒคํธ ์คํค๋ง
โ โโโ common-security/ # JWT ์ธ์ฆ, ํํฐ
โ โโโ common-observability/ # ๋ก๊น
, MDC
โ
โโโ services/ # ๋ง์ดํฌ๋ก์๋น์ค
โ โโโ auth-service/ # JWT ํ ํฐ ๋ฐํ
โ โโโ user-service/ # ์ฌ์ฉ์ ๊ด๋ฆฌ
โ โโโ catalog-service/ # ์ํ/์นดํ
๊ณ ๋ฆฌ ๊ด๋ฆฌ
โ โโโ inventory-service/ # ์ฌ๊ณ ๊ด๋ฆฌ (Redis + MariaDB)
โ โโโ order-service/ # ์ฃผ๋ฌธ ์ฒ๋ฆฌ (Saga ํจํด)
โ โโโ payment-service/ # ๊ฒฐ์ ์ฒ๋ฆฌ (Toss Payments)
โ
โโโ infra/ # ์ธํ๋ผ ์ฝ๋
โ โโโ docker-compose.yaml # ๋ก์ปฌ ์ธํ๋ผ (DB, Redis, Kafka)
โ โโโ k8s/ # Kubernetes ๋งค๋ํ์คํธ
โ โโโ config/ # ์ค์ ํ์ผ (Redis, Prometheus, Grafana)
โ โโโ makefiles/ # Makefile ๋ชจ๋
โ
โโโ load-test/ # k6 ๋ถํ ํ
์คํธ
โโโ scripts/ # ํ
์คํธ ์๋๋ฆฌ์ค
โโโ monitoring/ # k6 Grafana ๋์๋ณด๋
Outbox ํจํด + CDC๋ฅผ ํตํ ์ ๋ขฐ์ฑ ์๋ ์ด๋ฒคํธ ๋ฐํ
@UseCase
class CreateOrderUseCase(
private val orderRepository: OrderRepository,
private val outboxRepository: OrderOutboxRepository,
) {
@Transactional
fun execute(command: CreateOrderCommand): CreateOrderResult {
val order = orderRepository.save(order)
// Outbox ํ
์ด๋ธ์ ์ ์ฅ (๊ฐ์ ํธ๋์ญ์
)
outboxRepository.save(OrderOutboxEntry(
aggregateId = order.id.toString(),
eventType = "order.placed",
payload = objectMapper.writeValueAsString(cloudEvent),
topic = "order.placed",
))
return CreateOrderResult(order.id!!)
}
// Debezium CDC๊ฐ Outbox ํ
์ด๋ธ์ ๊ฐ์ํ๊ณ Kafka๋ก ๋ฐํ
}์ด๋ฒคํธ ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ
@Component
class KafkaOrderPlacedConsumer(
private val useCase: ReserveStockUseCase,
private val idempotencyChecker: IdempotencyChecker,
) {
@KafkaListener(topics = ["order.placed"])
fun onOrderPlaced(event: CloudEvent<OrderPlacedEvent>, ack: Acknowledgment) {
// 1. ๋ฉฑ๋ฑ์ฑ ์ฒดํฌ (Fast-path)
if (idempotencyChecker.isDuplicate(event.id, "RESERVE_STOCK")) {
ack.acknowledge()
return
}
// 2. UseCase ์คํ
useCase.execute(command, context)
// 3. ๋ฉฑ๋ฑ์ฑ ๊ธฐ๋ก ์ ์ฅ
idempotencyRepository.save(
InventoryEventIdempotency(event.id, "RESERVE_STOCK", referenceId)
)
ack.acknowledge()
}
}์ฃผ๋ฌธ ์์ฑ โ ์ฌ๊ณ ์์ฝ โ ๊ฒฐ์ โ ์ฃผ๋ฌธ ํ์ ํ๋ฆ์ Saga ํจํด์ผ๋ก ๊ตฌํ:
์ฃผ๋ฌธ ์์ฑ (ORDER_CREATED)
โ
์ฌ๊ณ ์์ฝ (order.placed โ inventory.reserved)
โ
๊ฒฐ์ ์์ฑ (inventory.confirmed โ payment.created)
โ
๊ฒฐ์ ์๋ฃ (payment.completed โ order.confirmed)
[๋ณด์ ํธ๋์ญ์
]
๊ฒฐ์ ์คํจ โ ์ฌ๊ณ ๋ณต๊ตฌ โ ์ฃผ๋ฌธ ์ทจ์
// Lua ์คํฌ๋ฆฝํธ๋ก ์์์ ์ฌ๊ณ ์ฐจ๊ฐ
val script = """
local available = redis.call('HGET', KEYS[1], 'availableQuantity')
if tonumber(available) >= tonumber(ARGV[1]) then
redis.call('HINCRBY', KEYS[1], 'availableQuantity', -ARGV[1])
redis.call('HINCRBY', KEYS[1], 'reservedQuantity', ARGV[1])
return 1
else
return 0
end
""".trimIndent()
redisTemplate.execute(script, keys, args)# k6 ์คํฌ๋ฆฝํธ๋ก ์ฑ๋ฅ ์ธก์
k6 run --out experimental-prometheus-rw scripts/inventory/baseline.test.js
# Grafana์์ ์ค์๊ฐ ๋ฉํธ๋ฆญ ํ์ธ
- Response Time (P50/P95/P99)
- Throughput (RPS)
- Error Rate
- Resource Usage- Java 21
- Docker & Docker Compose
- Gradle 8.x
- (์ ํ) k3d (Kubernetes ํ ์คํธ์ฉ)
cd infra
# Docker Compose๋ก DB, Redis, Kafka ์์
docker compose --profile full up -d
# ์ํ ํ์ธ
docker compose ps# ์ ์ฒด ๋น๋
./gradlew build
# ํน์ ์๋น์ค ์คํ
./gradlew :services:order-service:bootRun
# ๋๋ JAR ์คํ
java -jar services/order-service/build/libs/order-service-0.0.1-SNAPSHOT.jar# ์ฌ์ฉ์ ๋ฑ๋ก
curl -X POST http://localhost:8081/api/users/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123",
"name": "ํ
์คํธ"
}'
# ์ฃผ๋ฌธ ์์ฑ
curl -X POST http://localhost:8085/api/orders \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"items": [{"skuId": 1, "quantity": 2}],
"shippingAddressId": 1
}'- Grafana: http://localhost:3000 (admin/admin123)
- Prometheus: http://localhost:9090
- Kafka UI: http://localhost:18080
| ์๋น์ค | ํฌํธ | ์ค๋ช | ์์ธ ๋ฌธ์ |
|---|---|---|---|
| auth-service | 8089 | JWT ํ ํฐ ๋ฐํ ๋ฐ ์ธ์ฆ | README |
| user-service | 8081 | ์ฌ์ฉ์ ๊ด๋ฆฌ (๋ฑ๋ก, ํ๋กํ, ์ฃผ์) | README |
| catalog-service | 8084 | ์ํ/์นดํ ๊ณ ๋ฆฌ ๊ด๋ฆฌ | README |
| inventory-service | 8083 | ์ฌ๊ณ ๊ด๋ฆฌ (Redis + MariaDB) | README |
| order-service | 8085 | ์ฃผ๋ฌธ ์ฒ๋ฆฌ (Saga ํจํด) | README |
| payment-service | 8087 | ๊ฒฐ์ ์ฒ๋ฆฌ (Toss Payments) | README |
๋ชฉํ: ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณ๊ฒฝ๊ณผ ์ด๋ฒคํธ ๋ฐํ์ ์์์ฑ ๋ณด์ฅ
๊ตฌํ ๋ด์ฉ:
- ๋ชจ๋ ์๋น์ค์ Outbox ํ ์ด๋ธ ์ถ๊ฐ
- Debezium CDC Connector ์ค์ ์ผ๋ก Outbox โ Kafka ์๋ ๋ฐํ
- ๊ธฐ์กด ์ง์ Kafka ๋ฐํ ์ฝ๋๋ฅผ Outbox ํจํด์ผ๋ก ์ ํ
๊ฒฐ๊ณผ:
- DB ํธ๋์ญ์ ๊ณผ ์ด๋ฒคํธ ๋ฐํ์ ์์์ฑ 100% ๋ณด์ฅ
- ์ด๋ฒคํธ ์ ์ค ๋ฐฉ์ง
- ์๋น์ค๋ณ Outbox ํ
์ด๋ธ:
{service}_outbox
๋ชฉํ: ์ค๋ณต ์ด๋ฒคํธ ์ฒ๋ฆฌ ๋ฐฉ์ง
๊ตฌํ ๋ด์ฉ:
- ์๋น์ค๋ณ IdempotencyEntry ํ ์ด๋ธ ์ถ๊ฐ
- Consumer์์
(eventId, action)์ ๋ํฌ ์ ์ฝ์ผ๋ก ์ค๋ณต ๊ฒ์ฆ - Fast-path ๋ฉฑ๋ฑ์ฑ ์ฒดํฌ (DB ์กฐํ ์ต์ํ)
๊ฒฐ๊ณผ:
- order-service: 5๊ฐ Consumer์ ๋ฉฑ๋ฑ์ฑ ์ ์ฉ
- inventory-service: 4๊ฐ Consumer์ ๋ฉฑ๋ฑ์ฑ ์ ์ฉ
- payment-service: ๊ธฐ์กด ๋ฉฑ๋ฑ์ฑ ํ ์ด๋ธ ์ ์ง
๋ชฉํ: ์ผ๊ด๋ Consumer ๊ตฌํ ํจํด ํ๋ฆฝ
๊ตฌํ ๋ด์ฉ:
@Validated+@Valid์กฐํฉ์ผ๋ก CloudEvent ๊ฒ์ฆMessageContext๋ก correlationId, causationId ์ ๋ฌ- poison message ์ฒ๋ฆฌ ์ ๋ต ํต์ผ
- ์๋ ack ๋ชจ๋ (
MANUAL_IMMEDIATE)
๊ฒฐ๊ณผ:
- ๋ชจ๋ Consumer๊ฐ ๋์ผํ ํจํด ์ค์
- ์ด๋ฒคํธ ์ถ์ ๋ฐ ๋๋ฒ๊น ์ฉ์ด
๋ชฉํ: ๋๋ฉ์ธ ์ด๋ฒคํธ ์ถ์ถ ํจํด ์ ๊ฑฐ, ๋จ์ํ
๊ตฌํ ๋ด์ฉ:
pullDomainEvents()ํจํด ์ ๊ฑฐ- UseCase์์ Integration Event ์ง์ ์์ฑ ๋ฐ ๋ฐํ
- Port/Adapter ๊ตฌ์กฐ ์ ์ง (
IntegrationEventPublisher)
๊ฒฐ๊ณผ:
- ์ฝ๋ ๋ณต์ก๋ ๊ฐ์
- ์ด๋ฒคํธ ๋ฐํ ๋ก์ง ๋ช ํํ
- ๋ถํ์ํ ์ถ์ํ ์ ๊ฑฐ
๋ชฉํ: ์ฌ๋ฌ ์ ์ฅ์๋ฅผ ํ๋์ ๋ชจ๋ ธ๋ ํฌ๋ก ํตํฉ
๊ตฌํ ๋ด์ฉ:
- common ๋ชจ๋์ GitHub Packages์์ project dependency๋ก ๋ณ๊ฒฝ
- ์๋น์ค๋ณ ๋ ๋ฆฝ ๋น๋ ๋ฐ ๊ณตํต ๋น๋ ์คํฌ๋ฆฝํธ
- Spotless ์ฝ๋ ํฌ๋งทํ ํตํฉ
๊ฒฐ๊ณผ:
- ๋น๋ ์๊ฐ ๋จ์ถ (์บ์ ํ์ฉ)
- ์์กด์ฑ ๊ด๋ฆฌ ๋จ์ํ
- GH_USER/GH_TOKEN ๋ถํ์
๋ชฉํ: Kafka ํตํฉ ํ ์คํธ ์๋ํ
๊ตฌํ ๋ด์ฉ:
- Testcontainers๋ก Kafka ์ปจํ ์ด๋ ์คํ
- Producer/Consumer ํตํฉ ํ ์คํธ ์์ฑ
- ์ด๋ฒคํธ ๋ฐํ/์๋น E2E ๊ฒ์ฆ
๊ฒฐ๊ณผ:
- CI/CD ํ์ดํ๋ผ์ธ์์ Kafka ํ ์คํธ ๊ฐ๋ฅ
- ์ด๋ฒคํธ ๊ณ์ฝ ๋ณ๊ฒฝ ๊ฐ์ง
๋ชฉํ: ์ฑ๋ฅ ๊ธฐ์ค์ ์๋ฆฝ ๋ฐ ๋ณ๋ชฉ ์ง์ ํ์
๊ตฌํ ๋ด์ฉ:
- Smoke โ Baseline โ Stress 3๋จ๊ณ ํ ์คํธ
- Prometheus Remote Write๋ก ๋ฉํธ๋ฆญ ์์ง
- Grafana ๋์๋ณด๋๋ก ์ค์๊ฐ ์๊ฐํ
๊ฒฐ๊ณผ:
- ์ฌ๊ณ ์ฐจ๊ฐ API: P95 ์๋ต์๊ฐ 50ms ์ดํ
- Redis ๋์์ฑ ์ฒ๋ฆฌ ๊ฒ์ฆ
- ์์คํ ํ๊ณ์ ํ์ (RPS 1000+)
์ด ํ๋ก์ ํธ๋ ํฌํธํด๋ฆฌ์ค ๋ชฉ์ ์ผ๋ก ์ ์๋์์ต๋๋ค.
Contact: GitHub