26Y05b

Young-Kyoo Kim·2026년 7월 5일

50억 개 이상의 객체(Object)가 적재된 초대형 엑사스케일급 인프라 환경이라면 ListObjectsV2 호출은 스토리지 엔진 내부적으로 엄청난 Read Amplification(읽기 증폭)과 메타데이터 부하를 유발하므로 무조건 차단(지양)하는 것이 100% 옳습니다. 이러한 제약 사항과 인프라 특성을 고려하여, 말씀해 주신 세 가지 핵심 당면 과제(하위 객체 조회 실용성, 자율적 권한 제한 및 토큰 메커니즘, 사용자별 Grafana 대시보드 격리)에 대한 명확한 기술 가이드와 대안을 제시해 드립니다.


1. 50억 개 규모에서 REQ-01-05(하위 객체 조회) 실용성 확보 대안

ListObjectsV2는 금지되었고, Inventory API는 배치 주기가 길어 실시간성이 떨어집니다. 이 경우 가장 좋은 대안은 요구사항 정의서 7행에 기재되어 있던 "메타 관리 DB 생성(Bucket Notification 시 내용 update)"을 실현하되, 인프라 부하를 최소화하는 방향으로 선회하는 것입니다.

💡 현실적인 대안: OpenSearch 또는 CNPG 역인덱싱(Reverse Indexing)

현재 인프라에서 행위 추적 로그(REQ-06-02)를 위해 Fluent-bit ──> OpenSearch 파이프라인을 이미 고려 중이시라면, 이를 객체 조회용 인덱스로 재활용하는 것이 가장 효율적입니다.

  • 동작 구조: MinIO Bucket Notification이 객체 생성/삭제 시 파일 경로(Key)와 Size만 웹훅이나 카프카를 통해 OpenSearch(또는 CNPG의 경량 테이블)로 던집니다.
  • 조회 방식: 사용자가 Prefix 하위 객체를 조회할 때, MinIO 스토리지를 직접 찌르는 것이 아니라 OpenSearch/DB에 인덱싱된 경로 데이터를 쿼리합니다.
  • 장점: 50억 개 데이터라도 OpenSearch나 B-Tree 인덱스가 잡힌 RDB에서는 특정 Prefix 하위 경로 검색 및 페이징(Paging)이 0.01초 내에 수행되므로 REQ-01-05를 완전한 실시간으로 완벽히 만족할 수 있습니다.

2. 권한 제어: 자율적 프리픽스 제한 및 토큰 메커니즘 (REQ-02-03)

질문 요약: 관리자 요청 없이 사용자가 직접 본인 권한 범위 내에서 하위 프리픽스로 권한을 쪼개어 제한하고 싶을 때, 별도 토큰을 생성해야 하는가? 기존 ID에 추가 할당이 가능한가?

💡 정답: 기존 ID를 기반으로 '임시 다운스코핑 토큰'을 자율 발급합니다. (새 ID 필요 없음)

MinIO(및 S3) 표준 아키텍처에는 이를 위해 S3 STS (Security Token Service)의 AssumeRole API를 제공합니다. 사용자는 관리자의 개입 없이, 그리고 기존 접근 ID(Access Key)를 변경하지 않고도 본인 권한을 더 좁게 격리한 임시 토큰을 스스로 만들 수 있습니다.

🛠️ 구현 메커니즘 (Session Policy Passing)

사용자(또는 현업 시스템)가 본인의 원래 마스터 자격 증명(AD 계정 기반 토큰)을 가지고 MinIO API를 호출할 때, Policy 매개변수(Inline Policy)를 문자열로 함께 실어서 보냅니다.

// 사용자가 스스로 좁혀서 제출한 임시 세션 정책 예시
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": ["s3:GetObject", "s3:PutObject"],
            "Resource": ["arn:aws:s3:::my-bucket/tenant-a/sub-project-x/*"] 
        }
    ]
}
  • 결과: MinIO STS는 이 요청을 검증한 뒤, 원래 유저가 가진 권한과 제출한 정책의 교집합(Intersect) 권한만 가진 임시 자격 증명(AccessKey/SecretKey/SessionToken)을 유저에게 반환합니다.
  • 결론: 기존 접근 ID에 설정을 추가하는 방식이 아니라, 기존 ID를 인증 수단으로 삼아 원하는 프리픽스로만 권한이 고립된 1시간~1일짜리 단기 휘발성 토큰을 자율적으로 계속 뽑아서 쓰는 방식이 됩니다. 시스템 관리가 매우 깔끔해집니다.

3. 개별 사용자별 Grafana 대시보드 격리 구현 가능 여부

