ress 의 기술블로그
monitoring2026년 3월 27일

비즈니스 메트릭 수집기를 Go로 분리한 이유

인프라 메트릭은 있는데 비즈니스 메트릭이 없다. 좌석 판매 속도, 결제 전환율 같은 지표를 수집하기 위해 Go 별도 서비스를 만든 아키텍처 결정 과정

한 줄 요약#

시스템이 건강한지는 알 수 있는데, 비즈니스가 건강한지는 알 수 없었다. 좌석/결제/대기열 메트릭을 수집하는 Go 별도 서비스를 만들어서 비즈니스 관측성을 확보했다.


🔥 배경: 부하테스트에서 답할 수 없는 질문들#

Goti는 OTel Java Agent + Alloy + Mimir로 인프라/애플리케이션 메트릭을 수집하고 있었어요.

계층수집 메트릭도구
인프라CPU, Memory, Networkkube-prometheus-stack
JVMHeap, GC, ThreadOTel Java Agent
HTTPLatency (p50/p95/p99), Error Rate, RPSOTel Java Agent
DB PoolHikariCP active/idle/pendingOTel Java Agent

이 메트릭들은 **시스템이 건강한가?**를 알려줍니다. 하지만 K6로 5,000 VU 부하테스트를 돌리면서 이런 질문에 답할 수 없었어요:

  • 좌석이 몇 퍼센트 팔렸는가? 분당 판매 속도는?
  • 결제 전환율은? 타임아웃으로 잠금이 풀린 비율은?
  • 대기열에서 기다리는 사람 수 대비 남은 좌석은?
  • 현재 속도로 몇 분 후 매진인가?

이 지표들은 HTTP 메트릭으로는 유도 불가능해요. DB와 Redis의 현재 상태 스냅샷에서만 계산할 수 있습니다.

수집해야 할 메트릭은 25개 이상이었어요.

카테고리메트릭 예시데이터 소스
좌석goti_seats_remaining_total, goti_seats_sold_totalPostgreSQL
결제goti_payment_completed_total, goti_payment_timeout_totalPostgreSQL
경기goti_match_phase, goti_match_infoPostgreSQL
대기열goti_queue_length_total, goti_queue_gate_activeRedis
파생goti_seats_fill_ratio, goti_seats_sell_rate_per_min, goti_seats_estimated_soldout_minutes계산

🤔 대안 비교#

A. Spring Boot에 Actuator 엔드포인트 추가#

goti-server에 커스텀 메트릭을 추가하는 방식이에요.

@Component
public class SeatMetrics {
    private final MeterRegistry registry;

    @Scheduled(fixedRate = 15000)
    void updateSeatMetrics() {
        long sold = seatRepo.countByStatus(SOLD);
        registry.gauge("goti_seats_sold_total", sold);
    }
}

가장 단순하지만, 치명적인 문제가 있어요.

부하테스트 시 서버가 가장 바쁜 순간에, 같은 JVM에서 3.6M row seat_statuses JOIN 쿼리를 돌리면 HikariCP 커넥션 풀 경합이 발생합니다. 메트릭 수집이 비즈니스 쿼리와 커넥션을 놓고 경쟁하게 되는 거예요. 가장 메트릭이 필요한 순간에 오히려 부하테스트 결과를 오염시킵니다.

B. Go 별도 서비스#

독립 Go 서비스가 DB/Redis를 직접 읽어 /metrics로 노출하는 방식이에요.

C. Alloy receiver 플러그인#

Alloy(OTel Collector 기반)에 커스텀 SQL receiver를 추가하는 방식이에요. 가능하지만 SQL 기반 rate 계산 한계가 있고, 복수 datasource 설정이 복잡합니다.

비교#

항목A. Spring BootB. Go 서비스C. Alloy 플러그인
배포 결합도메트릭 변경 = 서버 재배포완전 독립Alloy 재배포만
서버 부하같은 JVM, 커넥션 풀 공유별도 커넥션 (maxConns=3)없음
파생 지표가능 (Java)가능 (Go, 스냅샷 비교)제한적 (SQL만)
리소스+50MB JVM 메모리~10MB 바이너리, ~30MB 메모리Alloy에 추가 부하
SQL 자유도JPA entity 매핑pgx 직접 쿼리, 크로스 스키마 JOINSQL receiver 설정

✅ 결정: Go 별도 서비스#

핵심 근거 4가지#

1. 관측과 비즈니스의 관심사 분리

