Backend
OpenTelemetry와 Kafka를 활용한 안정적인 Observability 구축기
홍성현Charles(찰스) / 공통플랫폼개발팀여기어때
2025년 11월 21일
원문에서 보기 ↗안녕하세요. 여기어때컴퍼니 공통플랫폼개발팀의 찰스입니다.
현대의 마이크로서비스 아키텍처(MSA) 환경에서 시스템의 복잡성은 계속해서 증가하고 있습니다. 수십, 수백 개의 서비스가 서로 유기적으로 통신하며 하나의 기능을 완성하기에, 장애가 발생하면 그 원인을 찾고 시스템의 병목 지점을 분석하는 일은 전통적인 모니터링 방식만으로는 한계가 존재합니다.
저희는 지난 ‘모니터링에서 옵저버빌리티로 더 나은 시스템 이해를 위한 여정’ 편에서, 바로 이러한 전통적인 모니터링 방식이 가진 한계와 시스템을 더 깊이 이해하기 위한 ‘관찰 가능성(Observability)’의 필요성에 대해 자세히 다룬 바 있습니다.
저희 여기어때에서는 이러한 복잡성을 해결하기 위해 Cloud Native Computing Foundation (CNCF) 프로젝트인 OpenTelemetry를 도입하여, 시스템 내부를 깊이 들여다보며 문제를 신속하게 진단할 수 있는 안정적인 Observability 환경을 구축할 수 있었습니다. 이번 글에서는 OpenTelemetry의 기본 개념부터 저희가 어떤 과정을 통해 현재의 아키텍처를 완성하게 되었는지, 그 경험을 공유 드리고자 합니다.
OpenTelemetry: Observability의 표준
시스템에 문제가 생겼을 때 단순히 ‘CPU 사용량이 80%를 넘었다’와 같이 이미 알고 있는 시나리오를 확인하는 것을 모니터링 이라고 합니다. 반면, 관측 가능성(Observability) 은 ‘왜 특정 안드로이드 버전 사용자 그룹에서만 주문이 실패하고 있을까?’처럼 전혀 예측하지 못했던 복잡한 질문에 답할 수 있는 시스템의 능력을 의미합니다. OpenTelemetry는 바로 이 관측 가능성을 실현하기 위해 등장한 표준 기술로, 복잡한 시스템의 상태를 이해하는 데 필요한 데이터(Traces, Metrics, Logs)를 어떻게 수집하고 전송할지에 대한 업계 표준 약속이라고 생각할 수 있습니다.