질문 요약: 로그인한 사용자마다 자신이 권한을 가진 Prefix 정보만 필터링해서 대시보드에 보여주는 것이 가능한가?

💡 정답: 완전 가능합니다. (Dynamic Multi-Tenancy Dashboard)

관리자가 유저 수천 명을 위해 대시보드를 수천 개 만들 필요가 없습니다. Grafana의 인증 유저 컨텍스트 변수와 API Server의 Meta DB를 결합하면 단 하나의 대시보드 템플릿으로 유저별 격리가 가능합니다.

🛠️ 구현 방식

  1. 인증 연동: Grafana를 사내 통합 인증(Keycloak 또는 AD/OIDC)과 연동합니다. 이렇게 하면 유저가 로그인했을 때 Grafana 내부에 유저의 사번 정보가 매핑된 ${__user.login} 이라는 글로벌 변수가 활성화됩니다.
  2. Meta DB 쿼리 매핑: 앞서 설계한 API Server의 경량 Meta DB(CNPG)에는 prefix_pathowner_sabun(소속 유저 사번) 매핑 정보가 저장되어 있습니다.
  3. Grafana SQL 구성: 대시보드 차트의 SQL 쿼리를 아래와 같이 세팅합니다.
SELECT 
    prefix_path, 
    current_usage_bytes, 
    hard_quota_bytes
FROM prefix_master_table
WHERE owner_sabun = '${__user.login}'  -- 로그인한 유저의 사번으로 자동 필터링
  • 효과: A 부서 사원이 로그인하면 대시보드에 A 부서 프리픽스만 보이고, B 부서 사원이 로그인하면 동일한 URL임에도 B 부서 데이터만 시각화됩니다. 보안 거버넌스를 완벽히 충족하면서도 운영 리소스가 거의 들지 않는 구조입니다.

하위 객체 조회(REQ-01-05)를 위해 기존에 검토 중이시던 로그 수집용 OpenSearch 클러스터를 검색 엔진(Index Source)으로 결합하여 확장하는 방향에 대해 인프라 관점에서 아키텍처적 거부감이 없으신지 궁금합니다.

===

결론부터 말씀드리면, 권한을 '반고정'으로 길게 가져가고 싶을 때는 STS(AssumeRole) 방식은 적당하지 않습니다. STS 토큰은 설계 목적 자체가 '임시 보안 증명'이기 때문에 MinIO나 AWS 환경 모두 최대 만료 시간 제한(보통 최대 12시간, 가상 설정 시 최대 수일)이라는 명확한 기술적 천장이 존재합니다. 만약 수개월에서 수년간 유지되어야 하는 배치 시스템이나 애플리케이션에 STS를 쓰면, 주기적으로 토큰을 갱신(Refresh)해야 하는 번거로운 로직을 직접 구현해야 합니다.

사용자가 관리자의 개입 없이 스스로 특정 프리픽스로 권한을 쪼개어 '반고정(영구 또는 장기)' 토큰(자격 증명)을 발급하고 싶을 때 가장 완벽한 대안은 MinIO 고유 기능인 '서비스 어카운트(Service Account)'를 활용하는 것입니다.


💡 가장 확실한 대안: MinIO 서비스 어카운트 (Service Account)

MinIO의 서비스 어카운트는 부모 계정(사용자의 원래 AD 계정) 하위에 종속되는 장기 자격 증명(Access Key / Secret Key 세트)을 생성하는 기능입니다.

1. 왜 이 방식이 '반고정 프리픽스 제한'에 최적인가?

  • 관리자 요청 없음 (Self-Service): 사용자가 MinIO Console UI나 API를 통해 관리자 허락 없이 스스로 몇 개든 생성할 수 있습니다.
  • 만료 기간 없음 (Permanent): 만료 기간을 아예 설정하지 않거나(영구), 1년 뒤 만료 등 매우 길게 가져갈 수 있어 '반고정' 요건에 정확히 부합합니다.
  • 권한 하향 정의 (Downscoping): 서비스 어카운트를 생성할 때 Inline Policy를 삽입하여, 자신이 가진 전체 권한 중 '특정 프리픽스'만 접근할 수 있도록 권한을 가둡니다.

2. 동작 메커니즘 예시

