
AI 이미지·동영상 서비스의 업로드 경계를 어떻게 설계할 것인가
요약
AI 이미지 및 동영상 생성 서비스에서 미디어 업로드 기능을 단순 파일 저장이 아닌 독립적인 보안 경계로 설계하는 방법을 다룹니다. 용도(purpose)에 따른 권한 분리, 파일 형식 검증, 보안 강화 전략을 제시합니다.
핵심 포인트
- 업로드를 생성 워크플로우의 독립적인 보안 경계로 설계해야 함
- 파일 용도(purpose)를 명시하여 권한, 크기, 저장 위치를 차등 적용
- 파일명에 의존하지 않고 서버 측에서 Content-Type과 용도를 엄격히 검증
- 사용자 파일명을 스토리지 키로 직접 사용하지 말고 안정적인 구조로 변환
이미지 생성 서비스가 텍스트 입력만을 다룰 때는 입력 데이터가 비교적 단순합니다.
하지만 이미지 투 이미지 (Image-to-Image) 편집, 이미지 투 비디오 (Image-to-Video) 생성, 참조 이미지를 사용한 생성, 동영상 편집까지 대응하게 되면, 사용자는 이미지·동영상·음성을 업로드하게 됩니다.
이때, 업로드 기능을 단순한 「파일을 받아서 URL을 반환하는 API」로 구현하면 나중에 많은 문제가 발생합니다.
- 누구의 파일인지 알 수 없음
- HTML 등 예상치 못한 형식이 저장됨
- 모델이 수용할 수 없는 미디어가 생성 API까지 도달함
- 원래의 파일명이 스토리지 키 (Storage Key)에 섞임
- 동영상의 길이를 알 수 없어 생성 비용을 계산할 수 없음
- 관리자 화면용 이미지와 사용자 소재가 동일한 권한으로 저장됨
AI Image Editor에서 이미지·동영상 입력을 다룰 때는, 업로드를 스토리지의 보조 기능이 아니라 생성 워크플로우 (Workflow)의 독립적인 보안 경계 (Security Boundary)로 생각하고 있습니다.
이 기사에서는 그 경계를 어떻게 나눌지를 정리합니다.
같은 이미지 파일이라도 용도에 따라 필요한 규칙은 다릅니다.
예를 들어, 사용자가 모델에 전달하는 참조 이미지와 관리자가 블로그 기사에 설정하는 커버 이미지는 저장 위치·크기 제한·권한이 동일할 필요가 없습니다.
따라서 업로드 요청에는 파일뿐만 아니라 명시적인 purpose를 포함합니다.
type UploadPurpose =
| 'generation-source'
| 'blog-cover'
...
서버는 purpose에 따라 다음 항목을 결정합니다.
- 누가 업로드할 수 있는가
- 허용하는 Content-Type
- 최대 파일 크기
- R2의 저장 프리픽스 (Prefix)
- 반환하는 공개 URL
용도를 파일명이나 화면 URL로부터 추측하지 않는 것이 중요합니다. 요청의 의미를 입력으로서 명시하고, 서버 측에서 열거형 (Enum) 값으로 검증합니다.
업로드 API는 생성 API와는 별개의 입구입니다.
생성 요청 측에서 로그인을 확인하더라도, 업로드 API 측에서도 다시 한번 세션 (Session)을 확인해야 합니다. 그렇지 않으면 스토리지만 익명 사용자에게 개방될 가능성이 있습니다.
기본적인 순서는 다음과 같습니다.
Read session
|
v
...
관리자 화면용 blog-cover나 blog-inline-image에서는 일반적인 로그인 확인에 더해 관리자 역할 (Admin Role)을 요구합니다.
또한, 이용 정지 중인 계정이 파일만 계속 늘리지 않도록 계정 제한도 저장 전에 적용합니다.
브라우저에서 전달된 파일명만으로 형식을 판단하는 것은 위험합니다.
photo.png.html과 같은 이름도 만들 수 있고, 확장자와 Content-Type이 일치하지 않는 경우도 있습니다.
구현에서는 용도별로 허용하는 Content-Type을 열거합니다.
const GENERATION_SOURCE_TYPES = new Set([
'image/jpeg',
'image/png',
...
여기서 중요한 것은 「모델 프로바이더 (Model Provider)가 수용할 수 있는 형식」을 모두 통과시키는 것이 아닙니다.
애플리케이션으로서 보관·배포·모더레이션 (Moderation)할 수 있는 형식만 허용합니다. 프로바이더의 사양이 넓더라도, 프로덕트 측의 경계는 좁아도 상관없습니다.
나아가 파일 크기 제한도 용도별로 설정합니다. 생성 소재는 동영상을 포함하기 때문에 비교적 크고, 블로그 이미지는 더 작게 제한할 수 있습니다.
사용자가 보낸 파일명을 그대로 오브젝트 키 (Object Key)로 사용하는 것은 피하는 것이 안전합니다.
파일명에는 공백, Unicode, 경로로 해석될 수 있는 문자, 개인정보가 포함될 가능성이 있습니다. 또한, 동일한 이름의 파일에 의한 덮어쓰기도 고려해야 합니다.
안정적인 키는 다음과 같은 구조로 생성할 수 있습니다.
uploads/{userId}/{purpose}/{uploadId}.{extension}
예:
uploads/user_123/generation-source/9c20...e1.png
여기서는 원래의 파일명을 키의 식별자로 사용하지 않습니다. 확장자만 정규화하고, ID는 서버 측에서 생성합니다.
이 구조에는 몇 가지 이점이 있습니다.
- 사용자 단위로 오브젝트를 추적하기 쉬움
- 동일한 이름의 파일이 충돌하지 않음
- 용도별 라이프사이클 (Lifecycle) 규칙을 설정하기 쉬움
- 공개 URL로부터 원래의 로컬 파일명이 유출되지 않음
업로드 후, 클라이언트는 생성 요청(generation request)에 스토리지 URL을 포함합니다.
하지만 "올바른 CDN 도메인의 URL인가"와 "해당 사용자가 소유한 소재인가"는 동일하지 않습니다.
사용자가 다른 계정의 URL을 알고 있는 경우, 이를 입력 소재로 재전송할 수 있는 가능성이 있습니다.
따라서 실무 설계에서는 다음 중 하나가 필요합니다.
- 업로드 시 미디어 레코드(media record)를 생성하고, 생성 시
mediaAssetId와 소유자를 검증한다. - 스토리지 키(storage key)의 사용자 영역과 세션 사용자를 서버 측에서 대조한다.
단순히 URL의 접두사(prefix)만 확인하는 것보다, 데이터베이스 상의 소유 레코드를 확인하는 것이 확장성이 더 높습니다. 삭제, 이용 이력, 감사(audit)에도 동일한 레코드를 사용할 수 있기 때문입니다.
이 부분은 혼동하기 쉬운 포인트입니다.
업로드 API가 동영상을 수락한다고 해서, 모든 모델이 동영상을 입력으로 사용할 수 있는 것은 아닙니다.
업로드 경계(upload boundary)는 "이 애플리케이션이 안전하게 저장할 수 있는가"를 판단합니다. 모델 경계(model boundary)는 "선택한 모델과 모드가 이 소재를 사용할 수 있는가"를 판단합니다.
Upload validation:
Can the application store this media?
Model validation:
...
예를 들어, 이미지-to-비디오(image-to-video) 모드에서는 이미지만 허용하고, 동영상 편집 모드에서는 동영상을 필수 사항으로 지정할 수 있습니다. 참조 소재의 최대 개수도 모델마다 다릅니다.
두 경계를 분리함으로써, 새로운 모델을 추가할 때마다 업로드 API의 허용 형식을 매번 수정할 필요가 없어집니다.
동영상 생성 가격이나 제한은 입력 동영상의 길이에 따라 달라질 수 있습니다.
브라우저에서는 업로드와 병행하여 <video> 요소를 통해 메타데이터를 읽어 초(second) 단위 시간을 가져올 수 있습니다.
const objectUrl = URL.createObjectURL(file)
const video = document.createElement('video')
video.onloadedmetadata = () => {
...
}
이를 통해 업로드 대기 시간을 늘리지 않고도 UI에서 대략적인 비용을 표시할 수 있습니다.
단, 클라이언트가 보내는 초 단위 시간은 과금의 최종 근거로 삼지 않는 것이 안전합니다. 신뢰성이 필요한 경우에는 서버 측 또는 미디어 처리 기반(media processing infrastructure)에서 메타데이터를 다시 가져옵니다.
클라이언트 값은 UX를 위해, 검증된 값은 과금과 제한을 위해 사용한다고 역할을 나눕니다.
오브젝트 스토리지(object storage)의 내부 URL을 그대로 영구 저장하면, 나중에 CDN 도메인을 변경할 때 이관이 어려워집니다.
애플리케이션이 관리하는 CDN 기반 URL과 안정적인 스토리지 경로(storage path)를 조합하여 공개 URL을 만드는 것이 다루기 쉽습니다.
function buildMediaUrl(baseUrl: string, storagePath: string) {
return new URL(storagePath, `${baseUrl}/`).toString()
}
스토리지(storage)는 저장을 담당하고, CDN은 배포를 담당하며, 데이터베이스(database)는 소유 관계와 용도를 담당합니다.
이 세 가지를 동일한 "URL 문자열"로 취급하지 않는 것이 추후 변경을 용이하게 만듭니다.
AI 이미지·동영상 서비스의 업로드 기능은 폼(form)의 부수적인 기능이 아닙니다.
안전하고 확장하기 쉬운 경계를 만들기 위해서는 다음 사항이 중요합니다.
- 업로드 용도를 명시한다
- 저장 전에 인증, 계정 상태, 권한(role)을 확인한다
- Content-Type과 크기를 허용 목록(allowlist)으로 검증한다
- 스토리지 키를 서버 측에서 생성한다
- URL이 아닌 소유 레코드를 검증한다
- 업로드 제한과 모델 제한을 분리한다
- 동영상 메타데이터를 UX용과 과금용으로 나누어 다룬다
- 스토리지 경로와 공개 CDN URL을 분리한다
모델은 입력 소재를 처리하지만, 그 소재를 안전하게 전달받을 책임은 애플리케이션 측에 있습니다.
업로드를 명확한 경계로 설계하면, 새로운 모델이나 입력 형식을 추가하더라도 스토리지와 권한 규칙을 깨뜨리지 않고 확장할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기