0. Contents
1. Overview
메인 앱은 그대로, Adapter가 형식을 바꿔 하나의 얼굴을 만든다
컨테이너가 통일하지 못하는 것: 내부 데이터 형식
- 컨테이너는 서로 다른 언어와 라이브러리로 만들어진 앱을 통일된 방식으로 실행한다
- 하지만 실행 환경이 통일됐다고 해서 내부 데이터 형식까지 통일되는 건 아니다
- 팀마다 다른 기술을 쓴다
- 서비스마다 메트릭과 로그를 각자의 방식으로 출력한다
Adapter 패턴: 표준 인터페이스로 변환
- Adapter 패턴은 이 문제에 대한 해답이다
- 메인 앱을 전혀 수정하지 않고, 옆에 Adapter 컨테이너를 붙인다
- 복잡한 내부 형식을 외부 세계가 이해할 수 있는 표준 인터페이스로 변환한다
- 전원 어댑터처럼 — 기기(메인 앱)는 그대로이고, 어댑터만으로 호환성을 확보한다
2. Problem
2.1 이질성이 만드는 충돌
서비스마다 다른 출력 형식
- 분산 시스템에서는 각 서비스가 서로 다른 형식으로 데이터를 출력하는 것이 자연스럽다
형식이 셋이면 수집하는 쪽이 셋을 다 알아야 한다
늘어나는 파싱 부담
- 외부 시스템이 모든 형식을 각각 파싱해야 한다면, 서비스가 늘어날수록 부담도 비례해서 늘어난다
- 이것이 이질성(Heterogeneity) 이 만드는 충돌이다
2.2 모니터링 관점의 문제
모니터링 도구의 기대: 통합된 뷰
- 모니터링 도구는 시스템 전체의 통합된 뷰를 기대한다
- 그런데 서비스마다 메트릭을 노출하는 포맷도, 프로토콜도 다르다면
- 단일 모니터링 솔루션으로 전체를 바라보는 것이 불가능해진다
적용 전에는 서버가 형식 셋을 알아야 하고, 적용 후에는 하나만 알면 된다
두 가지 불일치
- 형식 불일치 — 앱의 출력 형식이 모니터링 도구가 기대하는 형식과 다르다
- 접근 방식 불일치 — Prometheus처럼 HTTP 엔드포인트로 스크랩해야 하는데, 앱이 HTTP를 지원하지 않는다
3. Solution — Adapter 패턴
3.1 동작 구조
Sidecar 위의 단일 목적 컨테이너
- Adapter 컨테이너는 Sidecar 패턴 위에서 동작한다
- 같은 Pod 안에서 메인 앱과 볼륨을 공유한다
- 단 하나의 목적 — 형식 변환과 표준 인터페이스 노출 — 에만 집중한다
두 컨테이너는 emptyDir 하나로만 이어진다
컨테이너별 단일 책임
- 각 컨테이너의 단일 책임이 명확하다
| 컨테이너 | 역할 | 모르는 것 |
|---|---|---|
| Main App | 비즈니스 로직 수행 + 커스텀 형식 출력 | 외부 모니터링 도구의 존재 |
| Adapter | 내부 형식 → 표준 형식 변환 + HTTP 노출 | 메인 앱의 비즈니스 로직 |
3.2 예제 — Prometheus 모니터링
시나리오: 랜덤 숫자 생성기 모니터링
- 랜덤 숫자 생성기 앱을 Prometheus로 모니터링하는 시나리오다
- 앱은 숫자를 생성하는 데 걸린 시간을 로그 파일에 기록한다
메인 앱이 출력하는 로그 형식:
2024-01-15 10:23:45 duration=0.0023s value=42
Prometheus가 기대하는 형식:
# HELP random_duration 랜덤 숫자 생성 소요 시간
# TYPE random_duration gauge
random_duration 0.0023
예제의 문제: 형식과 엔드포인트
- 두 가지 문제가 있다
- 형식이 다르다
- HTTP 엔드포인트가 없다
- Adapter가 둘 다 해결한다
로그 파일 하나를 쓰고, 읽고, 긁어 간다
요청 시점 읽기와 Pull 방식
- 이 예제의 Adapter는 요청이 올 때 로그를 읽도록 만들어져 있다
- Kubernetes가 정한 규칙은 아니고, exporter 이미지를 어떻게 만들었느냐에 따라 달라진다
- Prometheus의 Pull 방식과 자연스럽게 맞아떨어지는 구조다
3.3 Example 17-1 YAML 분석
apiVersion: apps/v1
kind: Deployment
metadata:
name: random-generator
spec:
replicas: 1
selector:
matchLabels:
app: random-generator
template:
metadata:
labels:
app: random-generator
spec:
containers:
# ① 메인 앱 컨테이너 — 랜덤 숫자 서비스, 8080 포트
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: LOG_FILE
value: /logs/random.log # 로그 경로를 환경변수로 관리
ports:
- containerPort: 8080 # 서비스 트래픽 포트
protocol: TCP
volumeMounts:
- mountPath: /logs
name: log-volume # 이 경로에 로그 파일 작성
# ② Adapter 컨테이너 — Prometheus 형식으로 변환, 9889 포트
- image: k8spatterns/random-generator-exporter
name: prometheus-adapter
env:
- name: LOG_FILE
value: /logs/random.log # 메인 앱과 동일한 경로 (같은 파일 읽기)
ports:
- containerPort: 9889 # 모니터링 스크랩 포트
protocol: TCP
volumeMounts:
- mountPath: /logs
name: log-volume # 같은 볼륨 마운트
volumes:
- name: log-volume
emptyDir: {} # Pod 생성 시 빈 디렉토리 자동 생성
설계 포인트 세 가지
- 포트 분리 — 서비스 트래픽(:8080)과 모니터링(:9889)이 완전히 독립되어 있어 서로 간섭하지 않는다
- 환경변수로 경로 관리 —
LOG_FILE을 두 컨테이너 모두 환경변수로 관리한다- 경로가 바뀌어도 YAML만 수정하면 된다
- 하드코딩이 없다
- emptyDir의 역할 — Pod 생성 시 자동으로 빈 디렉토리가 만들어진다
- 두 컨테이너가 같은 경로(
/logs)로 마운트한다 - 메인 앱이 쓰면 Adapter가 읽는다
- 두 컨테이너를 연결하는 유일한 접점이다
- 두 컨테이너가 같은 경로(
포트 역할 구분
| 접근 주체 | 포트 | 컨테이너 |
|---|---|---|
| 외부 사용자/클라이언트 | :8080 | random-generator (랜덤 숫자 서비스) |
| Prometheus Server | :9889 | prometheus-adapter (메트릭 스크랩) |
3.4 로깅에 적용하기
메트릭에서 로깅으로
- Adapter 패턴은 메트릭뿐 아니라 로깅에도 그대로 적용된다
- 각 서비스가 서로 다른 형식과 상세 수준으로 로그를 출력하는 문제를 해결한다
로깅 Adapter의 세 가지 작업
- Adapter는 세 가지 작업을 수행한다
로깅 Adapter: 정규화 → 정리 → 보강
정규화(Normalize)
- 서로 다른 형식의 로그를 통일된 구조로 변환한다
Java: "2024-01-15 10:23:45 [ERROR] NullPointerException"
Python: {"level":"error","msg":"KeyError","ts":1705312345}
↓ Adapter 정규화
{"timestamp":"2024-01-15T10:23:45Z","level":"ERROR","message":"..."}
{"timestamp":"2024-01-15T10:23:45Z","level":"ERROR","message":"..."}
정리(Clean up)
- DEBUG, TRACE 같은 노이즈를 제거한다
- 민감 정보를 마스킹한다
보강(Enrich)
- Chapter 14의 Self Awareness 패턴을 함께 활용한다
- Downward API로 Pod 자신의 메타데이터를 환경변수로 받는다
- 로그에 컨텍스트를 자동으로 추가한다
# Self Awareness 패턴 — Downward API
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
Before (컨텍스트 없음):
{"level":"ERROR","message":"NullPointerException"}
After (Self Awareness로 보강):
{
"level": "ERROR",
"message": "NullPointerException",
"pod": "random-generator-7d6b9c-xk2p9",
"node": "worker-node-1",
"namespace": "production"
}
교체 가능한 Adapter
- 메인 앱은 Prometheus도, ELK도, Fluentd도 모른다
- Adapter만 교체하면 모니터링 도구를 바꿀 수 있다
4. Discussion
4.1 리버스 프록시로서의 Adapter
Sidecar의 특수화이자 리버스 프록시
- Adapter는 Sidecar 패턴의 특수화다
- 그리고 외부 시스템 관점에서 보면 리버스 프록시이기도 하다
같은 대리인, 반대 방향
Adapter의 본질: 복잡성 숨기기
- Adapter 뒤에 어떤 언어, 어떤 형식의 앱이 있든 외부 시스템은 신경 쓰지 않아도 된다
- 복잡성을 숨기고, 통합된 인터페이스만 노출한다
- 이것이 Adapter의 본질이다
이름을 구분하는 이유
- “Sidecar 컨테이너 추가했어요"라고 하면 무엇을 하는지 알 수 없다
- “Adapter 컨테이너 추가했어요"라고 하면 형식 변환과 인터페이스 통일이 목적임을 즉시 전달한다
- 패턴 이름 자체가 설계 의도를 전달하는 언어다
4.2 Sidecar 패턴 3형제
나란히 본 세 패턴
- Sidecar 패턴에서 파생된 세 가지 패턴을 나란히 보면 관계가 명확해진다
Sidecar에서 갈라진 두 갈래, 방향이 반대다
| 구분 | Sidecar | Adapter | Ambassador |
|---|---|---|---|
| 역할 | 보조 기능 일반 | 형식 변환 특화 | 외부 연결 특화 |
| 방향 | 내부 지원 | 외부→내부 (Reverse) | 내부→외부 (Forward) |
| 목적 | 다양 | 인터페이스 통일 | 외부 서비스 추상화 |
| 예시 | 로그 수집, 모니터링 | Prometheus 변환 | DB 연결, API 게이트웨이 |
Adapter 패턴의 핵심은 “메인 앱이 외부를 전혀 몰라도 된다는 것” 이다. 앱은 자기 언어로 자기 형식대로 데이터를 출력하고, Adapter가 모든 변환 책임을 전담한다. 이것이 클라우드 네이티브에서 이질적인 시스템들을 하나로 묶는 방법이다.
