미터링 파이프라인은 어디가 먼저 막히나: 장비 수가 아니라 단일 DB 스토리지
GPU 사용량을 수집·집계하는 파이프라인에서 병목을 찾고 그 앞에서 설계를 정한 기록입니다. 병목은 장비 수가 아니라 단일 DB 스토리지였습니다.
TL;DR
과금 근거가 되는 사용량을 매번 질의하던 구조에서 자체 보존으로 바꿨습니다. 고객사가 원본 증적을 요구하면 질의로는 감당되지 않습니다.
용량 병목은 장비 수가 아니라 데이터가 쌓이는 단일 DB 스토리지(관리형 MySQL, 최대 2TB)였습니다. 스키마를 인스턴스 수에 독립으로 둬서 인스턴스가 늘어도 구조를 바꾸지 않습니다.
GPU 클라우드의 과금은 사용량 데이터를 그대로 읽습니다. 그 데이터를 어떻게 남기느냐가 곧 과금의 신뢰도가 됩니다. 처음 구조는 집계 결과만 보존하고 원시 사용량은 모니터링 저장소에 매번 질의하는 형태였습니다.
고객사가 원본 증적을 요구하면서 이 구조의 한계가 드러났습니다. 질의로 대응하는 동안은 되지만 이후 이어질 가공 요구를 감당할 수 없었습니다.
1. 무엇을 만들었나
모니터링 저장소 (GPU · CPU · Memory)
↓ 5분 수집
[원천] 인스턴스 단위 저장
↓ 10분 집계
[구간] 워크로드 · 인스턴스 단위
↓ 일간 누적
[일별] 워크로드 · 인스턴스 단위
↓
사용자 조회 API
| 분류 | 기술 |
|---|---|
| Language / Framework | Java 17, Spring Boot |
| ORM | Spring Data JPA |
| Database | 관리형 MySQL (마스터/슬레이브 복제, 월별 파티셔닝) |
| Architecture | 헥사고날 아키텍처 |
| Batch | JobRunr |
| Source | Prometheus |
| Infra | Kubernetes |
2. 왜 셋으로 나눴나
저장 단위를 셋으로 나누면 구조가 복잡해집니다. 그래서 각각이 왜 필요한지부터 정리합니다. 셋은 설계 취향이 아니라 서로 다른 요구에서 나왔습니다.
| 저장 단위 | 주기 | 이건 왜 따로 두나 |
|---|---|---|
| 원천 | 5분 수집 (1분 표본) | 고객사가 원본 증적을 요구했다. 집계만 두면 대응할 수 없다 |
| 구간 | 10분 | 화면이 단위 시간당 사용량을 보여줘야 했고, 그 단위가 사이트마다 달라야 했다 |
| 일별 | 매일 새벽 | 과금의 기본 집계 단위. 최초 요구가 이것이다 |
구간 단위는 설정값으로 바꿀 수 있습니다. 다만 아무 값이나 받지 않습니다. 10분에서 60분 사이이면서 60의 약수(10·12·15·20·30·60)여야 하고, 위반하면 기동 시점에 실패합니다. 시간 격자가 어긋나면 구간 경계가 하루 단위로 딱 떨어지지 않기 때문입니다.
원천을 따로 두는 선택에는 비용이 따릅니다. 가장 많은 행이 쌓이는 곳이 원천이고 그래서 용량 설계의 대상도 원천입니다.
3. 어디가 먼저 막히나
가장 먼저 답해야 했던 질문은 단순했습니다. “어디까지 견뎌야 하나?”
처음에는 인프라 상한, 즉 GPU 장비 수를 기준으로 잡으면 충분하다고 생각했습니다. 하지만 그 가정이 성립하려면 장비 수보다 먼저 한계에 닿는 다른 제약이 없어야 합니다. 확인해 보니 그게 성립하지 않았습니다.
용량 설계는 보통 셋 중 하나에서 출발합니다.
- 인프라 상한 (GPU·노드·CPU 코어 수)
- 트래픽 예측 (TPS, 요청 수, 활성 사용자 수)
- 데이터 보존 요구 (보존 기간, 조회 패턴)
GPU 장비 수가 곧 인스턴스 상한이고 인스턴스 수가 곧 데이터 발생량이라 1번에서 출발하기 쉬웠습니다. 그런데 1번만 보면 둘을 놓칩니다.
- 데이터가 적재되는 DB의 스토리지 상한이 GPU 인프라 상한보다 먼저 도달할 수 있습니다.
- 보존 기간이 정해져야 누적량이 정해지는데, 무엇에 맞출지를 인프라 상한은 답해 주지 않습니다.
그래서 인프라·스토리지·사업 라이프사이클 셋을 함께 놓고 봤습니다.
GPU 인프라 (규모 배경)
│ 인스턴스가 늘어도
▼
스토리지 (병목 · 용량 앵커) ── 관리형 MySQL 최대 2TB
│ 1년 보존을 이 안에서 감당하는 규모가 경계
▼
사업 라이프사이클 (보존 1년)
제약 1. GPU 인프라는 규모 배경이지 병목이 아니다
인스턴스 수를 상한으로 내세우기 쉽지만 그렇게 하지 않았습니다. 두 가지 이유입니다.
- 스키마가 인스턴스 수에 독립적입니다. 인스턴스가 늘어도 복합 키와 월별 파티션 구조가 그대로라 인스턴스 증가는 레코드 수 증가로만 반영됩니다.
- 보유 규모와 실배포 규모는 다릅니다. GPU는 단가가 높아 보유 물량 전체를 동시에 배포·운영하는 극단은 현실적이지 않습니다. 확보한 장비를 전부 오케스트레이션하지 않으며 실배포 상한은 물리 머신 수에 종속됩니다.
인프라 보유 규모로 큰 인스턴스 수를 상한으로 잡을 수도 있었습니다. 하지만 그 수치는 실무 관점에서 방어되지 않습니다. 그래서 “몇 인스턴스까지 견딘다”가 아니라 “스토리지 스펙 안에서 얼마의 적재를 1년 보존으로 감당하나”를 경계로 잡았습니다.
제약 2. 스토리지 상한이 앵커다
미터링 데이터는 사용 중인 관리형 MySQL RDS에 적재됩니다. 이 RDS의 데이터 스토리지는 20GB에서 2TB 범위이고 사용률 임계치에 닿으면 설정한 최대 크기까지 자동 확장됩니다. 이 2TB가 설계의 앵커입니다. 처음부터 점유하는 크기가 아니라 사용량만큼 늘어나는 상한 스펙이라 실운영 규모의 1년 보존이 이 안에 들어오는지 확인한 것이지 2TB를 미리 잡아 두는 것이 아닙니다.
확인 방식은 셋입니다.
- 테이블별 로우당 최대 크기를 DB 스키마와 코드 기준으로 계산
- (로우당 크기) × (예상 레코드 수)가 2TB 안에 들어오는지 대조
- 스키마가 인스턴스 수에 독립이므로, 인스턴스 증가는 레코드 수 증가로만 반영되고 구조 변경 없이 스토리지 여유 안에서 처리
제약 3. 보존 기간은 사업 주기에서 나온다
세 번째는 인프라가 아니라 사업 쪽입니다. 운영하는 GPU 서비스는 사업 계약 1년 단위가 기본이었습니다. 계약이 끝나면 자원이 회수되고 재계약으로 재할당됩니다.
- 수년 이상 누적되는 영속 라이프사이클이 아닙니다
- 사업 단위 라이프사이클이면 1년 보존이 자연스러운 경계입니다
- 영속 서비스라면 “데이터를 계속 쌓아야 하나”라는 질문이 생기지만, 사업 단위면 회수 시점에 과거 데이터 조회 요구가 사실상 소멸합니다
소프트웨어 생명주기를 사업 라이프사이클과 맞춘 설계이고 이것이 보존 기간을 1년으로 확정한 근거입니다. 스펙에도 “보존 기간이 지난 데이터는 자동으로 삭제”로 명시했습니다.
실측과 설계 목표를 나눠 적는다
| 구분 | 내용 |
|---|---|
| 운영 실측 | 일 30~50만 건, 한 주기 약 10초. 수집(5분)·집계(10분)가 주기 안에서 끝난다. 1년 보존 자동 삭제가 동작한다 |
| 설계 목표 | 일 200만 건 규모까지 2TB 스펙 안에서 대응하도록 설계 |
일 200만은 실배포 상한(약 1,000장에 1분 1행이면 일 약 144만 행)에 여유를 얹은 값입니다. 스토리지에서 역산한 값이 아니라, 실배포 상한에서 나온 값이 스토리지 스펙 안에 들어오는지 확인한 것입니다.
4. 쓰기와 읽기가 부딪힐 때
수집과 집계가 같은 DB에서 동시에 돌면 셋이 걸립니다.
| 문제 | 내용 |
|---|---|
| 쓰기 경합 | 5분 수집과 10분 집계가 같은 DB에서 동시 실행되며 락 경합 |
| 조회 성능 불안정 | 배치가 대량 적재 중일 때 사용자 조회 API 응답 시간이 흔들림 |
| 장애 전파 | 외부 소스 하나의 응답 지연이 수집에서 집계, 조회까지 연쇄 |
세 방안을 놓고 봤습니다.
| 방안 | 평가 |
|---|---|
| 시간대 분리 | 5분·10분으로 촘촘해 시간대 확보가 어렵고 한 배치가 지연되면 연쇄 |
| 스키마 분리 + 트랜잭션 라우팅 (선택) | 영역별 장애 격리. DataSource 복잡도 증가 |
| 메시지 큐 기반 비동기 | 완전 분리가 되지만 현재 규모 대비 인프라가 과하고 순서 제어 부담 |
영역을 둘로 나눴습니다.
| 영역 | 역할 | 스키마 |
|---|---|---|
| 원천 | 데이터 수집만, 비즈니스 로직 없음 | 별도 스키마 (마스터/슬레이브) |
| 가공 | 집계와 조회, 비즈니스 로직 포함 | 기본 스키마 (마스터) |
가공은 집계 주기와 조회 패턴이 겹칠 가능성이 낮아 마스터 하나로 충분하다고 판단했습니다.
AbstractRoutingDataSource를 확장해 트랜잭션의 readOnly 속성으로 라우팅합니다.
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TransactionSynchronizationManager
.isCurrentTransactionReadOnly() ? "slave" : "master";
}
}
@Transactional(value = "sourceTransactionManager", readOnly = true)
public List<SourceMetric> findByRange(Instant from, Instant to) { ... }
@Transactional(value = "sourceTransactionManager")
public void saveAll(List<SourceMetric> instances) { ... }
5. 왜 지우고 다시 넣나
처음 고른 것과 그 근거
초기에는 INSERT ... ON DUPLICATE KEY UPDATE를 썼고 근거는 분명했습니다.
- 멱등성: 같은 키를 다시 수집해도 중복이 없다
- 시계열 배치의 널리 쓰이는 방식이다
리서치를 아무리 돌려도 이 결론이 나왔습니다.
전환점
리뷰에서 팀원 자문 결과가 리서치와 달랐습니다. 우리 맥락을 넣으면 그림이 바뀌었습니다.
| 항목 | 값 | 의미 |
|---|---|---|
| 개발팀 규모 | 5인 | 유지보수 인력이 한정된다 |
| 서비스 유형 | 엔터프라이즈 GPU | 정적 환경. 버스트 트래픽이 구조적으로 낮다 |
| 현재 규모 | 일 30~50만 건 | |
| 실배포 상한 | 약 1,000장, 일 약 144만 행 | 물리 머신 수에 종속된다 |
합리적인 설계였지만 우리 규모에서는 과설계였습니다.
리서치를 아무리 돌려도 이런 결론은 나오지 않았습니다. “실배포 상한이 정해진 정적 환경을 5인이 유지보수한다”는 맥락은 어떤 자료에도 없습니다. 이 판단은 서비스 맥락을 아는 사람에게서 나왔습니다.
비용 비교
ON DUPLICATE KEY UPDATE는 행마다 유니크 키 존재 여부를 확인하므로 내부 조회가 반드시 1건 발생합니다. JDBC batch로 한 번에 보내도 마찬가지입니다.
ODKU (N건): 내부 조회 × N + 삽입·갱신 × N = 2N 연산
DELETE + INSERT: 범위 삭제 × 1 + 일괄 삽입 × 1 = 2 연산
수집은 5분 주기로 하루 288회 돕니다.
| 규모 | ODKU | DELETE + INSERT |
|---|---|---|
| 현재 (일 30~50만 행) | 60만 ~ 100만 | 576 |
| 실배포 상한 (일 약 144만 행) | 약 288만 | 576 |
실측 성능 차이는 현재 규모에서 밀리초 단위입니다. 소규모에서는 체감되지 않습니다. 성능이 비슷하다면 더 단순한 쪽이 이깁니다. 5인 팀에서 하드코딩 SQL을 관리하는 비용이 성능 이점보다 컸습니다.
파이프라인 전체를 같은 모양으로
repository.deleteAllBy...(from, to); // 1. 범위 삭제
repository.createAll(entities); // 2. 일괄 삽입
| 항목 | Before | After |
|---|---|---|
| 저장 방식 | JDBC batch UPSERT / Native SQL | JPA saveAll (삭제 후 삽입) |
| PK 전략 | IDENTITY | SEQUENCE (Hibernate 배치 삽입 활성화) |
| 삭제 방식 | 없음 | 범위 기준 일괄 삭제 |
PK 전략을 바꾼 것은 곁다리가 아닙니다. IDENTITY는 Hibernate의 JDBC 배칭을 무력화합니다. 시퀀스 할당량을 배치 크기와 같게 맞춰야 배치 도중 시퀀스를 다시 받느라 끊기지 않습니다.
합리적 설계와 올바른 설계는 다릅니다. 일반론으로 옳은 결정이 특정 맥락에서는 과설계가 됩니다. 그 경계를 판단하는 것은 코드가 아니라 사람이었습니다.
6. 파티셔닝
과금 데이터는 연 단위 보관이 필요합니다. 보관 주기가 끝난 데이터를 지우는 가장 싼 방법은 파티션을 통째로 제거하는 것이고, 그래서 미터링 테이블 5개에 월별 RANGE 파티셔닝을 넣었습니다.
제약 하나가 PK 구조를 바꿨다
파티션 키는 테이블의 모든 유일성 키에 포함되어야 합니다. MySQL이 단일 파티션 안에서만 유일성을 검증하기 때문입니다. 파티션 키가 PK에 없으면 서로 다른 파티션에 같은 PK 값이 존재할 수 있습니다.
기존 테이블은 서로게이트 키를 단일 PK로 쓰고 있었습니다. 파티셔닝을 넣으려면 (id, 파티션 키) 복합 PK로 바꿔야 했습니다. 그런데 Hibernate는 파티셔닝을 모릅니다. DDL 생성에도 없고 엔티티 매핑에도 개념이 없습니다.
| 충돌 | 대응 |
|---|---|
| DDL 관리 | Hibernate 자동 생성으로는 불가 → Flyway로 분리 |
| 엔티티 PK | 단일 @Id → @IdClass 복합 PK + Key 클래스 |
| Repository 조회 | findById(복합키)와 단일 id 조회가 공존 → 새 컨벤션 |
@IdClass 복합 PK는 파티션 유무와 무관하게 동작해서 로컬과 운영이 같은 엔티티 코드를 공유할 수 있었습니다.
조회 컨벤션을 이름에서 정하다
복합 PK로 바꾸면 CrudRepository.findById()의 파라미터가 Key 클래스가 됩니다. 그런데 서로게이트 id는 여전히 전역적으로 유일해서 비즈니스 로직은 id 하나로 조회할 수 있어야 합니다.
기존 이름 findFirstById는 “여러 건 중 첫 번째”라는 뜻을 내포합니다. 실제로는 항상 1건이라, 복합 키를 모르는 사람이 보면 왜 First가 붙었는지 의문이 생깁니다. 대안을 놓고 본 뒤 findByKeyId로 정했습니다. “복합 Key의 Id로 찾는다”가 이름에 그대로 있습니다.
@Query("SELECT e FROM IntervalAggregationEntity e WHERE e.id = :id")
Optional<IntervalAggregationEntity> findByKeyId(@Param("id") Long id);
JPQL을 명시한 이유가 있습니다. @IdClass 환경에서 id는 엔티티의 직접 필드인데, Spring Data는 findByKeyId를 key.id 경로로 해석합니다. 엔티티에 key 프로퍼티가 없어 파생 쿼리 생성이 실패하므로, 읽기 좋은 이름을 유지하면서 쿼리를 직접 지정했습니다.
컨벤션은 설계 초기에 정하는 것이 아니라 충돌이 난 지점에서 필요에 의해 태어난다는 것을 여기서 봤습니다.
DDL에서 날짜를 빼다
초기 DDL에는 구체적인 날짜 파티션이 하드코딩돼 있었습니다.
PARTITION BY RANGE COLUMNS (collected_at) (
PARTITION p202603 VALUES LESS THAN ('2026-04-01'),
PARTITION p202604 VALUES LESS THAN ('2026-05-01'),
PARTITION p_max VALUES LESS THAN MAXVALUE
);
이 DDL을 내년 신규 설치에 그대로 쓰면 과거 날짜의 빈 파티션이 남습니다. 설치할 때마다 날짜를 고쳐야 하는 DDL은 재사용할 수 없습니다.
해결은 단순했습니다. DDL에는 p_max만 선언하고 파티션 관리 Job이 첫 실행에서 그것을 재구성해 필요한 월별 파티션을 만듭니다.
DDL 실행 직후: [p_max(MAXVALUE)]
Job 첫 실행 후: [p202603] [p202604] [p202605] [p202606] [p_max]
관리 Job은 매월 한 번 돌며 3개월 앞까지 미리 만듭니다. 스키마가 둘이라 DataSource별로 JdbcTemplate을 나눴고 운영자가 수동으로 돌릴 수 있는 진입점도 뒀습니다. 테스트는 대상 테이블 누락, 파티션 키 정확성, 중복 생성 방지, 연속 월 범위까지 7개 케이스로 잡았습니다.
7. 실패하면 어떻게 돌아오나
과금 데이터라 빠진 구간을 그대로 둘 수 없습니다. 복구는 세 단으로 되어 있습니다.
| 단 | 무엇을 막나 |
|---|---|
| 재시도 | 일시적 장애. 기본 2회, 3초 → 9초 간격 |
| 자동 백필 | 재시도로 끝나지 않은 누락. 다음 주기가 빠진 구간을 다시 처리한다 |
| 기동 시 설정 검증 | 설정끼리 어긋나 복구가 조용히 망가지는 것 |
재시도와 백필은 성격이 다릅니다. 재시도는 같은 시도를 반복하는 것이고, 백필은 다음 기회에 빠진 부분을 채우는 것입니다. 백필이 있으면 재시도 횟수를 과하게 늘릴 이유가 없습니다.
어디서부터 다시 시작할 것인가
재개 지점을 어디서 읽느냐가 이 설계의 핵심입니다. 기록된 데이터에서 읽습니다. 수집은 마지막 수집 시각, 구간 집계는 마지막 집계 끝 시각입니다.
다만 데이터만 보면 구멍이 생깁니다. 기록할 것이 없을 때 재개점이 그 자리에 멈춥니다. 수집 대상 워크로드가 없는 구간이 이어지면 시각이 전진하지 않고, 조회 범위가 복구 한도까지 벌어집니다. 그래서 두 규칙을 덧댑니다.
| 규칙 | 내용 |
|---|---|
| 수집 재개점의 바닥 | 기록된 마지막 수집 시각과, 잡 성공 시각에 여유를 물린 시각 중 나중 것을 쓴다 |
| 집계 재개점의 천장 | 마지막 수집 시각이 집계 재개점보다 앞서면 거기까지 시작점을 내린다 |
잡 성공 이력을 바닥으로 쓸 수 있는 근거는 조회 실패가 곧 잡 실패로 이어지도록 만들어 뒀기 때문입니다. 성공 이력이 있다는 것은 그 시점까지 조회가 끝났다는 뜻입니다. 성공 시각은 잡 수행 시간만큼 실제 조회 끝을 앞서므로 그만큼 여유를 물려 둡니다.
일별 집계는 날짜 단위라 초 단위 어긋남이 판정을 바꾸지 않습니다. 여기는 잡 이력 기준을 그대로 씁니다.
설정끼리의 의존을 기동 실패로 막다
여기에 숨은 의존이 하나 있습니다. 잡 성공 이력 보존 기간이 복구 창보다 짧으면 바닥이 사라집니다. 그러면 복구 시작점이 한도 끝까지 떨어집니다.
주석으로 적어 두면 언젠가 누가 지웁니다. 그래서 기동 시점에 두 값을 대조하고 어긋나면 애플리케이션이 뜨지 않게 했습니다.
if (retention.compareTo(recoveryWindow) < 0) {
throw new IllegalStateException(
"Job retention is shorter than max recovery window. Check the setting.");
}
되돌릴 수 없는 종류의 실수는 문서가 아니라 기동 실패로 막는 편이 확실합니다.
재집계 범위를 7일로 제한한 이유
수동 재집계는 한 번에 7일까지만 받습니다. 규모와 무관한 구조적 상한에서 나온 값입니다.
| 제한 | 성격 | 7일 기준 |
|---|---|---|
| 범위 쿼리의 시계열당 반환 포인트 11,000 | 기간만으로 결정 (시계열 수 무관) | 1분 간격으로 7.64일. 7일 = 10,080, 8일 = 11,520으로 초과 |
| 쿼리 시간 제한 | 규모에 비례 | 수천 시계열에서 닿는다 |
| 쿼리당 총 샘플 제한 | 규모에 비례 | 위보다 뒤에 있다 |
7일의 근거는 포인트 제한입니다. 1분 간격에서 8일을 한 쿼리로 조회할 방법이 없습니다. 규모 쪽 제한은 그보다 뒤에 있고, 그중 시간 제한이 먼저 닿습니다.
실제 상한은 이 설정값이 아니라 모니터링 저장소에 표본이 남아 있는 기간입니다. 보존 설정이 무엇이든 저장소 볼륨을 만든 시점부터만 표본이 있으므로, 과거 구간을 백필하기 전에 실제 보관 시작 시각을 먼저 조회합니다.
8. 왜 상시 배치인가
주기 실행이면 CronJob이 자연스러워 보입니다. 그런데 매 실행마다 JVM이 뜨는 데 약 30초가 걸리고 실제 작업은 약 10초입니다. 기동 비용이 작업의 3배입니다. 5분마다 이 비용을 내느니 서버를 띄워 두는 편이 나았습니다.
대가는 분명합니다. 하루 중 실제로 도는 시간은 짧고 나머지는 폴링과 헬스 체크만 하며 리소스를 점유합니다. 유휴 비용이 구현 복잡도 비용을 넘어서면 그때가 바꿀 시점입니다.
단주기 배치는 처리 시간이 실행 주기를 넘어서면 적체로 악화됩니다. 그래서 처리 시간을 주기보다 훨씬 작게 유지하는 것이 먼저였습니다.
9. 하고 보니 이름이 있던 것들
“이게 안전하겠다”는 직관으로 정한 것들이 사실 이름이 붙어 있는 방식이었습니다.
| 한 일 | 이름 |
|---|---|
| 마지막 지점 이후만 처리 | Incremental Load (High Water Mark) |
| 10분 고정 구간으로 집계 | Tumbling Window |
| 누락 구간을 다음 주기가 자동 처리 | Catch-up |
| 운영자가 기간을 지정해 재처리 | Backfill |
| 같은 구간을 다시 처리해도 결과가 같음 | Idempotency |
| 소스별로 잡을 분리 | Fault Isolation |
Catch-up과 Backfill은 같은 메서드의 오버로드로 뒀습니다. 하는 일은 “집계”로 같고 트리거 방식만 다르기 때문입니다.
aggregateUsage() // Catch-up (자동, 누락 구간 복구)
aggregateUsage(AggregateUsageRequest) // Backfill (수동, 기간 지정)
이름을 아는 것의 값은 셋입니다. 의사소통이 짧아지고(“HWM 기반 Incremental Load” 한 문장), 같은 갈래의 다른 선택지가 보이고(Snapshot Diff·CDC), 검색이 됩니다.
10. 이 설계가 유효한 구간
현 설계는 스토리지 앵커가 유효한 기간 동안 성립합니다. 제약 중 하나라도 바뀌면 재평가가 필요합니다.
| 시점 | 상황 | 저장 전략 |
|---|---|---|
| 현재 | 일 200만 건 규모까지 2TB 안에서 수용 | 삭제 후 삽입 유효 |
| 인스턴스 분할 기술 부분 도입 | 인스턴스 증가, 2TB 근접 | 재검토 트리거 |
| 전면 도입 / 장기 증설 | 2TB 초과 예상 | 저장 전략과 스토리지 구조 전면 재평가 (파티셔닝 고도화, 샤딩, 외부 저장소) |
단일 GPU를 여러 인스턴스로 나누는 기술은 수익성 개선 목적으로 검토되고 있습니다. 다만 이것은 설계 시점의 근거가 아니라 사후 고려 사항입니다. 현 설계는 그것이 없는 가정에서 스토리지 앵커로 귀결되었고 확산은 위 표의 트리거입니다. 적용 검증을 직접 수행해 가능성과 한계를 파악하고 있어, 이 트리거가 막연한 낙관이 아니라 구체적인 경계가 됩니다.
남은 과제도 있습니다.
- 안 끝난 구간이 쌓일 때 사람에게 가는 알림 경로
- 보관 주기가 만료된 파티션의 자동 제거
11. 마치며
돌아보면 이 작업에서 남은 것은 개별 해법보다 무엇이 먼저 한계에 닿는지를 고르는 일이었습니다.
| 질문 | 고른 답 |
|---|---|
| 어디까지 견디나 | 장비 수가 아니라 스토리지 |
| 왜 1년 보존인가 | 사업 계약 주기와 맞춘다 |
| 어떤 저장 방식인가 | 성능이 결정 변수가 아닌 규모에서는 단순한 쪽 |
| 어디서부터 다시 시작하나 | 실행 이력이 아니라 기록된 데이터 |
| 언제 이 설계를 버리나 | 미리 트리거로 적어 둔다 |
용량을 인스턴스 수로 잡는 것과 병목인 스토리지로 잡는 것은, 결과 수치가 비슷해 보여도 의사결정의 정확도가 다릅니다. “이 용량은 어떻게 산정했나요”라는 질문에 한 가지 근거만 대는 설계와, 왜 그것이 경계이고 왜 다른 것은 병목이 아닌지를 함께 답하는 설계는 다릅니다.
좋은 의사결정은 결과 수치보다 그 수치에 도달한 분석에서 나뉜다고 봅니다.
이 글은 Claude와 함께 작업했습니다.