06. Storage Design
목적
Trace 저장 방식과, 향후 다른 저장소(PostgreSQL, ClickHouse 등)로 교체하기 위한 구조를 설명한다.
범위
collector 모듈 내 io.apmx.collector.trace 패키지의 저장소 구현.
설계
MVP 기본값: H2 (파일 모드)
외부 DB 설치 없이 바로 실행해 볼 수 있도록 H2를 파일 모드(jdbc:h2:file:./data/apmx-collector)로 기본 설정했다. 데이터는 저장소 루트의 data/ 디렉터리에 생성되며 .gitignore에 포함되어 커밋되지 않는다.
Repository/Adapter 분리
[Controller] → TraceRepository (interface, port)
▲
│ 구현
TraceRepositoryImpl (adapter)
│ 사용
TraceJpaRepository (Spring Data JPA)
│
H2 / PostgreSQLTraceRepository 인터페이스는 도메인 모델(Trace, ServiceSummary, UriStatistic)만 다루고 JPA 관련 타입을 노출하지 않는다. 따라서 저장소를 교체하더라도(JPA → 다른 방식) 이 인터페이스의 구현체만 바꾸면 되고, Controller/API 코드는 변경할 필요가 없다.
PostgreSQL로 전환하는 방법
코드 변경 없이 환경변수만 바꾸면 된다 (collector/src/main/resources/application.yml 참고).
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/apmx
export SPRING_DATASOURCE_USERNAME=apmx
export SPRING_DATASOURCE_PASSWORD=apmxorg.postgresql:postgresql 드라이버는 이미 collector/build.gradle.kts에 runtimeOnly로 포함되어 있다.
테이블 구조
trace 테이블 (JPA ddl-auto: update로 자동 생성, MVP 한정 - 운영 전환 시 마이그레이션 도구(Flyway 등) 도입 필요):
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | bigint (PK) | 내부 식별자 |
| trace_id | varchar(64), unique | Agent가 생성한 Trace ID |
| service_name | varchar(128) | 서비스명 |
| host_name | varchar(128) | 호스트명 |
| language | varchar(32) | 예: java |
| runtime | varchar(32) | 예: spring-boot |
| method | varchar(16) | HTTP Method |
| uri | varchar(512) | 요청 URI |
| remote_addr | varchar(64), nullable | 요청 클라이언트 IP (X-Forwarded-For 우선) |
| status_code | int | HTTP 상태 코드 |
| start_time / end_time | bigint | epoch millis |
| duration_ms | bigint | 처리 시간(ms) |
| error | boolean | 에러 여부 |
| error_message | varchar(1024) | 예외 메시지 (nullable) |
| request_headers_json | clob, nullable | 요청 헤더 (JSON 문자열, 민감 헤더는 Agent에서 이미 제외됨) |
| response_headers_json | clob, nullable | 응답 헤더 (JSON 문자열, 민감 헤더는 Agent에서 이미 제외됨) |
헤더는 조회 패턴이 Trace 상세 화면 1건 표시로 한정되어 있어 별도 정규화 테이블 없이 JSON 문자열 컬럼으로 저장한다 (과설계 방지). apmx.header.capture.enabled=false(기본값)인 Agent가 보낸 Trace는 두 컬럼 모두 null이다.
(service_name, start_time), (trace_id)에 인덱스를 두어 서비스별 조회/Trace 단건 조회 성능을 확보했다.
span 테이블 (@ApmxTrace 메서드 호출 스택, 하나의 Trace에 0개 이상 매핑):
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | bigint (PK) | 내부 식별자 |
| trace_id | varchar(64) | 소속된 Trace의 ID (trace.trace_id, FK 제약 없이 값만 참조) |
| span_id | varchar(64), unique | Agent가 생성한 Span ID |
| parent_span_id | varchar(64), nullable | 부모 Span ID. 최상위 Span은 trace_id와 동일한 값 |
| class_name | varchar(256) | 계측된 메서드의 클래스명 |
| method_name | varchar(128) | 계측된 메서드명 |
| args | varchar(1024), nullable | 인자값 (안전 타입만 값, 그 외는 클래스명만) |
| start_time / end_time | bigint | epoch millis |
| duration_ms | bigint | 처리 시간(ms) |
| error | boolean | 예외 발생 여부 |
| error_message | varchar(1024), nullable | 예외 메시지 |
Trace와 마찬가지로 저장/조회는 SpanRepository(port) → SpanRepositoryImpl(adapter) → SpanJpaRepository(Spring Data JPA) 3계층 구조를 그대로 따른다. trace_id에 인덱스를 두어 Trace 상세 조회 시 해당 Trace의 Span 목록을 빠르게 가져온다.
resource_metrics 테이블 (Agent가 10초마다 수집해 heartbeat에 동봉한 JVM 리소스 메트릭 스냅샷, docs/04-agent-design.md 참고 - 조회 API는 아직 없고 저장만 한다):
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | bigint (PK) | 내부 식별자 |
| agent_id | varchar(256) | 소속된 Agent의 ID (agent.agent_id, FK 제약 없이 값만 참조) |
| timestamp | bigint | 스냅샷 수집 시각(epoch millis, heartbeat 전송 시각이 아님) |
| cpu_usage | double, nullable | 프로세스 CPU 사용률(0.0~1.0). 측정 불가 시 -1 |
| heap_used / heap_max | bigint, nullable | Heap 사용량/최대치(byte) |
| thread_count | int, nullable | 스레드 총 개수 |
| gc_count / gc_time | bigint, nullable | 전체 GC 누적 실행 횟수 / 누적 시간(ms) |
(agent_id, timestamp)에 인덱스를 두어 Agent별 시계열 조회 성능을 확보했다(아직 조회 API는 없지만 이후 추가할 때를 대비함). SpanRepository와 동일하게 ResourceMetricRepository(port) → ResourceMetricRepositoryImpl(adapter) → ResourceMetricJpaRepository(Spring Data JPA) 3계층 구조를 따른다.
예시
로컬 H2 콘솔은 MVP에서 별도로 열지 않았다. 필요하면 spring.h2.console.enabled=true를 application.yml에 추가해 확인할 수 있다.
TODO
- ClickHouse/Elasticsearch 어댑터 추가
- 대용량 트래픽을 고려한 파티셔닝/보존 기간(retention) 정책
- Flyway 등 스키마 마이그레이션 도구 도입