https://opentelemetry.io/img/otel-diagram.svg
조금 더 기술적으로 정의하면, OpenTelemetry는 “Telemetry 데이터(Traces, Metrics, Logs)의 생성 및 수집을 표준화하기 위한 도구, API, SDK의 모음”입니다. CNCF의 주요 프로젝트 중 하나로, 기존의 분산 추적 표준이던 OpenTracing과 OpenCensus가 통합되어 탄생했습니다. 과거에는 Datadog, New Relic 같은 각 Observability 솔루션이 제공하는 독자적인 Agent와 SDK를 사용해야 했습니다. 이는 특정 벤더에 기술적, 비용적으로 종속되는 ‘Vendor Lock-in’ 문제를 일으킵니다. 예를 들어, 비용이나 정책상의 이유로 다른 솔루션으로 전환하려면, 데이터를 수집하는 모든 애플리케이션의 코드를 다시 수정해야 하는 막대한 엔지니어링 비용이 발생하게 됩니다.
OpenTelemetry는 이러한 문제를 해결하여 OTel 표준 API에 맞춰 애플리케이션을 한 번만 계측(Instrumentation)하면, 이후에는 Collector 설정 변경만으로 데이터를 원하는 어떤 백엔드로든 자유롭게 전송할 수 있습니다. 이는 마치 전 세계 어디서나 사용할 수 있는 ‘여행용 멀티 어댑터’처럼, 우리의 시스템을 특정 기술에 얽매이지 않고 비즈니스와 기술 환경 변화에 유연하게 대응할 수 있는 강력한 기반이 되어줍니다.
이러한 표준화를 실현하기 위해 OpenTelemetry 생태계의 모든 구성 요소는 OTLP(OpenTelemetry Protocol)라는 공통 언어를 사용합니다. OTLP는 Traces, Metrics, Logs 데이터를 SDK에서 Collector로, 또는 Collector에서 백엔드로 전송할 때 사용되는 표준화된 데이터 형식과 전송 방식을 정의합니다. OTLP는 고성능의 gRPC 또는 HTTP 통신을 기반으로 대용량의 Telemetry 데이터를 효율적으로 처리하며, 이를 통해 SDK 언어와 백엔드 시스템에 상관없이 일관된 데이터 전송을 보장합니다.
OpenTelemetry의 3가지 핵심 요소: Traces, Metrics, Logs
OpenTelemetry는 시스템을 관찰하기 위한 세 가지 핵심 데이터 유형(Signal)을 정의합니다. 이 세 가지 Signal이 유기적으로 결합될 때 비로소 시스템에 대한 깊이 있는 통찰력, 즉 Observability를 확보할 수 있습니다.
- Traces: 분산 환경에서 특정 요청의 전체 생명주기를 시각화하는 데이터입니다. 예를 들어, 사용자가 ‘예약하기’ 버튼을 누를 때 생성된 요청은 고유한
Trace ID를 갖게 됩니다. 이 요청이 인증 서비스, 상품 정보 서비스, 결제 서비스를 순차적으로 거칠 때, 각 서비스에서의 작업 단위는Trace ID를 공유하는 별개의 'Span'으로 기록됩니다. Traces를 통해 '결제 서비스의 카드사 API 연동 Span에서 3초 이상 소요되었다'와 같은 병목 지점을 정확히 찾아내고, 요청이 어떤 경로로 흘러갔는지 한눈에 파악할 수 있습니다. - Metrics: 시간의 흐름에 따라 측정된 시스템의 상태 정보입니다. CPU 사용량 같은 시스템 지표뿐만 아니라, ‘5분간 결제 성공 요청 수’처럼 계속 누적되는 카운터(Counter), ‘현재 활성 사용자 수’와 같이 오르내릴 수 있는 게이지(Gauge), ‘API 응답 시간 분포’를 분석하여 p95, p99 같은 백분위 응답 시간을 계산할 수 있는 히스토그램(Histogram) 등 애플리케이션의 상태를 정량적으로 파악하는 데 사용됩니다.
- Logs: 시스템에서 발생하는 특정 이벤트에 대한 타임스탬프가 기록된 텍스트 데이터입니다. 전통적으로 가장 많이 사용되어 온 데이터 유형이지만, OpenTelemetry는 여기에
Trace ID,Span ID와 같은 컨텍스트 정보를 더한 '구조화된 로그(Structured Logs)'를 지향합니다. 덕분에 특정 에러 로그를 발견했을 때, 그 로그가 어떤 사용자 요청(Trace)의 어느 작업(Span)에서 발생했는지 즉시 연결하여 문제의 상세한 원인과 전후 맥락을 파악하는 데 매우 유용합니다.
OpenTelemetry Collector: 데이터 파이프라인의 핵심
Collector는 애플리케이션에서 생성된 Telemetry 데이터를 수신(Receive), 처리(Process), 내보내기(Export)하는 역할을 수행하는 OpenTelemetry의 핵심 컴포넌트입니다. 애플리케이션 SDK와 백엔드 시스템 사이에서 중계자 역할을 하며, 데이터 처리 부담을 애플리케이션에서 분리하고 유연하며 강력한 파이프라인을 구성할 수 있게 해줍니다.

