26Y02x6

Young-Kyoo Kim·2026년 7월 2일

결론부터 말씀드리면 100% 구현 가능함. 다만, 앞서 언급하신 대로 하나의 버킷에 수억 건의 객체가 존재할 수 있는 고부하 환경이므로, 아무런 대책 없이 S3 ListObjects API를 순정 상태로 호출하면 API 서버와 MinIO 클러스터가 동시에 타임아웃으로 붕괴할 위험이 있음.

따라서 이 API는 1) 이전에 설계한 CNPG 메타 DB를 조회하는 방식(가장 추천) 또는 2) MinIO AIStor API를 극한으로 최적화하여 래핑하는 방식 중 하나를 선택하여 구현해야 함.


방안 1. 이전에 기획한 CNPG 메타 DB를 활용하는 방식 (강력 추천)

Kafka ➡️ Redis ➡️ CNPG 파이프라인을 통해 메타데이터를 이미 적재하고 있다면, S3를 찌를 필요가 전혀 없음. API 서버는 단순한 SQL 쿼리 실행기 역할만 수행하므로 리스크가 제로에 가까움.

① 특정 Prefix 아래의 총 사용량 및 파일 수 조회 (/usage)

  • 동작 방식: Prefix 테이블에서 ID를 찾고, 파티셔닝된 Object 테이블에서 COUNTSUM을 집계하여 리턴함.
  • API 구현 로직 (SQL 기반):
-- API 서버 내부에서 실행할 쿼리
SELECT COUNT(*) as total_objects, COALESCE(SUM(size), 0) as total_bytes
FROM storage_objects
WHERE prefix_id = (SELECT id FROM storage_prefixes WHERE full_path = :request_prefix);
  • 이점: 수억 건의 데이터가 있어도 인덱스 타겟 파티션 1개만 스캔하므로 수 밀리초(ms) 안에 응답이 끝남.

② 특정 Prefix 아래의 객체 리스트 조회 (/objects)

  • 위험 요소: 만약 특정 prefix 아래에 수백만 개의 파일이 있다면, 이를 한 번에 JSON 배열로 리턴하는 순간 API 서버 메모리가 고갈(OOM)됨.
  • 해결책: Cursor 기반 페이지네이션(Pagination)을 API 스펙에 반드시 강제해야 함.
  • API 구현 스펙 예시:
  • 요청: GET /api/v1/storage/objects?prefix=dir1/&limit=1000&cursor_file=last_seen_file.txt
  • 쿼리:
SELECT name, size, created_at
FROM storage_objects
WHERE prefix_id = :prefix_id AND name > :cursor_file
ORDER BY name ASC
LIMIT :limit;

방안 2. DB를 쓰지 않고 MinIO AIStor API를 직접 래핑하는 방식

만약 실시간 정합성을 위해 메타 DB를 거치지 않고 MinIO 스토리지를 직접 찔러야 한다면, API 서버 내부에서 MinIO Go/Python SDK를 사용할 때 S3 호환 파라미터를 세밀하게 통제해야 함.

① 특정 Prefix 아래의 사용량 조회 ➡️ MinIO Admin API 활용

  • S3 API에는 특정 폴더의 크기를 바로 리턴해주는 표준 API가 없음. 원래는 전체 리스팅을 해서 합산을 해야 하지만, 상용 MinIO AIStor는 Admin API를 통해 서버 측에서 계산된 프리픽스별 디스크 사용량(DataUsageInfo)을 제공함.
  • API 서버 구현 방식 (Python MinIO Admin SDK 예시):
# API 서버 내부 가상 코드
from minio import Minio
# Admin 클라이언트를 통해 S3 리스팅 없이 서버 측 집계 데이터 반환 요청
# (주의: 이 또한 수억 건인 경우 백엔드 가비지 컬렉션 주기에 따라 약간의 레이턴시가 있을 수 있음)
usage_info = minio_admin_client.get_bucket_usage(bucket_name, prefix=target_prefix)
return {
    "size_bytes": usage_info.size,
    "object_count": usage_info.object_count
}

② 특정 Prefix 아래의 객체 리스트 조회 ➡️ S3 Delimiter & ContinuationToken 활용

  • 계층형 탐색 강제 (Delimiter='/'): 수억 개의 하위 파일을 다 가져오지 않고, 파일 시스템 탐색기처럼 딱 현재 레벨의 하위 폴더(Prefix)와 파일 목록만 끊어서 가져오도록 래핑해야 함.
  • ContinuationToken 토스: MinIO가 리턴해주는 페이징용 포인터(NextContinuationToken)를 API 서버가 보관하지 말고, 최종 클라이언트(포털 또는 Workbench)에게 그대로 넘겨주어 무상태(Stateless)를 유지해야 함.
  • API 서버 구현 스펙 예시:
  • 요청: GET /api/v1/storage/raw-list?prefix=dir1/&token=xyz...
  • API 서버 내부 로직:
# API 서버가 MinIO SDK를 호출할 때 설정을 극단적으로 타이트하게 제한
objects = minio_client.list_objects(
    bucket_name, 
    prefix=requested_prefix, 
    recursive=False, # ◀ 수억 건 폭주를 막기 위해 하위 폴더 재귀 탐색 절대 금지
    delimiter='/',   # ◀ 딱 한 단계 아래의 구조만 단건 분리
    max_keys=100,    # ◀ 한 번에 최대 100개씩만 페이징
    start_after=requested_token
)

💡 DevOps 관점의 최종 권장 아키텍처 결론

  • 사용량/용량 조회(COUNT/SUM): MinIO Admin API를 래핑하거나, Prometheus 메트릭 엔드포인트를 API 서버가 파싱해서 던져주는 방식이 안전함.
  • 객체 리스트 조회(ListObjects): 6,000개 테넌트 규모에서는 결국 유저들이 각자 자기가 구축한 메타 DB(방안 1)를 조회하게 유도하고, 진짜 S3 스토리지 다이렉트 리스팅 API는 아주 특수한 관리자 UI나 Workbench 초기 로딩 시점에만 Strict 페이징(Recursive=False, MaxKeys=100)을 걸어 제한적으로 제공하는 것이 리스크를 관리하는 정석임.

0개의 댓글