서비스가 정상인지 어떻게 알 수 있을까? 로그를 뒤져보기 전에, 서비스 스스로가 "나 괜찮아" 또는 "나 지금 좀 이상해"라고 알려줄 수 있다면?

관찰성(Observability)이란

관찰성은 시스템의 내부 상태를 외부에서 파악할 수 있는 능력입니다. 보통 세 가지 축(pillar)으로 나눕니다.

  • Metrics: 수치로 표현되는 측정값 (요청 수, 응답 시간, 메모리 사용량)
  • Traces: 요청이 여러 서비스를 거치는 경로 추적
  • Logs: 이벤트 기록

Quarkus는 이 세 가지를 모두 프레임워크 수준에서 지원합니다. 확장만 추가하면 대부분의 기본 설정이 자동으로 잡히기 때문에 초기 구축이 빠릅니다.


MicroProfile Health — 헬스 체크

MicroProfile Health는 애플리케이션의 상태를 HTTP 엔드포인트로 노출하는 표준 스펙입니다.

BASH
./mvnw quarkus:add-extension -Dextensions="smallrye-health"

확장을 추가하는 것만으로 세 개의 엔드포인트가 생깁니다.

엔드포인트용도어노테이션
/q/health/live프로세스가 살아있는지@Liveness
/q/health/ready요청을 처리할 준비가 되었는지@Readiness
/q/health/started시작이 완료되었는지@Startup
/q/health위 세 가지 모두 통합-

기본적으로 데이터소스, 메시지 브로커 등의 연결 상태를 자동으로 체크합니다. 확장만 추가하면 별도의 코드 없이도 이렇게 응답합니다.

JSON
// GET /q/health
{
  "status": "UP",
  "checks": [
    {
      "name": "Database connections health check",
      "status": "UP",
      "data": {
        "<default>": "UP"
      }
    }
  ]
}

커스텀 Health Check 구현

비즈니스 로직에 맞는 헬스 체크를 직접 만들 수 있습니다.

JAVA
@Liveness
@ApplicationScoped
public class ExternalApiHealthCheck implements HealthCheck {

    @Inject
    ExternalApiClient apiClient;

    @Override
    public HealthCheckResponse call() {
        try {
            apiClient.ping();
            return HealthCheckResponse.up("외부 API 연결");
        } catch (Exception e) {
            return HealthCheckResponse.named("외부 API 연결")
                .down()
                .withData("error", e.getMessage())
                .build();
        }
    }
}
JAVA
@Readiness
@ApplicationScoped
public class CacheHealthCheck implements HealthCheck {

    @Inject
    CacheManager cacheManager;

    @Override
    public HealthCheckResponse call() {
        boolean cacheAvailable = cacheManager.isAvailable();
        return HealthCheckResponse.named("캐시 서비스")
            .status(cacheAvailable)
            .withData("provider", "Redis")
            .build();
    }
}

@Liveness는 프로세스가 정상인지를 판단합니다. 이 체크가 실패하면 Kubernetes가 Pod를 재시작합니다. 반면 @Readiness는 트래픽을 받을 준비가 되었는지를 판단합니다. 실패하면 로드밸런서에서 제외되지만 Pod가 재시작되지는 않습니다.

Liveness 체크에 외부 의존성(DB, 캐시 등)을 넣으면 위험할 수 있습니다. DB가 잠깐 느려졌을 때 Pod가 불필요하게 재시작될 수 있기 때문입니다. 외부 의존성은 Readiness에 넣는 것이 일반적인 패턴입니다.

Kubernetes Probe 연동

Kubernetes 확장과 함께 사용하면 Probe가 자동으로 매핑됩니다.

PROPERTIES
# Health Check + Kubernetes 확장이 함께 있으면 자동 설정
# 필요시 커스터마이징
quarkus.kubernetes.liveness-probe.initial-delay=5s
quarkus.kubernetes.liveness-probe.period=10s
quarkus.kubernetes.readiness-probe.initial-delay=5s
quarkus.kubernetes.readiness-probe.failure-threshold=3
quarkus.kubernetes.startup-probe.initial-delay=0s
quarkus.kubernetes.startup-probe.period=2s
quarkus.kubernetes.startup-probe.failure-threshold=30

Micrometer Metrics — 메트릭 수집

Quarkus는 Micrometer를 메트릭 수집 라이브러리로 사용합니다. Spring Boot에서도 같은 라이브러리를 쓰기 때문에 익숙한 분들이 많을 겁니다.

BASH
./mvnw quarkus:add-extension -Dextensions="micrometer-registry-prometheus"