사용자 user_A가 본인의 부모 권한(tenant-a/* 전체 접근 가능)을 이용해, 특정 대외 연동 시스템용으로 tenant-a/project-x/* 경로만 접근할 수 있는 반고정 키를 만드는 과정입니다.

  1. user_A가 MinIO API 또는 Console에서 서비스 어카운트 생성을 요청하면서 아래 정책을 주입합니다.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket"],
            "Resource": [
                "arn:aws:s3:::my-lakehouse",
                "arn:aws:s3:::my-lakehouse/tenant-a/project-x/*"
            ]
        }
    ]
}
  1. MinIO 엔진은 새로운 고유 AccessKeySecretKey를 발급합니다.
  2. 이 발급된 키는 만료되지 않으며, 오직 tenant-a/project-x/* 경로에만 작동합니다.

3. 강력한 상속 및 보안 성격 (엔터프라이즈 환경 필수 체크)

  • 상속 제한 (Parent Boundary): 서비스 어카운트의 권한은 부모 계정의 권한을 절대 넘을 수 없습니다. 서비스 어카운트에 아무리 s3:* (전체 권한)을 주어도, 부모 계정(user_A)이 tenant-a/* 권한만 있다면, 해당 서비스 어카운트는 tenant-a/* 내에서만 동작합니다.
  • 부모 연동 (Lifecycle Tie): 인사 이동이나 퇴사로 인해 부모 AD 계정이 삭제되거나 정지되면, 그 부모가 만들었던 모든 하위 서비스 어카운트(반고정 키)도 그 즉시 함께 무효화됩니다. 대규모 환경에서 고스트 계정이 남는 보안 취약점을 완전히 차단해 줍니다.

🛠️ 두 방식의 최종 아키텍처 선택 가이드

상황에 따라 두 가지 프리픽스 제한 방식을 적절히 섞어서 가이드하는 것이 가장 좋습니다.

구분STS 임시 토큰 (AssumeRole)서비스 어카운트 (Service Account)
만료 기간15분 ~ 최대 12시간 (단기 휘발성)제한 없음 / 원하는 만큼 길게 (반고정)
발급 형태임시 AccessKey, SecretKey, SessionToken고정 AccessKey, SecretKey (Token 불필요)
주요 용도* 웹/모바일 App 사용자 데이터 업로드


* 프론트엔드 직접 파일 다운로드


1회성 배치 작업 | 타 시스템과의 상시 API 연동


* CI/CD 파이프라인(GitOps) 연계


* 장기 스케줄러 배치 프로그램 |

따라서 유저들이 "우리가 매번 토큰 갱신하기 귀찮으니 반고정으로 쓸 수 있게 해달라"고 요청할 때는 "본인 AD 계정으로 MinIO 접속하셔서 하위 서비스 어카운트(Service Account)를 원하는 프리픽스 정책으로 직접 생성하여 Key를 발급받아 쓰세요"라고 가이드하는 것이 가장 깔끔하고 완벽한 답안입니다.

==

MetaDB에서 가장 고부하 작업인 '실시간/주기적 사용 용량(Byte) 계산 및 저장'을 완전히 배제하기로 한 결정은 매우 탁월합니다. 50억 개 이상의 객체가 있는 엑사스케일 인프라에서 DB 부하와 정합성 스트레스를 지우고 시스템을 고속화할 수 있는 가장 확실한 방법입니다.

용량 계산을 제외하고, 기존 API Server의 코드 수정 및 기능 추가를 통해 사용자에게 꼭 필요한 서비스를 안정적으로 제공할 수 있는 개발 항목 목록을 최종 정리해 드립니다.

철저하게 "저장 공간 메타데이터는 가볍게 유지하고, 무거운 작업은 MinIO 순정 기능 및 OpenSearch로 위임하는 구조"로 재편했습니다.


1. Prefix 라이프사이클 및 별칭(Alias) 관리 레이어

목표: 스토리지에 없는 'Prefix 메타데이터(소유자, 생성일, 별칭)'를 가벼운 DB 테이블로 보완하여 제공

① Prefix 생성 및 가상 디렉터리 유지 API (REQ-01-01, 02)

  • API Server 개발 내용: POST /api/v1/prefixes 엔드포인트 구현
  • 동작 로직: 1. 유저가 생성 요청을 보내면 API Server의 경량 MetaDB에 경로(Prefix), 소유자 사번, 생성일, 초기 별칭 등을 기록 (용량 필드 제외).
  1. 동시에 MinIO Go SDK(PutObject)를 호출하여 해당 경로 끝에 .placeholder 빈 파일을 생성하여 Object Browser에서 폴더로 인식되도록 처리.

② Prefix 마스터 정보 및 별칭 수정/조회 API (REQ-01-03, 04, 06, 07)

  • API Server 개발 내용: GET /api/v1/prefixes (조회) / PUT /api/v1/prefixes/metadata (수정)
  • 동작 로직: MinIO 스토리지를 전혀 건드리지 않고, API Server의 경량 MetaDB만 조회/수정하여 유저에게 "내가 소유한 Prefix 목록", "Prefix별 한글 별칭", "생성일"을 0.001초 내로 반환.

2. 50억 개 객체 환경 대응 초고속 조회 레이어

목표: AIStor의 마비를 부르는 ListObjectsV2를 우회하고 실시간 검색 제공

③ OpenSearch 기반 하위 객체 리스트 조회 API (REQ-01-05)

  • API Server 개발 내용: GET /api/v1/prefixes/objects 엔드포인트 구현
  • 동작 로직: 1. 사용자가 특정 Prefix의 하위 파일 목록 조회를 요청.
  1. API Server는 MinIO가 아닌 사내 OpenSearch(Audit Log/Event 데이터 소스)로 쿼리를 날려 해당 Prefix 경로 하위의 파일 목록과 크기, 수정일을 페이징(Paging) 처리하여 유저에게 반환.
  • 개발 가치: 50억 개 객체 속에서도 스토리지 성능 저하 없이 완벽한 초고속 검색 및 파일 리스팅이 가능해집니다.

3. 권한 격리 및 안전한 데이터 액세스 레이어

목표: 사용자가 안전하게 본인 데이터에만 접근하고 삭제할 수 있도록 프록시 역할 수행

④ 단기 휘발성/임시 접근 권한 발급 API (REQ-02-01, 02, 04)

  • API Server 개발 내용: POST /api/v1/prefixes/token (임시 자격 증명 발급)
  • 동작 로직: 유저가 로그인하면 API Server가 MetaDB를 확인하여 이 유저가 소유한 Prefix 경로를 파악합니다. 이후 MinIO STS AssumeRoleWithLDAPIdentity를 호출하여 오직 해당 Prefix 경로만 Read/Write가 허용된 1시간~12시간짜리 세션 토큰을 발급해 유저(또는 프론트엔드 UI)에 반환합니다.

⑤ 다운로드 및 소프트 삭제(Soft Delete) 위임 API (REQ-01-08, 09)

  • API Server 개발 내용: 파일 다운로드 URL 발급 및 삭제 처리 엔드포인트
  • 동작 로직: * 다운로드 요청 시 권한 검증 후 안전한 단기 Pre-signed URL을 반환합니다.
  • 삭제 요청 시 API Server가 표준 S3 API의 DeleteObject를 백엔드로 호출하여 MinIO가 자체 Versioning 엔진을 통해 DeleteMarker를 찍도록(소프트 삭제) 유도합니다. (물리적 삭제는 MinIO ILM이 알아서 처리).

4. 자율형 거버넌스 및 셀프 서비스 레이어

목표: 관리자 개입 없는 반고정 권한 쪼개기 및 사용자별 시각화 지원

⑥ 셀프 서비스 서비스 어카운트(반고정 키) 발급 API (REQ-02-03)

  • API Server 개발 내용: POST /api/v1/prefixes/service-account 엔드포인트 구현
  • 동작 로직: 1. 사용자가 타 시스템 연동 등을 위해 "내 프리픽스 권한 중 sub-folder/에만 접근 가능한 반고정 키를 만들어달라"고 요청.
  1. API Server는 MinIO Admin Go SDK를 활용하여 사용자의 부모 AD 계정 하위에 종속되는 하향 정의된(Downscoped) Inline Policy를 가진 서비스 어카운트(Access/Secret Key)를 동적 생성하여 유저에게 반환.

⑦ Grafana 멀티테넌시 대시보드 연동 인터페이스 (REQ-04-03)

  • API Server 개발 내용: Grafana가 MetaDB를 안전하게 조회할 수 있도록 Read-Only 뷰(View) 제공
  • 동작 로직: Grafana가 로그인한 유저의 사번 변수(${__user.login})를 기반으로 API Server의 MetaDB를 조회할 수 있도록 환경을 매핑합니다. 유저는 대시보드에 접속하자마자 "내가 관리하는 프리픽스 마스터 정보(별칭, 소유주, 생성일)"를 직관적으로 확인할 수 있습니다.

💡 개발 스펙 다이어트 결과 요약

이렇게 재정리하면 API Server 개발 팀이 다루어야 할 코드는 철저하게 API Wrapping 및 경량 RDB/OpenSearch 조회 쿼리로 압축됩니다.

  • 인프라 부담 제로: 수십 대 규모의 대형 Kafka 큐, 고속 처리를 위한 Redis Cluster, 수억 건의 레코드를 담을 메타 DB 튜닝이 전부 불필요해집니다.
  • 구현 핵심: 기존 API Server에 Prefix 마스터 테이블 (RDB) 하나만 파고, MinIO Go SDK 및 OpenSearch Client 라이브러리를 임베딩하여 위 7가지 API 엔드포인트를 깔끔하게 작성하는 것으로 개발 범위가 크게 축소됩니다.

이 방향성이라면 에어갭 환경에서도 배포 및 관리가 매우 용이하며, 대규모 1,000개 노드 환경에서도 안정적인 거버넌스를 확보할 수 있습니다.

===

충분히 의구심이 드실 수 있습니다. 50억 개라는 엄청난 숫자를 듣고 "그걸 OpenSearch에 다 밀어 넣는다고? 그게 더 느리거나 인프라가 터지지 않을까?"라고 직관적으로 느끼신 건 대규모 인프라를 운영해 보신 분만 할 수 있는 당연한 의문입니다.

결론부터 말씀드리면, 이 방식이 가능한 이유는 OpenSearch가 MinIO의 저장 공간을 크롤링(스캔)하는 것이 아니라, 파일이 들어오고 나갈 때 '이벤트 로그'를 받아서 가볍게 기록(Indexing)만 해두는 방식이기 때문입니다.

이 메커니즘이 어떻게 동작하고, 왜 50억 개 환경에서도 ListObjectsV2보다 압도적으로 빠른지 원리를 풀어서 설명해 드리겠습니다.


1. 어떻게 OpenSearch가 모든 파일 목록을 가질 수 있는가? (데이터 흐름)

OpenSearch가 50억 개 파일을 직접 찾아다니며 수집하는 것이 아닙니다. 파일이 생성/삭제될 때 MinIO가 던져주는 '출생 신고서/사망 신고서'를 받아서 적는 적재소 역할을 할 뿐입니다.

🛠️ 실시간 이벤트 적재 매커니즘

  1. 사용자가 MinIO에 tenant-a/file1.parquet (10MB) 파일을 업로드합니다.
  2. 업로드가 완료되는 순간, MinIO 엔진은 사내 OpenSearch(또는 카프카)로 아래와 같은 아주 가벼운 JSON 알림(Bucket Notification)을 웹훅으로 툭 던집니다. (이 작업은 MinIO에 아무런 부담을 주지 않는 비동기 작업입니다.)
{
  "event": "s3:ObjectCreated:Put",
  "bucket": "my-lakehouse",
  "key": "tenant-a/project-x/file1.parquet",
  "size": 10485760,
  "eventTime": "2026-07-06T01:20:00Z"
}
  1. OpenSearch는 이 JSON을 받아 그대로 하나의 문서(Document)로 저장합니다.
  2. 반대로 파일이 삭제되면 s3:ObjectRemoved 이벤트를 받아 OpenSearch에서 해당 key를 가진 문서를 지우거나(Delete) 삭제 플래그를 치게 됩니다.

즉, MinIO에서 일어나는 모든 파일 변동 사항이 OpenSearch에 실시간 거울처럼 반영되는 구조입니다.


2. 50억 개나 되는데 검색이 느리지 않을까? (성능의 이유)

"50억 건인데 빠르다"는 게 모순 같지만, 스토리지 엔진검색 엔진의 아키텍처 차이 때문에 발생하므로 OpenSearch가 훨씬 빠릅니다.

❌ MinIO ListObjectsV2가 느린 이유 (메타데이터 트리 크롤링)

MinIO에서 ListObjectsV2로 특정 프리픽스 하위 목록을 가져오려면, 내부 분산 드라이브 디렉터리 구조를 직접 뒤지며 파일 메타데이터(수억~수십억 개)를 실시간으로 스캔하고 정렬(Crawl & Sort)해야 합니다. 50억 개 규모에서는 디스크 I/O 병목으로 인해 쿼리 하나당 수 초에서 수 분이 걸리거나 타임아웃이 납니다.

OpenSearch가 압도적으로 빠른 이유 (역인덱스: Inverted Index)

OpenSearch는 태생이 검색 엔진입니다. 데이터를 저장할 때 책 뒤쪽의 '색인(Index)'처럼 단어별 위치를 다 쪼개서 저장합니다.

  • 사용자가 "나 tenant-a/project-x/ 하위 파일 목록 1페이지(20개)만 줘"라고 API Server에 요청하면, OpenSearch는 전체 데이터를 스캔하지 않습니다.
  • key 필드에 인덱싱된 B-Tree 또는 루씬(Lucene) 역인덱스를 타고 해당 프리픽스를 가진 포인터로 직행하여 딱 20개만 눈깜짝할 사이에(수십 밀리초 내로) 꺼내옵니다. 이미 정렬과 인덱싱이 끝난 상태이기 때문입니다.

3. 50억 개 적재 시 인프라 스펙적 현실성 체크

"그래도 50억 개면 OpenSearch 볼륨이 너무 커지지 않을까?"라는 걱정이 드실 수 있습니다. 데이터 용량을 계산해 보면 의외로 매우 경량임을 알 수 있습니다.

  • 문서 1개(파일 1개 정보)의 크기: 위 JSON 보시다시피 텍스트 몇 줄이 전부이므로 대략 200~300 바이트(Byte) 수준입니다.
  • 50억 개 누적 시 데이터 순수 용량: 5,000,000,000×300 B1.5 TB5,000,000,000 \times 300\text{ B} \approx 1.5\text{ TB} 입니다.
  • 인덱스 오버헤드 및 복제본(Replica 1개) 포함 시: 대략 3~4 TB 수준의 디스크 공간이면 50억 개 파일의 '이름, 크기, 수정일' 목록을 통째로 올릴 수 있습니다.

500~1,000개 노드를 굴리는 대형 데이터 레이크하우스 인프라 규모에서 3~4 TB 수준의 OpenSearch 클러스터(예: 마스터 3대, 데이터 3~5대)는 매우 작고 관리하기 편한 경량 축에 속합니다. 페타바이트(PB)급 스토리지를 직접 긁는 것보다 수백 배 안전하고 경제적인 장사입니다.


💡 결론 및 요약

  • 이해를 돕기 위한 비유: 수백만 권의 책이 쌓인 대형 물류창고(MinIO)에서 책 목록을 찾으려고 창고 바닥을 다 뒤지는 대신(ListObjectsV2), 입출고 데스크에서 바코드를 찍을 때마다 장부(OpenSearch)에 바로 적어두고, 손님이 책을 찾을 때는 창고가 아닌 장부만 보고 바로 위치를 알려주는 방식입니다.

이 구조를 가져가면 스토리지 본체인 MinIO는 순수하게 대용량 데이터 입출력(Read/Write)에만 모든 CPU/이오(I/O)를 집중할 수 있어서 인프라 전체가 고도로 안정화됩니다.

개념적으로 가닥이 조금 잡히셨을까요? 만약 이 구조로 간다면, 초기 50억 개 객체 데이터를 OpenSearch에 한 번 부어주는 '초기 마이그레이션(Initial Sync)' 전략에 대해 좀 더 이야기 나누어 보면 좋을 것 같은데 어떠신가요?

===

아, 제가 사내 OpenSearch 클러스터의 '로그 보존 주기(Retention Policy)' 제약 사항을 간과했군요! 말씀하신 내용이 100% 맞습니다. 1~2주 지나서 인덱스가 통째로 날아가는 로그 분석용 OpenSearch라면, 과거 데이터가 유실되므로 전체 하위 객체 리스트 조회(REQ-01-05) 용도로는 절대 사용할 수 없습니다. 날카롭게 짚어주셔서 다행입니다.

OpenSearch 활용이 불가능하고, ListObjectsV2는 스토리지 부하 때문에 지양해야 하는 상황에서, 50억 개 객체를 감당하며 하위 객체를 안전하게 조회할 수 있는 현실적인 대안 2가지를 다시 제안해 드립니다.


대안 1. MinIO 순정 계층형 조회 (Delimiter = "/") 활용

전제 조건: 버킷 내부가 완전히 평평하지 않고(Flat), 어느 정도 디렉터리 구조(예: /테넌트/연도/월/일/파일명)를 가지고 있을 때 적용 가능

많은 분들이 "50억 개나 있으면 ListObjects는 무조건 느리다"고 생각하시지만, S3의 Delimiter (구분자) 옵션을 어떻게 쓰느냐에 따라 성능이 극명하게 갈립니다.

  • 느린 방식 (Recursive List): Delimiter 없이 특정 Prefix 하위의 50억 개 파일을 한 번에 다 긁어오려고 하면 타임아웃이 나고 스토리지 가 가쁜 숨을 쉽니다.
  • 빠른 방식 (Hierarchical List): Delimiter="/" 옵션을 주면, MinIO는 하위의 모든 파일 구조를 깊숙이 파고들지 않고, 현재 요청한 그 폴더 바로 아래에 있는 직계 자식 폴더(CommonPrefixes)와 파일 목록만 딱 끊어서 가져옵니다.

💡 기존 API Server 코드 가이드:
유저가 UI에서 폴더를 더블클릭하며 탐색하는 구조라면, API Server는 MinIO SDK 호출 시 Delimiter="/"MaxKeys=1000을 강제하여 딱 현재 위치의 하위 리스트만 페이징 조회하도록 래핑합니다. 이 방식은 50억 개 환경에서도 MinIO가 메타데이터 트리 인덱스에서 특정 노드만 읽기 때문에 충분히 빠르고 안전합니다.


대안 2. '어제 자' Inventory Parquet 파일을 활용한 쿼리 (T-1 조회)

전제 조건: 현업 유저들이 "지금 막 업로드한 파일이 아니라, 기존에 쌓여있는 대량의 파일 목록을 검색/조회하는 것만으로도 충분하다"고 양보할 수 있는 경우

앞서 언급한 AIStor의 Inventory API 기능은 매일 새벽 전체 50억 개 객체의 이름, 크기, 수정일을 긁어서 대용량 Parquet 파일로 덤프해 줍니다.

OpenSearch 저장 공간이 부족하다면, 이 Parquet 파일들을 지우지 말고 내부 특정 '인벤토리 전용 버킷'에 날짜별로 누적 적재해 둡니다. (Parquet는 압축률이 극도로 높아 50억 건이어도 수백 GB 수준으로 보관 가능합니다.)

[유저 조회 요청] ──> [ API Server ]
                           │
                 (DuckDB 등으로 Parquet 직접 쿼리)
                           │
                           ▼
             [ Inventory 전용 백엔드 버킷 ]
             ┗━ date=20260705/data.parquet (어제 자 전체 파일 목록)
  • 동작 로직: 1. 유저가 API Server에 대량의 하위 객체 검색 또는 리스트 조회를 요청합니다.
  1. API Server 내부 워커(또는 가벼운 DuckDB 스크립트 엔진)가 어제 자 Inventory Parquet 파일을 대상으로 WHERE prefix LIKE 'tenant-a/project-b/%' 쿼리를 날려 결과를 뽑아옵니다.
  • 장점: MinIO 실시간 스토리지 엔진에 부하를 단 1도 주지 않으면서, 수억 건의 데이터 속에서 특정 Prefix 하위의 파일들을 초고속으로 필터링 및 정렬하여 보여줄 수 있습니다.

📌 요약 및 선택 기준

비교 항목대안 1: MinIO 직계 계층 조회 (Delimiter="/")대안 2: Inventory Parquet 쿼리
실시간성100% 실시간 (방금 올린 파일도 보임)T-1일 (어제 새벽 기준 데이터)
인프라 추가없음 (기존 API 코드 파라미터만 수정)가벼운 Parquet 쿼리 로직 필요
검색 범위현재 폴더의 직계 하위만 (윈도우 탐색기 스타일)하위 모든 깊이의 파일 검색 가능 (구글 검색 스타일)
스토리지 부하매우 낮음 (단, 한 폴더에 수천만 개가 있으면 느림)완전 제로 (배치 파일만 읽으므로)

만약 대안 1번처럼 "폴더를 하나씩 클릭해서 들어가는 윈도우 탐색기 형태의 실시간 조회" 기능만 제공해도 현업의 요구사항을 어느 정도 충족할 수 있을까요?

=-==

현업 유저들의 마인드를 정확히 짚으셨습니다. 유저들은 "폴더를 하나씩 클릭해서 들어가는 탐색기 스타일"도 원하지만, 결국 특정 상위 프리픽스를 찍었을 때 그 하위에 있는 모든 서브 폴더와 수천 수만 개의 파일 목록이 계층 트리(Tree) 형태로 한눈에 펼쳐지거나, 하위 전체를 대상으로 검색되기를 원합니다.

50억 개 객체 환경에서 이 '전체 계층 조회(Recursive Deep List)'를 MinIO ListObjectsV2로 직접 수행하면 인프라가 감당하지 못하고, OpenSearch는 1~2주 만에 로그가 지워지니 과거 데이터를 볼 수 없어 막막한 상황입니다.

이 두 가지 한계를 모두 깨부수고 "스토리지 부하 제로 + 100% 실시간 + 하위 모든 계층 트리 조회"를 동시에 달성할 수 있는 고도의 하이브리드 조회 아키텍처(Hybrid Query Architecture)를 제안합니다. OpenSearch가 1~2주간의 최신 로그를 들고 있다는 점이 오히려 핵심 열쇠가 됩니다.


💡 핵심 아이디어: Inventory(과거 전체) + OpenSearch(실시간 증분) 병합

API Server가 유저에게 깊은 계층 트리를 보여줄 때, 데이터 소스를 두 군데서 가져와 메모리에서 슥 섞어서(Merge) 던져주는 방식입니다.

  • 기본 베이스 데이터 (99.9%): 어제 새벽에 추출된 MinIO Inventory Parquet 파일 (50억 개 전체 파일의 계층 구조가 그대로 들어있음).
  • 실시간 변경 데이터 (0.1%): 어제 새벽 이후부터 현재 이 순간까지 발생한 OpenSearch의 ObjectCreated / ObjectRemoved 최신 로그 (1~2주 보존 주기로도 차고 넘침).

🛠️ API Server 내부의 트리 구성 메커니즘 (Step-by-Step)

유저가 "A 테넌트 하위의 모든 계층 구조를 다 보여줘"라고 요청했을 때 API Server가 단 수십 밀리초(ms) 내로 트리를 만드는 과정입니다.

Step 1. 어제 자 백엔드 데이터 초고속 로드 (Base)

API Server는 내부 자원(예: DuckDB 라이브러리 등)을 이용해 어제 자 Inventory Parquet 파일에서 A 테넌트/*에 해당하는 파일 목록을 쿼리합니다. Parquet는 열 지향(Columnar)이라 특정 경로를 기가바이트(GB) 단위 속도로 순식간에 읽어 가벼운 트리 구조(JSON)를 메모리에 올립니다.

Step 2. OpenSearch에서 '어제 새벽 이후'의 증분만 쿼리 (Delta)

OpenSearch에 쿼리를 날려 "어제 새벽 00:00부터 ~ 현재 시간"까지 발생한 로그 중 A 테넌트 하위의 이벤트만 가져옵니다.

  • 어제 새벽 이후에 새로 생긴 파일 목록 (ObjectCreated)
  • 어제 새벽 이후에 지워진 파일 목록 (ObjectRemoved)
  • 주의: 하루 동안 발생하는 파일 변동량은 50억 개에 비해 극히 일부(수천~수만 건 수준)이므로 OpenSearch 쿼리 속도는 엄청나게 빠릅니다.

Step 3. 메모리에서 두 데이터 병합 (Merge) 및 트리 완성

API Server가 Step 1에서 만든 가상 트리 구조에 Step 2의 증분 데이터를 반영합니다.

  • OpenSearch에서 삭제된 파일로 확인된 녀석들은 트리에서 제거.
  • OpenSearch에서 새로 생성된 파일들은 트리의 서브 디렉터리 경로에 추가.

Step 4. 유저에게 최종 계층 JSON 반환

최종적으로 100% 실시간 정합성이 맞춤화된 깊은 계층 구조 트리 데이터가 유저 화면에 뿌려집니다.


🎯 이 하이브리드 방식이 주는 압도적인 이점

  1. MinIO 본체 부하 "완전한 제로(0)"
  • 유저가 아무리 수백 번, 수천 번 깊은 계층 트리를 열어보고 하위 파일을 뒤져도 MinIO의 S3 메타데이터 엔진(List API)은 단 한 번도 호출되지 않습니다. 50억 개 인프라가 100% 안전하게 보호됩니다.
  1. OpenSearch 저장 공간 절약
  • OpenSearch는 원래 용도대로 '1~2주짜리 로그 분석' 스펙을 그대로 유지하면 됩니다. 과거 50억 개 데이터를 영구 보존할 필요가 없어 인프라 증설 압박이 없습니다.
  1. 100% 완벽한 실시간성 보장
  • 유저가 3초 전에 mc나 API로 올린 파일도 OpenSearch의 실시간 로그 분을 통해 베이스 트리에 결합되므로, 유저 눈에는 방금 올린 파일까지 계층 구조에 완벽히 실시간으로 포함되어 보입니다.

📌 요약 가이드

현업 유저들이 원하는 '깊은 계층적 탐색 및 검색'을 만족하기 위해 개발 팀이 API Server에 추가해야 할 핵심 로직은 "Parquet 파일 읽기 + OpenSearch 최근 24시간 로그 결합기(Aggregator)"입니다.

이 방식이라면 메타 DB에 수십억 건의 용량 데이터를 넣을 필요도 없고, 무거운 카프카 레이어를 전면 전개하지 않아도 50억 개 환경에서 유저들이 감탄할 만한 속도의 전체 계층 트리 뷰를 제공할 수 있습니다.

어제 자 전체 목록(Parquet)과 오늘 실시간 로그(OpenSearch)를 메모리 상에서 매핑하여 최종 트리를 완성하는 이 연동 개념이 개발 관점에서 실현 가능해 보이시는지 궁금합니다.

==

0개의 댓글