https://opentelemetry.io/docs/collector/img/otel-collector.svg
Collector는 다음과 같은 구성 요소로 이루어진 파이프라인을 통해 동작합니다.
- Receivers: OTLP, Jaeger, Prometheus 등 다양한 프로토콜로 데이터를 수신하는 입력단입니다.
- Processors: 수신된 데이터가 최종 목적지로 전달되기 전에 거치는 중간 처리 단계입니다. 데이터를 묶어서 보내는 일괄 처리(batching)로 네트워크 효율을 높이거나, 필요 없는 데이터를 필터링하고, 유용한 정보를 추가하는 등 다양한 데이터 가공을 담당합니다.
- Exporters: 처리된 데이터를 특정 목적지로 내보내는 출력단입니다. 이 목적지는 Loki나 Prometheus와 같은 최종 백엔드 시스템일 수도 있고, 디버깅을 위한 콘솔(logging)이나 다른 데이터 파이프라인으로 전달하는 중간 지점이 될 수도 있습니다. OpenTelemetry는 다양한 목적지에 맞춰 수십 가지의 Exporter를 기본적으로 제공하여 높은 확장성을 보장합니다.
이러한 구조 덕분에, 애플리케이션은 데이터 생성에만 집중할 수 있고, 데이터의 처리와 라우팅에 대한 복잡한 로직은 모두 Collector에서 중앙 관리할 수 있습니다.
여기어때 OpenTelemetry 구성
지금까지 OpenTelemetry의 핵심 개념과 Collector에 대해 알아보았습니다. 저희는 이러한 개념들을 바탕으로 안정성과 확장성, 그리고 관리 효율성을 고려하여 데이터 파이프라인을 구축했습니다.
지금부터는 저희가 현재의 아키텍처를 어떻게 설계하고 구성했는지, 그 과정을 다이어그램과 함께 단계별로 소개해 드리겠습니다.