이 확장 하나로 /q/metrics 엔드포인트가 활성화되고, Prometheus가 스크래핑할 수 있는 포맷으로 메트릭이 노출됩니다.

자동 수집 메트릭

확장만 추가하면 다음 메트릭들이 자동으로 수집됩니다.

  • **JVM 메트릭 **: 힙 메모리, GC, 스레드 수, 클래스 로딩
  • **HTTP 서버 메트릭 **: 요청 수, 응답 시간, 상태 코드별 카운트
  • ** 시스템 메트릭 **: CPU 사용률, 파일 디스크립터
  • ** 데이터소스 메트릭 **: 커넥션 풀 상태 (Agroal 사용 시)
PLAINTEXT
# GET /q/metrics 응답 일부

# HTTP 요청 메트릭
http_server_requests_seconds_count{method="GET",uri="/api/users",status="200"} 1523.0
http_server_requests_seconds_sum{method="GET",uri="/api/users",status="200"} 45.3

# JVM 힙 메모리
jvm_memory_used_bytes{area="heap",id="G1 Eden Space"} 52428800.0
jvm_memory_max_bytes{area="heap",id="G1 Old Gen"} 536870912.0

# 커넥션 풀
agroal_active_count{datasource="default"} 5
agroal_available_count{datasource="default"} 15

커스텀 메트릭

비즈니스 메트릭을 직접 정의할 수도 있습니다.

JAVA
@ApplicationScoped
@Path("/orders")
public class OrderResource {

    @Inject
    MeterRegistry registry;

    // Counter: 주문 생성 횟수
    @POST
    public Response createOrder(OrderDto order) {
        Order created = orderService.create(order);

        registry.counter("orders.created",
            "type", order.getType(),
            "region", order.getRegion()
        ).increment();

        return Response.created(URI.create("/orders/" + created.getId())).build();
    }

    // Timer: 외부 결제 API 호출 시간 측정
    public PaymentResult processPayment(PaymentRequest request) {
        return registry.timer("payment.processing")
            .record(() -> paymentClient.process(request));
    }

    // Gauge: 현재 처리 대기 중인 주문 수
    @PostConstruct
    void initGauges() {
        registry.gauge("orders.pending", orderQueue, Queue::size);
    }
}

어노테이션 기반으로도 메트릭을 추가할 수 있습니다.

JAVA
@ApplicationScoped
public class NotificationService {

    @Counted(value = "notifications.sent", description = "발송된 알림 수")
    @Timed(value = "notifications.duration", description = "알림 발송 소요 시간")
    public void sendNotification(String userId, String message) {
        // 알림 발송 로직
    }
}

Prometheus 설정

Prometheus에서 Quarkus 메트릭을 스크래핑하려면 prometheus.yml에 타겟을 추가합니다.

YAML
# prometheus.yml
scrape_configs:
  - job_name: 'quarkus-app'
    metrics_path: '/q/metrics'
    scrape_interval: 15s
    static_configs:
      - targets: ['my-app:8080']

Kubernetes 환경에서는 ServiceMonitor를 사용하는 것이 일반적입니다.

YAML
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-app-monitor
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: my-app
  endpoints:
    - port: http
      path: /q/metrics
      interval: 15s

OpenTelemetry — 분산 추적

마이크로서비스 환경에서 하나의 요청이 여러 서비스를 거칠 때, 어디서 지연이 발생하는지 추적하려면 분산 추적(Distributed Tracing)이 필요합니다.

BASH
./mvnw quarkus:add-extension -Dextensions="opentelemetry"
PROPERTIES
# application.properties

# OTLP 엔드포인트 설정 (Jaeger, Tempo 등)
quarkus.otel.exporter.otlp.endpoint=http://jaeger:4317

# 서비스 이름
quarkus.otel.resource.attributes=service.name=my-app

# 샘플링 비율 (1.0 = 100%, 0.1 = 10%)
quarkus.otel.traces.sampler=parentbased_traceidratio
quarkus.otel.traces.sampler.arg=1.0

자동 계측

OpenTelemetry 확장을 추가하면 다음 항목들이 자동으로 계측됩니다.

  • **HTTP 서버 **: 들어오는 모든 요청에 트레이스 생성
  • **HTTP 클라이언트 **: RestClient/WebClient로 나가는 요청에 트레이스 전파
  • gRPC: gRPC 요청/응답 트레이싱
  • ** 데이터베이스 **: JDBC 쿼리 트레이싱
  • ** 메시징 **: Kafka, AMQP 메시지 트레이싱

별도의 코드 없이도 서비스 간 트레이스가 전파됩니다. 서비스 A가 서비스 B를 호출하면, 같은 Trace ID로 묶여서 Jaeger/Tempo에서 하나의 요청 흐름으로 볼 수 있습니다.