메트릭 수집은 비즈니스 로직이 아니에요. 관측 주기 변경, 메트릭 추가가 비즈니스 서비스 배포를 트리거하면 안 됩니다. collector만 배포하면 되니까 메트릭 추가/변경이 분 단위로 가능해요.

2. 부하테스트 안정성

가장 메트릭이 필요한 순간이 부하테스트 중인데, 그때 서버가 가장 바빠요. 독립 커넥션 풀(maxConns=3)로 비즈니스 트래픽과 완전 격리됩니다. 서버가 죽어도 메트릭은 계속 수집돼요.

3. MSA 대비

현재 5개 서비스로 분리 완료 상태예요. 각 서비스에 메트릭 코드를 넣으면 크로스 서비스 파생 지표(대기열 대비 잔여 좌석 등) 계산이 불가능합니다. 단일 수집기가 모든 서비스의 DB를 읽어 통합 뷰를 제공해요.

4. Go 선택 이유

  • client_golang은 Prometheus의 공식 Go 클라이언트 — 가장 성숙하고 문서 풍부
  • distroless 바이너리 ~10MB (Java의 1/20), 메모리 ~30MB
  • pgx 드라이버로 크로스 스키마 JOIN 자유 (JPA entity 매핑 불필요)
  • 인프라/DevOps 담당(본인)만 관리하므로 팀 학습 비용 없음

아키텍처#

PostgreSQL ──┐
             ├── goti-metrics-collector (Go) ──→ :9090/metrics
Redis    ────┘         │
                       │  Alloy ServiceMonitor scrape
                       ↓
                    Alloy → Mimir → Grafana Dashboard

구현 결과#

파일줄 수역할
cmd/collector/main.go156진입점, DB/Redis 연결, graceful shutdown
internal/metrics/registry.go202Prometheus 메트릭 25+ 정의
internal/poller/db_poller.go202PostgreSQL 폴링 (좌석/주문/경기)
internal/poller/redis_poller.go64Redis 폴링 (대기열/가드레일)
internal/aggregator/aggregator.go100파생 지표 계산
internal/config/config.go67환경변수 설정
합계791

프레임워크 없이 net/http + promhttp만 사용합니다. 의존성은 pgx/v5, go-redis/v9, client_golang 세 개뿐이에요.


⚠️ 트레이드오프#

인프라 오버헤드: ECR 레포, CI/CD 파이프라인, K8s Deployment, ServiceMonitor, NetworkPolicy, AuthorizationPolicy 전부 새로 구축해야 했어요. 실제로 3-Layer 보안 개통에 1.5세션이 소요됐습니다.

DB 스키마 결합: collector가 DB 테이블 구조를 직접 알아야 해요. 스키마 변경 시 collector도 수정 필요합니다. Read Replica 사용으로 write path에는 영향 없어요.

기술 스택 분산: 팀 전체는 Java/Spring Boot인데 Go 서비스가 하나 추가됩니다. 이 서비스는 인프라 담당자만 관리하므로 팀 학습 비용은 없어요.


📚 교훈#

"관측 가능하다"와 "비즈니스를 이해한다"는 다르다#

CPU, 메모리, 레이턴시가 잘 보인다고 해서 시스템을 이해하는 건 아니에요. "p99 레이턴시가 200ms다"보다 **"좌석 80% 팔렸고, 현재 속도로 3분 후 매진"**이 의사결정에 훨씬 유용합니다.

인프라 메트릭은 "문제가 있다"를 알려주고, 비즈니스 메트릭은 "무엇이 일어나고 있다"를 알려줘요.

메트릭 수집은 비즈니스 코드에 넣지 말자#

부하테스트 중에 메트릭이 가장 필요한데, 그때 서버가 가장 바빠요. 같은 프로세스에서 집계 쿼리를 돌리면 관측이 시스템을 방해하는 역설이 발생합니다. 관측 로직은 비즈니스 로직과 물리적으로 분리하는 게 맞아요.


다음 글 예고#

791줄짜리 서비스를 만들었는데, 실제로 Grafana에 메트릭이 뜨기까지는 훨씬 많은 삽질이 필요했어요. 다음 글에서는 3-Layer 보안(Istio L7 + NetworkPolicy L3/L4) 개통부터 DB 스키마 불일치까지 E2E 트러블슈팅 과정을 다룹니다.

Goti Metrics Collector Series1 / 2

다음에 읽을 글