마이크로서비스 패턴 — Fault Tolerance, 서비스 디스커버리, 메시징
분산 시스템에서 "모든 서비스가 항상 정상 동작한다"고 가정하는 건 위험하다. 네트워크는 끊기고, 서비스는 느려지고, 컨테이너는 죽는다. 이런 장애를 어떻게 견디는 시스템을 만들 수 있을까?
SmallRye Fault Tolerance — 장애 내성 패턴
Quarkus는 MicroProfile Fault Tolerance 표준의 구현체인 SmallRye Fault Tolerance를 사용합니다. Spring 생태계의 Resilience4j와 같은 역할이지만, 어노테이션 기반으로 더 선언적입니다.
<!-- pom.xml -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
@Retry — 재시도
일시적인 장애(네트워크 타임아웃, 일시적 서버 오류)에 대응합니다.
@ApplicationScoped
public class PaymentService {
@Retry(maxRetries = 3, delay = 1000, retryOn = TimeoutException.class)
public PaymentResult processPayment(PaymentRequest request) {
return paymentGateway.charge(request);
}
}
maxRetries: 최대 재시도 횟수delay: 재시도 간격 (밀리초)retryOn: 특정 예외에서만 재시도abortOn: 특정 예외에서는 재시도하지 않음
재시도는 멱등성(idempotent)이 보장되는 작업에만 적용해야 합니다. 결제 같은 비멱등 작업에 무작정 재시도를 걸면 중복 결제가 발생할 수 있습니다.
@Timeout — 시간 제한
응답이 너무 오래 걸리는 호출을 강제로 중단합니다.
@Timeout(value = 3, unit = ChronoUnit.SECONDS)
public InventoryStatus checkInventory(String productId) {
return inventoryClient.check(productId);
}
타임아웃이 발생하면 TimeoutException이 던져집니다. 보통 @Retry나 @Fallback과 함께 사용합니다.
@CircuitBreaker — 서킷 브레이커
연속적인 실패가 감지되면 ** 일정 시간 동안 호출 자체를 차단 **합니다. 전기 회로의 차단기와 같은 개념입니다.
@CircuitBreaker(
requestVolumeThreshold = 10, // 최소 요청 수
failureRatio = 0.5, // 실패율 50% 이상이면 OPEN
delay = 5000, // OPEN 상태 유지 시간 (5초)
successThreshold = 3 // HALF-OPEN에서 성공 3번이면 CLOSED
)
public RecommendationList getRecommendations(String userId) {
return recommendationService.fetch(userId);
}
서킷 브레이커의 세 가지 상태를 정리하면 이렇습니다.
- CLOSED (정상): 모든 요청이 통과. 실패를 카운팅
- OPEN (차단): 요청을 즉시 실패 처리.
CircuitBreakerOpenException발생 - HALF-OPEN (탐색): 일부 요청만 통과시켜서 서비스 복구 여부 확인
정상 동작 실패 누적 시간 경과 성공 확인
CLOSED ──→ OPEN ──────→ HALF-OPEN ──────→ CLOSED
↑ │
└────────────────────┘
실패 시 다시 OPEN
@Fallback — 대체 응답
장애 시 미리 정의해둔 대체 로직을 실행합니다.
@Retry(maxRetries = 2)
@Fallback(fallbackMethod = "getDefaultRecommendations")
public RecommendationList getRecommendations(String userId) {
return recommendationService.fetch(userId);
}
// 시그니처가 원본 메서드와 동일해야 함
public RecommendationList getDefaultRecommendations(String userId) {
return RecommendationList.of("인기 상품 TOP 10");
}
@Fallback은 다른 어노테이션(@Retry, @CircuitBreaker, @Timeout)과 조합해서 사용합니다. 모든 재시도가 실패하거나 서킷이 열려 있을 때 Fallback이 호출됩니다.
Fallback 클래스를 별도로 분리할 수도 있습니다.
@Fallback(value = MyFallbackHandler.class)로FallbackHandler<T>인터페이스 구현체를 지정하면 됩니다.
@Bulkhead — 격벽
동시 실행 수를 제한해서 하나의 느린 서비스가 전체 시스템의 스레드를 고갈시키는 것을 방지합니다.
@Bulkhead(value = 5) // 동시 최대 5개 요청만 처리
@Fallback(fallbackMethod = "bulkheadFallback")
public Report generateReport(String reportId) {
return reportService.generate(reportId);
}
public Report bulkheadFallback(String reportId) {
return Report.queued("리포트 생성 대기 중입니다. 잠시 후 다시 시도해주세요.");
}
비동기 방식에서는 대기 큐를 설정할 수도 있습니다.
@Bulkhead(value = 5, waitingTaskQueue = 10)
@Asynchronous
public CompletionStage<Report> generateReportAsync(String reportId) {
return reportService.generateAsync(reportId);
}
어노테이션 조합 — 실전 패턴
실무에서는 이 어노테이션들을 조합해서 사용합니다.
@ApplicationScoped
public class OrderService {
@Timeout(value = 5, unit = ChronoUnit.SECONDS)
@Retry(maxRetries = 3, delay = 500, retryOn = TimeoutException.class)
@CircuitBreaker(requestVolumeThreshold = 20, failureRatio = 0.5, delay = 10000)
@Fallback(fallbackMethod = "orderFallback")
@Bulkhead(value = 10)
public OrderResult placeOrder(OrderRequest request) {
return orderGateway.submit(request);
}
public OrderResult orderFallback(OrderRequest request) {
// 주문을 메시지 큐에 넣고 나중에 처리
orderQueue.enqueue(request);
return OrderResult.pending("주문이 접수되었습니다. 잠시 후 확인해주세요.");
}
}
실행 순서는 다음과 같습니다.
요청 → Bulkhead(동시성 제한) → Timeout(시간 제한) → CircuitBreaker(차단 여부)
→ Retry(재시도) → 실제 메서드 호출
→ 모두 실패 시 → Fallback
Resilience4j(Spring) vs SmallRye(Quarkus) 비교
| 구분 | Resilience4j | SmallRye Fault Tolerance |
|---|---|---|
| 표준 | 독자적 API | MicroProfile 표준 |
| 설정 방식 | 프로퍼티 + 빌더 패턴 | ** 어노테이션 중심** |
| 서킷 브레이커 | 슬라이딩 윈도우 기반 | 롤링 윈도우 기반 |
| 비동기 지원 | CompletableFuture | CompletionStage + Mutiny |
| 메트릭 | Micrometer 연동 | MicroProfile Metrics 자동 |
| 런타임 설정 변경 | Registry를 통해 가능 | 프로퍼티로 오버라이드 가능 |
SmallRye는 MicroProfile 표준이라 벤더 종속성이 없고, 다른 MicroProfile 런타임(Open Liberty, Payara 등)에서도 같은 코드가 동작합니다.
Stork — 서비스 디스커버리
마이크로서비스 간 통신에서 상대방 서비스의 주소를 어떻게 알아낼까? Quarkus는 SmallRye Stork 를 사용해서 서비스 디스커버리와 로드 밸런싱을 제공합니다.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-stork</artifactId>
</dependency>
Consul을 사용한 서비스 디스커버리
# application.properties
quarkus.stork.order-service.service-discovery.type=consul
quarkus.stork.order-service.service-discovery.consul-host=localhost
quarkus.stork.order-service.service-discovery.consul-port=8500
quarkus.stork.order-service.load-balancer.type=round-robin
@RegisterRestClient(baseUri = "stork://order-service")
public interface OrderClient {
@GET
@Path("/orders/{id}")
Order getOrder(@PathParam("id") Long id);
}
stork:// 프로토콜을 사용하면 Stork가 자동으로 서비스 디스커버리를 수행하고, 여러 인스턴스 중 하나를 로드 밸런싱 전략에 따라 선택합니다.
지원하는 디스커버리 소스
- Consul: HashiCorp Consul
- Kubernetes: k8s DNS/API를 통한 서비스 조회
- Eureka: Netflix Eureka (Spring Cloud와 호환)
- Static List: 고정 주소 목록 (개발/테스트용)
# Kubernetes 환경
quarkus.stork.order-service.service-discovery.type=kubernetes
quarkus.stork.order-service.service-discovery.k8s-namespace=production
# 개발 환경 — 고정 주소
quarkus.stork.order-service.service-discovery.type=static
quarkus.stork.order-service.service-discovery.address-list=localhost:8081,localhost:8082
SmallRye Reactive Messaging — 메시징
마이크로서비스 간의 비동기 통신은 메시지 브로커를 통해 이루어집니다. Quarkus는 SmallRye Reactive Messaging 을 통해 Kafka, AMQP, MQTT 등을 통일된 API로 다룹니다.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-messaging-kafka</artifactId>
</dependency>
메시지 발행 (Producer)
@ApplicationScoped
public class OrderEventProducer {
@Channel("order-events")
Emitter<OrderEvent> emitter;
public void publishOrderCreated(Order order) {
OrderEvent event = new OrderEvent("CREATED", order.id, order.totalAmount);
emitter.send(event);
}
}
메시지 소비 (Consumer)
@ApplicationScoped
public class OrderEventConsumer {
@Incoming("order-events")
public void onOrderEvent(OrderEvent event) {
switch (event.type()) {
case "CREATED" -> inventoryService.reserve(event.orderId());
case "CANCELLED" -> inventoryService.release(event.orderId());
}
}
}
스트림 변환 (Processor)
@ApplicationScoped
public class OrderEventProcessor {
@Incoming("raw-orders")
@Outgoing("processed-orders")
public ProcessedOrder process(RawOrder raw) {
// 변환 로직
return new ProcessedOrder(raw.id(), calculateTotal(raw));
}
}
Kafka 설정
# application.properties
mp.messaging.outgoing.order-events.connector=smallrye-kafka
mp.messaging.outgoing.order-events.topic=orders
mp.messaging.outgoing.order-events.value.serializer=io.quarkus.kafka.client.serialization.ObjectMapperSerializer
mp.messaging.incoming.order-events.connector=smallrye-kafka
mp.messaging.incoming.order-events.topic=orders
mp.messaging.incoming.order-events.value.deserializer=io.quarkus.kafka.client.serialization.ObjectMapperDeserializer
mp.messaging.incoming.order-events.group.id=inventory-service
Spring Kafka에서는
@KafkaListener와KafkaTemplate을 사용합니다. Quarkus의 Reactive Messaging은@Incoming/@Outgoing으로 통일되어 있어서, Kafka를 AMQP로 바꿀 때 설정만 변경하면 코드는 동일 하게 유지됩니다.
MicroProfile 표준 — Health, Config, OpenTelemetry
Quarkus의 마이크로서비스 지원은 개별 라이브러리가 아니라 MicroProfile 표준 을 기반으로 합니다.
Health Check
@Liveness
@ApplicationScoped
public class DatabaseHealthCheck implements HealthCheck {
@Inject
EntityManager em;
@Override
public HealthCheckResponse call() {
try {
em.createNativeQuery("SELECT 1").getSingleResult();
return HealthCheckResponse.up("Database");
} catch (Exception e) {
return HealthCheckResponse.down("Database");
}
}
}
/q/health/live와 /q/health/ready 엔드포인트가 자동으로 생성됩니다. Kubernetes의 Liveness/Readiness Probe와 바로 연동됩니다.
MicroProfile Config
@ApplicationScoped
public class PaymentConfig {
@ConfigProperty(name = "payment.timeout", defaultValue = "5000")
int timeout;
@ConfigProperty(name = "payment.retry.max", defaultValue = "3")
int maxRetries;
}
환경 변수, 시스템 프로퍼티, application.properties 등 여러 소스에서 설정을 통합해서 가져옵니다. Spring의 @Value와 비슷하지만 MicroProfile 표준입니다.
OpenTelemetry
quarkus.otel.exporter.otlp.endpoint=http://localhost:4317
quarkus.otel.service.name=order-service
별도 코드 없이 설정만으로 분산 추적이 활성화됩니다. HTTP 요청, DB 쿼리, 메시징 등에 자동으로 Span이 생성됩니다.
정리
Quarkus의 마이크로서비스 패턴을 한눈에 보면 이렇습니다.
| 패턴 | Quarkus | Spring 대응 |
|---|---|---|
| Fault Tolerance | SmallRye (MicroProfile) | Resilience4j |
| 서비스 디스커버리 | Stork | Spring Cloud Discovery |
| 메시징 | Reactive Messaging | Spring Kafka / Spring AMQP |
| Health Check | MicroProfile Health | Actuator |
| 설정 관리 | MicroProfile Config | Spring Cloud Config |
| 분산 추적 | OpenTelemetry | Micrometer Tracing |
공통점은 모두 MicroProfile 표준 을 기반으로 한다는 것입니다. 벤더 종속이 적고, 어노테이션 기반으로 선언적으로 사용할 수 있습니다. Spring Cloud와 비교했을 때 설정이 더 간결하고, 빌드 타임 최적화의 이점을 그대로 받습니다.