커스텀 Span 추가

자동 계측으로 부족한 경우, 특정 비즈니스 로직에 Span을 직접 추가할 수 있습니다.

JAVA
@ApplicationScoped
public class RecommendationService {

    @Inject
    Tracer tracer;

    public List<Product> getRecommendations(String userId) {
        Span span = tracer.spanBuilder("recommendations.compute")
            .setAttribute("user.id", userId)
            .startSpan();

        try (Scope scope = span.makeCurrent()) {
            // 추천 알고리즘 실행
            List<Product> candidates = fetchCandidates(userId);
            span.addEvent("후보 상품 조회 완료", Attributes.of(
                AttributeKey.longKey("candidate.count"), (long) candidates.size()
            ));

            List<Product> ranked = rankProducts(candidates, userId);
            span.addEvent("랭킹 완료");

            return ranked;
        } catch (Exception e) {
            span.setStatus(StatusCode.ERROR, e.getMessage());
            span.recordException(e);
            throw e;
        } finally {
            span.end();
        }
    }
}

더 간단하게 어노테이션으로도 가능합니다.

JAVA
@ApplicationScoped
public class PaymentService {

    @WithSpan("payment.process")
    public PaymentResult processPayment(
            @SpanAttribute("payment.method") String method,
            @SpanAttribute("payment.amount") double amount) {
        // 결제 로직
        return new PaymentResult(true);
    }
}

Jaeger/Tempo로 트레이스 확인

Jaeger를 로컬에서 빠르게 확인하려면 다음과 같이 실행합니다.

BASH
docker run -d --name jaeger \
  -p 16686:16686 \   # Jaeger UI
  -p 4317:4317 \     # OTLP gRPC
  jaegertracing/all-in-one:latest

브라우저에서 http://localhost:16686에 접속하면 트레이스를 시각적으로 확인할 수 있습니다. 각 Span의 소요 시간, 속성, 이벤트 등을 상세히 볼 수 있어서 성능 병목을 찾는 데 유용합니다.


로깅

Quarkus는 JBoss Logging 을 기본 로깅 프레임워크로 사용합니다. 개발자 입장에서는 java.util.logging, SLF4J, Log4j 등 어떤 로깅 API를 사용해도 내부적으로 JBoss Logging으로 통합됩니다.

JAVA
import io.quarkus.logging.Log;

@ApplicationScoped
public class UserService {

    public User findUser(String id) {
        Log.infof("사용자 조회: id=%s", id);
        // ...
    }
}

JSON 포맷 로깅

프로덕션에서는 구조화된 로그가 중요합니다. 로그 수집 도구(ELK, Loki 등)가 파싱하기 쉽기 때문입니다.

BASH
./mvnw quarkus:add-extension -Dextensions="logging-json"
PROPERTIES
# JSON 로깅 활성화
quarkus.log.console.json=true

# 추가 필드
quarkus.log.console.json.additional-field.service.value=my-app
quarkus.log.console.json.additional-field.environment.value=production

출력 결과가 이렇게 바뀝니다.

JSON
{
  "timestamp": "2026-03-28T10:15:30.123Z",
  "level": "INFO",
  "loggerName": "c.e.UserService",
  "message": "사용자 조회: id=user-123",
  "service": "my-app",
  "environment": "production",
  "traceId": "abc123def456",
  "spanId": "789ghi"
}

OpenTelemetry 확장과 함께 사용하면 traceIdspanId가 자동으로 로그에 포함됩니다. 로그에서 특정 트레이스 ID를 검색하면 해당 요청의 모든 로그를 한눈에 볼 수 있어서, 메트릭-트레이스-로그 세 축을 연결하는 핵심 고리가 됩니다.

로그 레벨 설정

PROPERTIES
# 전체 로그 레벨
quarkus.log.level=INFO

# 패키지별 로그 레벨
quarkus.log.category."com.example".level=DEBUG
quarkus.log.category."org.hibernate.SQL".level=DEBUG

# 파일 출력
quarkus.log.file.enable=true
quarkus.log.file.path=/var/log/my-app.log
quarkus.log.file.rotation.max-file-size=10M
quarkus.log.file.rotation.max-backup-index=5

DevServices for Observability

공부하면서 특히 편리했던 기능이 DevServices입니다. Dev Mode에서 관찰성 도구를 자동으로 띄워줍니다.

BASH
# Dev Mode 실행
./mvnw quarkus:dev

