테스트 결과 집계와 리포팅으로 품질 가시성 만드는 법

분산 테스트 러너 결과를 표준화해 수집·저장·분석하고, 품질 게이트와 플래키 탐지까지 연결하는 테스트 리포팅 체계를 정리한다.

2026-08-14 · 최초 발행 2025-12-23

흩어진 테스트 결과를 배포 판단에 쓰려면

테스트 실행 결과 집계·리포팅 체계는 여러 테스트 러너가 만든 결과를 한 형식으로 모아 저장하고, 품질 지표와 리포트를 자동으로 제공하는 데이터 파이프라인 및 서비스 구성이다. CI/CD 속도가 빨라지고 마이크로서비스, 멀티러너, 멀티플랫폼 환경이 늘수록 테스트 신뢰도와 피드백 루프를 따로 관리하기 어려워진다.

이 체계의 범위는 Agent 또는 HTTP 기반 수집, 스트리밍·배치 처리, 표준 스키마 저장소, API와 대시보드, 알림 및 배포 차단 게이트까지 이어진다. 이벤트 기반 수집과 스키마 버저닝, 멱등 집계, 빌드·릴리스·커밋 시간축 분석, SLA/SLO와 연결된 품질 게이트가 중심 메커니즘이다.

결과가 들어와 리포트가 되기까지

XML/JSON 업로드배치/스트리밍 전송중복 제거/스키마 검증쓰기 트랜잭션, upsert쿼리/집계대시보드/게이트파싱 오류 또는 재시도 초과테스트 러너(JUnit, PyTest,Cypress, K6)수집기(Agent/HTTP Ingest)메시지 큐/스트리밍(Kafka)집계기(Stream/BatchProcessor)저장소(PostgreSQL/ClickHouse/TSDB)리포팅 API(REST/GraphQL)소비자(BI, CI 품질 게이트,알림)DLQ(죽은 편지 큐)

수집 계층은 JUnit XML, TestNG, PyTest, Cypress, K6 등 서로 다른 결과 포맷을 받아 HTTP 또는 Agent 업로드로 전달한다. 배치 업로드와 Kafka 같은 스트리밍 전송을 함께 지원할 수 있으며, 네트워크 장애에는 재시도와 버퍼링이 필요하다. 결과 원본은 보존하고, 필드 매핑과 PII 마스킹 같은 전처리 규칙을 적용한다.

정규화 단계에서는 test_id, suite, case_name, status, duration_ms, retries, commit_sha, build_id, service, env, error_type, timestamp를 공통 필드로 다룬다. Avro 또는 JSON Schema 기반 버전 관리와 하위 호환 전략을 두고, test_id+build_id 멱등키로 중복 전송을 제거해 재전송에도 안전하게 만든다.

저장소는 최근 데이터를 다루는 OLTP RDB와 장기 추이를 분석하는 OLAP을 나누는 구성이 가능하다. PostgreSQL 같은 RDB와 ClickHouse 같은 OLAP을 함께 두고, 최근 N일은 RDB, 장기 추이는 OLAP에 보관한다. commit_ts, service, status 기준 인덱스와 파티셔닝으로 조회 성능을 확보하며, 원시 결과와 집계 지표는 보존 기간을 분리한다. GDPR 및 개인정보 제거 정책도 이 단계에서 적용한다.

리포팅 서비스는 API 게이트웨이를 통해 서비스·릴리스·브랜치별 실패율, 평균 소요 시간, flake율을 제공한다. Grafana, Metabase, Looker 등의 대시보드와 연결하고 Slack, Email, PR 코멘트로 알릴 수 있다. 커버리지, 실패 임계, 플래키 한도를 품질 게이트로 연결하면 배포 차단과 승인 흐름까지 이어진다.

신뢰할 수 있는 집계를 위한 운영 기준

플래키 테스트는 이력에서 상태 전환 빈도를 분석해 탐지하고, 자동 재시도와 샤딩 제어에 반영한다. 테스트에는 서비스·도메인·위험도 라벨과 소유권을 부여하고, SLA 합의도 문서화한다. 스키마, 규칙, 게이트 정책의 변경 이력과 롤백 절차를 남겨야 리포트의 판단 근거가 유지된다.

필수 필드는 test_id, suite, case_name, status(pass/fail/skip/flaky), duration_ms, retries, build_id, commit_sha, service, env, timestamp로 정한다. 멱등키와 자연키를 구분하고 상태 코드 및 오류 유형 사전을 운영한다.

입력은 XML/JSON 업로드나 배치 폴더 스캔이 될 수 있다. 처리 단계는 스키마 검증, 변환(Map/Normalize), 중복 제거, 윈도우·세션 기반 집계로 이어지고, 원시 이벤트 저장과 요약 테이블(materialized view) 생성을 함께 수행한다.

파싱 오류는 DLQ로 격리한 뒤 샘플링 재처리와 공급자 피드백 루프를 운영한다. 최소 적어도 한 번(at-least-once) 전송과 멱등 upsert를 결합해 최종 일관성을 확보하며, 대량 재처리에서는 파티션 키 재조정과 백필 백오프를 적용한다.