여기어때 OpenTelemetry 구성
① Application에서 데이터 수집
모든 데이터 수집은 애플리케이션에 설치된 OpenTelemetry SDK 로부터 시작됩니다. 저희는 코드 변경을 최소화하고 도입 장벽을 낮추기 위해, SDK가 제공하는 자동 계측(Auto-Instrumentation) 방식을 주로 사용하고 있습니다. 이는 개발자가 직접 코드를 수정하지 않아도, SDK가 런타임에 널리 사용되는 라이브러리나 프레임워크(예: Spring, Django, Express.js)의 코드를 감지하고 자동으로 계측하는 강력한 기능입니다. 이를 통해 외부 HTTP 요청, DB 쿼리 등 표준적인 작업에 대한 Span 데이터가 자동으로 생성됩니다.
저희와 같은 쿠버네티스 환경에서는 OpenTelemetry Operator를 활용하여 이 과정을 더욱 자동화하고 중앙에서 관리할 수 있습니다. Operator를 사용하면, 개발자가 배포 명세(manifest)에 특정 어노테이션(annotation)만 추가하는 것만으로도 Operator가 Pod 생성 시점에 자동으로 SDK를 주입해 줍니다. 또한, SDK가 바라볼 Collector 주소나 샘플링 설정 등 모든 계측(Instrumentation) 관련 구성을 Operator의 Custom Resource(CR)를 통해 한곳에서 관리할 수 있어, 코드나 빌드 스크립트를 변경할 필요 없이 계측을 적용하고 설정을 일괄 변경할 수 있습니다.
이렇게 OpenTelemetry SDK로부터 생성된 데이터는 네트워크 효율성을 높이기 위해 SDK 내부에서 설정된 배치(Batch) 단위로 묶여 중앙 Gateway Collector로 전송됩니다.
② Gateway Collector에서 데이터 처리
Gateway Collector는 모든 애플리케이션의 SDK로부터 데이터를 수신하는 중앙 관문 역할을 합니다. Gateway라는 중앙 관문을 둠으로써 얻게 되는 이점은 다음과 같습니다.
- 정책의 중앙화: 모든 데이터에 공통적으로 적용해야 할 규칙을 Gateway Collector 한 곳에서 일괄적으로 관리할 수 있습니다. 예를 들어, 모든 Telemetry 데이터에 쿠버네티스 클러스터명을 공통으로 추가해야 할 때, 수백 개의 서비스 설정을 바꾸는 대신 Gateway Collector 설정 파일 하나만 수정하여 배포하면 됩니다.
- 애플리케이션 부담 감소: 데이터 정제나 복잡한 처리 로직, 샘플링 규칙 적용 등을 Gateway에 위임함으로써, 애플리케이션 SDK는 최소한의 작업만 수행하게 되어 비즈니스 로직에 더 많은 리소스를 집중할 수 있습니다.
- 아키텍처 단순화: 모든 Telemetry 데이터는 Gateway라는 단일 출구를 통해 외부(Kafka)로 나가게 되므로, 네트워크 및 보안 정책 관리가 용이해집니다.
이러한 이점들을 바탕으로, 저희는 Gateway Collector가 수신한 데이터를 processors를 통해 정제하고 exporters를 통해 Kafka로 전송하도록 파이프라인을 구성했습니다. 실제 설정 예시는 다음과 같습니다.
# Gateway Collector 설정 예시 (Helm Chart values.yaml)
mode: deployment
config:
# ...
processors:
transform/drop_unneeded_resource_attributes:
error_mode: ignore
trace_statements:
- context: resource
statements:
- delete_key(attributes, "process.command_args")
- delete_key(attributes, "process.executable.path")
# ... (불필요한 속성들 제거)
transform/add_default_namespace:
error_mode: ignore
trace_statements:
- context: resource
statements:
- set(attributes["service.namespace"], "unknown_namespace") where attributes["service.namespace"] == nil
transform/truncate_log_body:
error_mode: ignore
log_statements:
- context: log
statements:
- set(body.string, Substring(body.string, 0, 10000)) where Len(body.string) > 10000
exporters:
kafka:
# ...
sending_queue:
queue_size: 1000000000
sizer: bytes
batch:
flush_timeout: 200ms
max_size: 1000000
min_size: 1
service:
pipelines:
logs:
receivers:
- otlp
processors:
- memory_limiter
- transform/add_default_namespace
- transform/drop_unneeded_resource_attributes
- transform/truncate_log_body
- batch
exporters:
- kafka
traces:
# ...
metrics:
# ...
주요 설정
- Processors (
transform): 불필요한 리소스 속성(예:process.command_args)을 제거하고,service.namespace가 없는 경우 기본값을 추가하며, 로그 크기가 백엔드 제한을 초과하는 대용량 로그 본문을 잘라내는 등 데이터를 일괄 정제합니다. - Exporters (
kafka):sending_queue의sizer와batch옵션을 통해, Kafka가 허용하는 최대 메시지 크기(max_message_bytes, 1MB)를 초과하지 않도록 배치의 크기를 바이트(byte) 단위로 제어하여 안정적인 전송을 보장합니다.
③ Kafka: 안정성과 확장성의 핵심
이렇게 Gateway Collector를 거친 모든 데이터는 각각의 Signal(Logs, Traces, Metrics)에 맞는 Kafka 토픽으로 전달됩니다. 이처럼 Kafka를 파이프라인 중간에 둠으로써 얻게 된 이점은 다음과 같습니다.
- 데이터 유실 방지: 모든 데이터는 우선 Kafka에 안정적으로 저장되므로, Loki와 같은 최종 백엔드 시스템의 장애나 점검 여부와 관계없이 데이터 유실을 원천적으로 방지할 수 있습니다. Gateway Collector는 백엔드 상태와 무관하게 데이터를 Kafka에 전송하기만 하면 되고, 백엔드 시스템이 복구된 후 Kafka에 저장된 데이터부터 순차적으로 다시 처리할 수 있습니다.
- 트래픽 급증 대응: 대규모 프로모션 등으로 트래픽이 순간적으로 급증하더라도 Kafka가 이를 안정적으로 흡수하는 완충제 역할을 합니다. 후단의 Collector들은 자신들의 처리 능력에 맞게 데이터를 가져와 처리하므로, 갑작스러운 부하에도 시스템 전체가 안정적으로 운영될 수 있습니다.
- 시스템 간 디커플링: 데이터 생산자(Gateway)와 소비자(Signal별 Collector)가 완전히 분리됩니다. 덕분에 향후 Traces 데이터를 분석하는 새로운 시스템을 도입해야 할 때, 기존 파이프라인을 전혀 건드리지 않고 Kafka의 Traces 토픽을 구독하는 새로운 Collector만 추가하면 되므로 아키텍처를 유연하게 확장할 수 있습니다.
④ Signal Collector에서 최종 처리
Kafka의 각 토픽으로 전달된 데이터는 Traces, Metrics, Logs를 각각 전담하는 독립적인 Collector 그룹에 의해 소비(consume)됩니다. 저희는 Grafana LGTM 스택 을 활용하여, Trace는 Tempo , Metric은 Mimir , Log는 Loki 를 통해 데이터를 적재하고, Grafana를 통해 이 모든 데이터를 통합적으로 시각화하고 있습니다.