관련 확장이 추가되어 있으면 Dev Mode 시작 시 다음을 자동으로 실행합니다.

  • Grafana: 대시보드 UI (기본 대시보드 포함)
  • Prometheus: 메트릭 수집 (스크래핑 설정 자동)
  • Jaeger/OTLP Collector: 트레이스 수집

Dev UI(http://localhost:8080/q/dev-ui)에서 이 도구들의 링크를 바로 확인할 수 있습니다. 로컬 개발 중에 실제 프로덕션과 유사한 관찰성 환경을 별도의 설정 없이 경험할 수 있다는 점이 인상적이었습니다.

PROPERTIES
# DevServices 관련 설정 (기본값으로도 충분하지만 커스터마이징 가능)
quarkus.otel.exporter.otlp.endpoint=http://localhost:4317

# Dev Mode에서만 전체 트레이스 수집
%dev.quarkus.otel.traces.sampler.arg=1.0

# 프로덕션에서는 10%만 수집
%prod.quarkus.otel.traces.sampler.arg=0.1

Spring Actuator와 비교

Spring Boot에서 넘어온 개발자라면 Actuator와 자연스럽게 비교하게 됩니다.

항목Spring Boot ActuatorQuarkus
Health 엔드포인트/actuator/health/q/health
Metrics 엔드포인트/actuator/metrics/q/metrics
메트릭 라이브러리MicrometerMicrometer (동일)
분산 추적Micrometer Tracing + Brave/OTelOpenTelemetry
로깅Logback/Log4j2JBoss Logging
Info 엔드포인트/actuator/info/q/info (확장 필요)
커스텀 엔드포인트@Endpoint 어노테이션없음 (REST 엔드포인트로 직접 구현)

큰 차이점 중 하나는 Quarkus가 OpenTelemetry를 1등 시민(first-class citizen)으로 지원한다는 것입니다. Spring Boot도 3.x부터 OpenTelemetry를 지원하지만, Quarkus는 처음부터 OTel 중심으로 설계되었기 때문에 자동 계측의 범위가 넓고 설정이 간결합니다.

반면 Spring Boot Actuator는 더 많은 종류의 엔드포인트(beans, env, configprops 등)를 기본 제공합니다. Quarkus에서는 이런 정보를 Dev UI에서 확인할 수 있지만, 프로덕션 엔드포인트로는 노출되지 않습니다.


실전 관찰성 스택 예시

프로덕션에서 Quarkus 관찰성 스택을 구성하면 보통 이런 형태가 됩니다.

PLAINTEXT
Quarkus App
  ├── /q/health → Kubernetes Probe
  ├── /q/metrics → Prometheus → Grafana (대시보드)
  └── OTLP export → Tempo/Jaeger (트레이싱)
                  → Loki (로그 수집)

필요한 확장과 설정을 한 번에 정리하면 이렇습니다.

PROPERTIES
# application.properties — 관찰성 통합 설정

# Health
# (smallrye-health 확장만 추가하면 자동 설정)

# Metrics → Prometheus
# (micrometer-registry-prometheus 확장)
quarkus.micrometer.export.prometheus.path=/q/metrics

# OpenTelemetry → Tempo/Jaeger
quarkus.otel.exporter.otlp.endpoint=http://tempo:4317
quarkus.otel.resource.attributes=service.name=my-app,service.version=1.0.0

# 프로파일별 샘플링 비율
%dev.quarkus.otel.traces.sampler.arg=1.0
%prod.quarkus.otel.traces.sampler.arg=0.1

# JSON 로깅
%prod.quarkus.log.console.json=true

정리

관찰성은 "있으면 좋은 것"이 아니라 프로덕션 운영의 필수 요소입니다. Quarkus는 확장 몇 개를 추가하는 것만으로 Health Check, Metrics, Tracing, 구조화 로깅까지 갖출 수 있게 해줍니다.

기억해둘 포인트를 정리하면 이렇습니다.

  • Health Check: Liveness에는 자체 상태만, Readiness에 외부 의존성 체크를 넣는 것이 안전한 패턴
  • Metrics: Micrometer 기반이므로 Spring Boot와 코드 호환성이 높음. @Counted, @Timed, MeterRegistry 모두 사용 가능
  • OpenTelemetry: 확장 추가만으로 HTTP, DB, 메시징의 자동 계측이 활성화됨. @WithSpan으로 커스텀 Span 추가
  • ** 로깅 **: JSON 포맷 + OpenTelemetry 연동으로 traceId가 로그에 자동 포함되어 메트릭-트레이스-로그 연결이 가능
  • DevServices: Dev Mode에서 Grafana, Prometheus, Jaeger가 자동 시작되어 로컬에서도 관찰성을 체험할 수 있음
댓글 로딩 중...