통신은 TLS로 보호하고 OIDC/OAuth 토큰 검증, RBAC, 비밀값 및 PII 마스킹, 스키마 레지스트리 권한 분리를 적용한다. 스트리밍의 실시간성과 배치의 단순성, 원시 보존 기간과 저장 비용, 게이트 엄격도와 개발 효율 사이의 균형도 함께 결정해야 한다.

처리 방식에 따른 선택 기준

항목 배치+RDB 중심 스트리밍+OLAP 혼용
성능 단일 노드 IOPS 한계, 대량 재처리 부담 고처리량, 실시간 근사 응답
확장성 수직 확장 중심, 샤딩 난도 높음 수평 확장 용이, 파티션 기반
일관성 강한 트랜잭션, 단순 쿼리 최종 일관성, 멱등 설계 필요
안정성 아키텍처 단순, 운영 용이 구성요소 증가, 운영 난이도 상승
운영 편의 비용 예측 용이, 러닝커브 낮음 지표 다양화·대시보드 최적화 용이

초기에는 배치와 RDB로 시작할 수 있다. 트래픽이 늘고 실시간 품질 지표 요구가 커질 때 스트리밍 단계를 도입하는 방식이 가능하다.

수집기와 저장소 구현 예시

다음 수집기는 requests==2.x, junitparser==3.x를 전제로 JUnit 결과를 HTTP Ingest에 POST 전송한다.

# python3.11
# pip install requests junitparser
import os, json, time, hashlib
import requests
from junitparser import JUnitXml

INGEST_URL = os.environ.get("INGEST_URL", "https://reporting.example.com/ingest")
TOKEN = os.environ.get("INGEST_TOKEN")

def to_event(suite_name, case, build_id, service, env):
    test_id = hashlib.sha1(f"{suite_name}:{case.classname}.{case.name}".encode()).hexdigest()
    status = "pass"
    if case.result:
        rtype = case.result[0]._tag  # failure, error, skipped
        status = "fail" if rtype in ("failure", "error") else "skip"
    return {
        "schema_version": "1.0",
        "test_id": test_id,
        "suite": suite_name,
        "case_name": f"{case.classname}.{case.name}",
        "status": status,
        "duration_ms": int((case.time or 0) * 1000),
        "retries": 0,
        "build_id": build_id,
        "commit_sha": os.environ.get("GIT_SHA", ""),
        "service": service,
        "env": env,
        "timestamp": int(time.time() * 1000),
    }

def post_events(events):
    headers = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
    resp = requests.post(INGEST_URL, headers=headers, data=json.dumps(events), timeout=10)
    resp.raise_for_status()

if __name__ == "__main__":
    xml = JUnitXml.fromfile("junit.xml")
    events = []
    for suite in xml:
        for case in suite:
            events.append(to_event(suite.name, case, os.environ.get("BUILD_ID","local"),
                                   os.environ.get("SERVICE","unknown"), os.environ.get("ENV","dev")))
    # 배치 전송 및 재시도
    for i in range(3):
        try:
            post_events(events)
            break
        except Exception as e:
            if i == 2: raise
            time.sleep(2 ** i)

PostgreSQL에서는 test_idbuild_id를 유니크 키로 두어 멱등 upsert의 기준으로 사용한다.

create table if not exists test_results (
  id bigserial primary key,
  test_id varchar(64) not null,
  suite text not null,
  case_name text not null,
  status varchar(10) not null, -- pass/fail/skip/flaky
  duration_ms integer not null,
  retries smallint not null default 0,
  build_id text not null,
  commit_sha char(40),
  service text not null,
  env text not null,
  event_ts timestamptz not null default now(),
  unique (test_id, build_id) -- 멱등 upsert 키
);
create index on test_results (service, event_ts desc);
create index on test_results (status, event_ts desc);

품질 지표를 운영에 연결하는 점검 항목

실패율, 평균 및 백분위수(p95) 소요 시간, 플래키율의 SLO와 게이트 임계를 정한다. 러너 수를 자동 조정하고 느린 테스트를 분리해 샤딩과 병렬화를 운영한다. 처리량, 지연, DLQ 비율처럼 파이프라인 자체의 메트릭도 대시보드에서 확인한다.

장기 보관 데이터는 OLAP 압축을 사용하고, 원시 이벤트 TTL과 요약 테이블 우선 조회로 비용을 통제한다.

이 체계를 적용하면 로그 탐색에서 대시보드 원클릭 전환을 기준으로 빌드 실패 원인 식별 시간을 50% 이상 단축할 수 있다. 히스토리 기반 자동 재시도와 격리로 플래키율 3060% 저감을 기대할 수 있고, 샤딩·스킵 규칙과 실패 시 조기 실패 Fast-Fail을 통해 평균 빌드 시간을 1025% 단축할 수 있다. 변경 이력과 지표를 보존하면 품질 가시성과 책임 소재가 명확해지고, 컴플라이언스·감사 대응과 회귀 결함의 사전 차단에도 도움이 된다.

테스트 자동화품질 지표CI/CD플래키 테스트품질 게이트