OpenTelemetry + Grafana LGTM
Collector를 이처럼 Signal별로 분리하여 구성하면 다음과 같은 이점을 얻을 수 있습니다.
- 독립적인 최적화 및 확장: 각 Signal은 데이터의 특성과 최종 백엔드의 처리량이 모두 다릅니다. Collector를 분리함으로써 각 파이프라인을 개별적으로 최적화하고 확장(scale-out)할 수 있습니다.
- 장애 격리: 특정 Signal(예: Logs)의 처리에 문제가 생기더라도 다른 Signal(예: Traces)의 파이프라인에 영향을 주지 않아, 장애 전파를 막고 전체 시스템의 안정성을 높일 수 있습니다.
다음은 Log Collector의 설정 예시입니다.
# Log Collector 설정 예시 (Helm Chart values.yaml)
mode: deployment
config:
receivers:
kafka:
brokers:
- "..."
autocommit:
enable: false # default = true
message_marking:
after: true # (default = false) 메시지가 성공적으로 export 된 후 commit
processors:
batch:
send_batch_size: 500 # default = 8192
send_batch_max_size: 500 # default = 0
exporters:
otlphttp:
endpoint: "..."
sending_queue:
enabled: false # (default = true)
service:
pipelines:
logs:
receivers:
- kafka
processors:
- memory_limiter
- batch
exporters:
- otlphttp
# KEDA
extraManifests:
- apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: "{{ .Values.fullnameOverride }}-keda-kafka-scaledobject"
namespace: "{{ .Release.Namespace }}"
spec:
scaleTargetRef:
name: "{{ .Values.fullnameOverride }}"
minReplicaCount: 1
maxReplicaCount: 6
pollingInterval: 30
triggers:
- type: kafka
metadata:
bootstrapServers: '{{ join "," .Values.config.receivers.kafka.brokers }}'
consumerGroup: "{{ .Values.config.receivers.kafka.group_id }}"
topic: "{{ .Values.config.receivers.kafka.logs.topic }}"
lagThreshold: "500"
offsetResetPolicy: "latest"
limitToPartitionsWithLag: "true"
authenticationRef:
name: "{{ .Values.fullnameOverride }}-keda-trigger-auth-kafka-credential"
주요 설정
- Receivers (
kafka):autocommit.enabled: false와message_marking.after: true옵션은 데이터 유실 방지를 위한 핵심 설정입니다. 이 설정은 Kafka로부터 메시지를 가져온 후, 백엔드로의 전송이 성공적으로 완료되었을 때만 Kafka offset을 커밋하도록 강제합니다. 덕분에 Collector가 비정상적으로 종료되더라도, 재시작 시 마지막으로 성공했던 지점부터 데이터를 다시 처리하므로 데이터 유실을 방지할 수 있습니다. - Processors (
batch): 최종 백엔드 시스템의 최대 요청 크기와 네트워크 효율성을 고려하여send_batch_size를 조정합니다. - Exporters (
otlphttp): 최종 백엔드로 데이터를 전송합니다. 저희는sending_queue.enabled옵션을false로 설정하여 exporter 단계의 자체 재시도 큐를 비활성화하고, 대신kafkareceiver의 offset commit 메커니즘을 통해 데이터 전송 실패 시 재처리를 보장합니다. 이는 백엔드 전송에 실패했음에도 불구하고 처리되지 않은 메시지의 offset이 커밋되어 데이터가 누락되는 상황을 방지합니다. - KEDA (
ScaledObject): KEDA(Kubernetes Event-driven Autoscaling) 를 연동하여 Kafka Lag 기반의 이벤트 기반 오토스케일링을 구성했습니다.lagThreshold설정은 Kafka 토픽에 처리되지 않은 메시지(Lag)가 해당 값을 초과하면 Collector의 Pod 수를 자동으로 늘려, 트래픽 급증에 신속하게 대응할 수 있습니다.
맺음말
지금까지 OpenTelemetry의 기본 개념과, 이를 활용하여 여기어때가 어떻게 안정적인 Observability 아키텍처를 구축했는지 그 과정을 공유해 드렸습니다.
OpenTelemetry와 Kafka를 활용한 현재의 아키텍처를 통해 저희는 벤더 종속성에서 벗어나 유연하고 확장 가능한 데이터 파이프라인을 구축했으며, 어떤 상황에서도 Telemetry 데이터를 유실 없이 안정적으로 수집할 수 있는 견고한 기반을 마련했습니다.
이 글이 복잡한 MSA 환경에서 안정적인 Observability를 고민하는 분들께 조금이나마 도움이 되기를 바랍니다.
긴 글 읽어주셔서 감사합니다.