MSA에서 Observability가 중요한 이유
모놀리식 아키텍처에서는 하나의 애플리케이션 로그와 스택 트레이스만으로 대부분의 문제를 파악할 수 있었습니다. 하지만 MSA(Microservices Architecture) 환경에서는 수십에서 수백 개의 서비스가 네트워크를 통해 상호작용하므로, 단일 요청이 여러 서비스를 거치며 어디서 지연이 발생하고 어디서 오류가 났는지 추적하기가 극도로 어려워집니다.
관측 가능성(Observability)은 시스템의 외부 출력(로그, 메트릭, 트레이스)을 통해 내부 상태를 이해할 수 있는 능력을 의미합니다. 이는 단순 모니터링을 넘어, 예측하지 못한 장애 상황에서도 문제의 근본 원인을 파악할 수 있게 해줍니다.
Observability의 3축 (Three Pillars)
Observability는 로그(Logs), 메트릭(Metrics), 트레이스(Traces) 세 가지 축으로 구성됩니다. 각각은 서로 보완적이며, 세 가지 모두를 갖추어야 완전한 관측 가능성을 확보할 수 있습니다.
| 축 | 설명 | 특성 | 도구 |
|---|---|---|---|
| Logs (로그) | 이산적인 이벤트 기록 | 고해상도, 비구조화/구조화, 대용량 | ELK, Loki |
| Metrics (메트릭) | 시계열 수치 데이터 | 집계 가능, 저비용, 알림 설정 | Prometheus, Datadog |
| Traces (트레이스) | 분산 요청 흐름 추적 | 인과관계 파악, 지연 분석 | Jaeger, Zipkin |
분산 트레이싱 (Distributed Tracing)
분산 트레이싱은 하나의 요청이 여러 서비스를 거치는 전체 경로를 추적하는 기술입니다. 각 서비스 구간(Span)의 소요 시간과 관계를 시각화하여 병목 지점을 파악할 수 있습니다.
핵심 개념
- Trace: 하나의 요청에 대한 전체 여정. 고유한 Trace ID로 식별됩니다.
- Span: 하나의 서비스 내에서의 작업 단위. 시작/종료 시간, 태그, 로그를 포함합니다.
- Context Propagation: Trace ID와 Span ID를 서비스 간 HTTP 헤더 또는 메시지 헤더로 전파합니다.
Zipkin 설정
# docker-compose.yml - Zipkin
zipkin:
image: openzipkin/zipkin:latest
ports:
- "9411:9411"
environment:
STORAGE_TYPE: elasticsearch
ES_HOSTS: http://elasticsearch:9200
<!-- Spring Boot + Zipkin 의존성 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>io.zipkin.reporter2</groupId>
<artifactId>zipkin-reporter-brave</artifactId>
</dependency>
# application.yml - 트레이싱 설정
management:
tracing:
sampling:
probability: 1.0 # 개발: 100%, 프로덕션: 0.1 (10%) 권장
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
logging:
pattern:
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
Jaeger 설정
# docker-compose.yml - Jaeger
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
- "4318:4318" # OTLP HTTP
environment:
COLLECTOR_OTLP_ENABLED: "true"
SPAN_STORAGE_TYPE: elasticsearch
ES_SERVER_URLS: http://elasticsearch:9200
<!-- Jaeger + OpenTelemetry 의존성 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
# application.yml - Jaeger/OTLP 설정
management:
tracing:
sampling:
probability: 0.1
otlp:
tracing:
endpoint: http://jaeger:4318/v1/traces
커스텀 Span 생성
@Service
@RequiredArgsConstructor
public class PaymentService {
private final ObservationRegistry observationRegistry;
public PaymentResult processPayment(PaymentRequest request) {
return Observation.createNotStarted("payment.process", observationRegistry)
.lowCardinalityKeyValue("payment.method", request.getMethod())
.highCardinalityKeyValue("payment.orderId", request.getOrderId())
.observe(() -> {
// PG사 API 호출
PaymentResult result = pgClient.charge(request);
// 결과에 따라 추가 태그
if (!result.isSuccess()) {
Observation.current(observationRegistry)
.lowCardinalityKeyValue("payment.error", result.getErrorCode());
}
return result;
});
}
}
Micrometer를 활용한 메트릭 수집
Micrometer는 JVM 기반 애플리케이션의 메트릭 수집을 위한 벤더 중립적인 인터페이스입니다. SLF4J가 로깅의 Facade인 것처럼, Micrometer는 메트릭의 Facade 역할을 합니다.
커스텀 비즈니스 메트릭
@Component
@RequiredArgsConstructor
public class OrderMetrics {
private final MeterRegistry meterRegistry;
// Counter: 주문 수 카운팅
public void recordOrderCreated(String paymentMethod) {
Counter.builder("orders.created.total")
.description("생성된 주문 수")
.tag("payment_method", paymentMethod)
.register(meterRegistry)
.increment();
}
// Timer: 주문 처리 시간
public void recordOrderProcessingTime(long durationMs, String status) {
Timer.builder("orders.processing.duration")
.description("주문 처리 소요 시간")
.tag("status", status)
.register(meterRegistry)
.record(Duration.ofMillis(durationMs));
}
// Gauge: 현재 처리 중인 주문 수
public void registerPendingOrdersGauge(AtomicInteger pendingCount) {
Gauge.builder("orders.pending.count", pendingCount, AtomicInteger::get)
.description("현재 처리 대기 중인 주문 수")
.register(meterRegistry);
}
// Distribution Summary: 주문 금액 분포
public void recordOrderAmount(double amount) {
DistributionSummary.builder("orders.amount")
.description("주문 금액 분포")
.baseUnit("won")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry)
.record(amount);
}
}
Prometheus + Grafana 연동
Spring Boot Actuator + Prometheus 설정
<!-- 의존성 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
endpoint:
health:
show-details: always
probes:
enabled: true
metrics:
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active:default}
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'spring-boot-apps'
metrics_path: '/actuator/prometheus'
scrape_interval: 10s
# Kubernetes 서비스 디스커버리
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
Grafana 대시보드 핵심 지표
# RED 메서드 기반 쿼리 (Rate, Errors, Duration)
# Rate: 초당 요청 수
rate(http_server_requests_seconds_count{application="order-service"}[5m])
# Errors: 에러율
sum(rate(http_server_requests_seconds_count{application="order-service", status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count{application="order-service"}[5m]))
# Duration: 95 퍼센타일 응답 시간
histogram_quantile(0.95,
sum(rate(http_server_requests_seconds_bucket{application="order-service"}[5m])) by (le)
)
# USE 메서드 (Utilization, Saturation, Errors)
# CPU 사용률
process_cpu_usage{application="order-service"}
# JVM 메모리 사용량
jvm_memory_used_bytes{application="order-service", area="heap"}
/
jvm_memory_max_bytes{application="order-service", area="heap"}
중앙 로깅: ELK 스택
MSA 환경에서 각 서비스의 로그를 개별적으로 확인하는 것은 비현실적입니다. ELK(Elasticsearch + Logstash + Kibana) 스택을 통해 모든 서비스의 로그를 중앙에서 수집, 검색, 시각화할 수 있습니다.
구조화된 로깅 (JSON 로그)
<!-- logback-spring.xml -->
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>traceId</includeMdcKeyName>
<includeMdcKeyName>spanId</includeMdcKeyName>
<customFields>{"service":"order-service","env":"production"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON" />
</root>
</configuration>
<!-- 출력 예시 -->
<!--
{
"@timestamp": "2026-03-25T10:30:15.123Z",
"level": "INFO",
"logger_name": "c.e.o.OrderService",
"message": "주문 생성 완료",
"traceId": "6a3f7b2c1d4e5f",
"spanId": "a1b2c3d4",
"service": "order-service",
"env": "production",
"orderId": "ORD-12345"
}
-->
Logstash 파이프라인 설정
# logstash.conf
input {
beats {
port => 5044
}
}
filter {
json {
source => "message"
}
# traceId로 분산 트레이싱 연결
if [traceId] {
mutate {
add_field => { "trace_link" => "http://jaeger:16686/trace/%{traceId}" }
}
}
# 에러 로그 분류
if [level] == "ERROR" {
mutate {
add_tag => ["error"]
}
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
}
}
Spring Boot Actuator 심화 활용
// 커스텀 Health Indicator
@Component
public class KafkaHealthIndicator implements HealthIndicator {
private final KafkaAdmin kafkaAdmin;
@Override
public Health health() {
try {
Map<String, Object> configs = kafkaAdmin.getConfigurationProperties();
AdminClient client = AdminClient.create(configs);
DescribeClusterResult cluster = client.describeCluster();
int nodeCount = cluster.nodes().get(5, TimeUnit.SECONDS).size();
client.close();
if (nodeCount > 0) {
return Health.up()
.withDetail("broker_count", nodeCount)
.withDetail("cluster_id", cluster.clusterId().get())
.build();
} else {
return Health.down()
.withDetail("reason", "No brokers available")
.build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
}
// 커스텀 Info Contributor
@Component
public class DeploymentInfoContributor implements InfoContributor {
@Value("${app.version:unknown}")
private String appVersion;
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("deployment", Map.of(
"version", appVersion,
"timestamp", LocalDateTime.now(),
"javaVersion", System.getProperty("java.version")
));
}
}
종합 아키텍처: Observability 스택
┌─────────────────────────────────────────┐
│ Grafana Dashboard │
│ (메트릭 시각화 + 알림) │
└─────┬──────────────┬────────────────┬────┘
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌──────▼─────┐
│Prometheus │ │ Jaeger/ │ │ Kibana │
│(메트릭) │ │ Zipkin │ │ (로그) │
│ │ │(트레이스) │ │ │
└─────▲─────┘ └─────▲─────┘ └──────▲─────┘
│ │ │
┌──────────┐ /actuator/ Trace 전파 JSON 로그
│ Service │─────prometheus──────────┼────────────────┤
│ A │ │ │ │
└──────────┘ │ │ │
┌──────────┐ │ │ │
│ Service │─────────┼──────────────┼────────────────┤
│ B │ │ │ │
└──────────┘ │ │ │
┌──────────┐ │ │ │
│ Service │─────────┴──────────────┴────────────────┘
│ C │
└──────────┘
Observability 도입 체크리스트
| 단계 | 항목 | 도구 |
|---|---|---|
| 1단계 | 구조화된 JSON 로깅 + 중앙 수집 | Logback + ELK/Loki |
| 2단계 | Actuator + Prometheus 메트릭 | Micrometer + Prometheus + Grafana |
| 3단계 | 분산 트레이싱 | Micrometer Tracing + Jaeger/Zipkin |
| 4단계 | 알림 체계 구축 | Grafana Alerting / PagerDuty |
| 5단계 | 대시보드 표준화 (RED/USE) | Grafana Dashboard |
| 6단계 | 로그-메트릭-트레이스 연결 | Grafana Tempo + Loki 연동 |
마무리
MSA 환경에서의 Observability는 선택이 아닌 필수입니다. 로그, 메트릭, 트레이스 세 축을 모두 갖추고, 이들을 상호 연결(Correlation)하여 활용해야 진정한 관측 가능성을 확보할 수 있습니다. 특히 Trace ID를 로그에 포함시켜 Jaeger의 트레이스와 Kibana의 로그를 자유롭게 오갈 수 있도록 구성하는 것이 핵심입니다. 단계별로 도입하되, 로그 중앙화부터 시작하여 메트릭, 트레이싱 순으로 확장하는 것을 권장합니다.
'Architecture' 카테고리의 다른 글
| 헥사고날 아키텍처 완벽 가이드 - 포트와 어댑터로 클린 아키텍처 구현 (0) | 2026.04.08 |
|---|---|
| DDD(Domain-Driven Design) 실전 가이드 - 전략적 설계부터 전술적 구현까지 (2) | 2026.04.07 |
| MSA 관측 가능성(Observability) 완벽 가이드 (0) | 2026.03.30 |
| CQRS & 이벤트 소싱 패턴 - 언제 왜 사용하는가 (0) | 2026.03.27 |
| MSA 아키텍처 완벽 가이드 - 설계 원칙부터 실전 패턴까지 (0) | 2026.03.25 |