AI 제품에 이미지 업로드 기능을 추가했습니다. 어려운 점은 그 주변의 모든 것이었습니다.
요약
AI 제품에 이미지 업로드 기능을 추가할 때 발생하는 복잡한 엔지니어링 고려 사항을 다룹니다. 단순한 기능 구현을 넘어 파일 검증, 모더레이션, 사용자 쿼터 관리, 예외 처리 등 시스템의 신뢰성을 확보하기 위한 설계 과정을 설명합니다.
핵심 포인트
- 이미지 업로드는 단순한 파일 전송 이상의 복잡한 요청 경계 변화를 야기함
- 파일 형식, 크기 제한, 모더레이션 시점 등 다양한 검증 단계가 필요함
- 리소스 낭비를 막기 위해 자격 확인(Eligibility check)을 요청 초기 단계로 이동해야 함
- 부분적 실패 시 데이터 정리 및 동시 작업 보호 등 예외 처리가 필수적임
최근 저는 AIMakeTattoo에 Image to Tattoo workflow를 추가했습니다.
사용자 관점에서 이 기능은 매우 간단해 보입니다:
- 사진 또는 스케치 업로드.
- 타투 스타일 선택.
- 비율 및 복잡도 조정.
- 타투 디자인 생성.
애플리케이션 관점에서 보면, 이미지 업로드는 훨씬 더 방대한 고려 사항들을 불러왔습니다:
- multipart request (멀티파트 요청) 파싱
- 파일 검증 (file validation)
- 이미지 모더레이션 (image moderation)
- 익명 및 로그인 사용자 쿼터 (quotas)
- 유료 크레딧 (paid credits)
- 동시 작업 보호 (concurrent-job protection)
- 프로바이더 업로드 (provider uploads)
- OAuth 상태 손실 (OAuth state loss)
- 개인정보 보호 결정 (privacy decisions)
- 부분적 실패 후 정리 (cleanup after partial failure)
이미지 생성 요청 자체는 비교적 쉬운 부분 중 하나였습니다.
이미지 업로드가 요청 경계(request boundary)를 변화시켰습니다
기존의 text-to-image (텍스트-투-이미지) 흐름은 구조화된 필드(structured fields)를 수용했습니다.
서버는 프롬프트(prompt)와 선택된 컨트롤(controls)을 검증하고, 사용자가 생성 자격이 있는지 확인하며, 작업을 제출하고 작업 ID(job ID)를 반환할 수 있었습니다.
파일을 추가하는 것은 그 경계를 변화시켰습니다.
이제 라우트(route)는 다음과 같은 여러 가지 새로운 질문에 답해야 했습니다:
- 어떤 파일 형식을 허용해야 하는가?
- 개별 이미지의 최대 크기는 얼마인가?
- 전체 multipart request에 별도의 크기 제한을 두어야 하는가?
- 모더레이션(moderation)은 언제 실행되어야 하는가?
- 자격이 없는 요청이 모더레이션 단계에 도달해도 되는가?
- 애플리케이션이 원본 이미지를 저장해야 하는가?
- 사용자가 파일을 선택한 후 로그인하면 어떤 일이 발생하는가?
- 어떤 실패 상황에서 생성 잠금(generation lock)을 해제해야 하는가?
- 어느 시점에 유료 크레딧을 소모해야 하는가?
이 질문들 중 그 어떤 것도 생성된 이미지의 품질을 직접적으로 개선하지는 않았습니다.
하지만 이 질문들은 해당 기능이 제품의 신뢰할 수 있는 부분으로서 동작할지 여부를 결정했습니다.
첫 번째 요청 흐름은 작동했지만, 불필요한 작업을 수행했습니다
저의 초기 흐름은 대략 다음과 같았습니다:
- Multipart 요청을 파싱(Parse)합니다.
- 사용자 또는 익명 방문자를 식별(Resolve)합니다.
- 활성 생성 잠금(Active-generation lock)을 획득합니다.
- 업로드된 이미지를 검토(Moderate)합니다.
- 무료 할당량(Free quota) 및 유료 크레딧(Paid credits)을 확인합니다.
- 생성 제공자(Generation provider)에게 이미지를 업로드합니다.
- 생성 작업(Generation job)을 제출합니다.
- 작업 메타데이터(Job metadata)를 저장합니다.
- 올바른 허용량(Allowance)을 차감합니다.
해피 패스(Happy path)는 잘 작동했습니다.
하지만 사용 가능한 모든 허용량을 명백히 소진한 사용자조차도, 요청이 거부되기 전에 이미지 검토(Image moderation) 단계에 도달할 수 있었습니다.
이는 애플리케이션이 결코 진행될 수 없는 생성을 위해 작업을 수행하고 있음을 의미했습니다.
조기 자격 확인(Early eligibility check)을 추가했습니다
저는 읽기 전용(Read-only) 자격 확인 단계를 요청의 시작 부분에 더 가깝게 이동시켰습니다.
수정된 흐름은 다음과 같습니다:
- 요청을 파싱(Parse)하고 검증(Validate)합니다.
- 사용자 또는 익명 방문자를 식별(Resolve)합니다.
- 현재의 허용량(Allowance) 및 크레딧(Credit) 상태를 읽습니다.
- 명백히 자격이 없는 요청을 거부합니다.
- 활성 생성 잠금(Active-generation lock)을 획득합니다.
- 이미지를 검토(Moderate)합니다.
- 최종적인 권한 있는 자격 확인(Authoritative eligibility checks)을 실행합니다.
- 이미지를 업로드하고 생성 작업(Generation job)을 제출합니다.
- 작업 메타데이터(Job metadata)를 영속화(Persist)합니다.
- 올바른 허용량(Allowance) 또는 크레딧(Credit)을 차감합니다.
조기 확인(Early check)은 다음과 같은 명백한 사례들을 거부할 수 있습니다:
- 일일 허용량을 모두 소진한 익명 방문자
- 관련 한도에 도달한 로그인 사용자
- 무료 생성 횟수와 유료 크레딧이 모두 없는 사용자
하지만 이 조기 확인은 의도적으로 권고(Advisory) 수준으로 유지됩니다.
이는 아무것도 차감하지 않으며, 최종 생성을 승인하지도 않습니다.
단지 다음 질문에만 답할 뿐입니다:
이 요청이 명백히 계속 진행할 수 없는 상태인가?
조기 확인이 권한을 가질 수 없는 이유
할당량(Quota) 읽기 작업은 즉시 오래된 정보(Stale)가 될 수 있습니다.
사용자에게 무료 생성 기회가 한 번 남아있고, 거의 동시에 두 개의 요청을 보낸다고 가정해 봅시다.
두 요청 모두 동일한 초기 상태를 볼 수 있습니다.
유료 크레딧 잔액 또한 조기 확인 시점과 제공자 작업이 제출되는 시점 사이에 변경될 수 있습니다.
따라서 최종 확인은 다른 질문에 답합니다:
지금 이 요청이 생성 권한 (generation right)을 소모하는 것을 허용해도 됩니까?
이 두 가지 확인 절차는 반복적으로 보일 수 있지만, 각각 서로 다른 책임을 가집니다.
조기 확인 (early check)은 불필요한 작업을 방지합니다.
최종 확인 (final check)은 상태 변경 (state change)을 보호합니다.
이는 또한 권고 확인 (advisory check) 단계에서 확신할 수 있는 결정을 내릴 수 없을 때, 실패하더라도 허용 (fail open)되어야 함을 의미합니다. 최적화 단계에서의 일시적인 문제가 정당한 사용자를 거절하는 새로운 이유가 되어서는 안 됩니다.
락 (lock)이 가장 위험한 실패 모드를 만들어냈습니다
AIMakeTattoo는 사용자당 단 하나의 활성 생성 (active generation)만 허용합니다.
이는 실수로 인한 중복 제출을 줄이고, 동시에 여러 요청이 동일한 허용량 (allowance)을 사용하려고 시도하는 것을 방지합니다.
해당 라우트 (route)는 워크플로 (workflow)의 비용이 많이 드는 단계가 시작되기 전에 활성 작업 락 (active-job lock)을 획득합니다.
명백한 거절 경로들은 해당 락을 올바르게 해제했습니다.
더 위험한 문제는 락이 이미 획득된 이후에 예기치 않은 예외 (exception)가 발생하는 경우였습니다.
예를 들어:
- 사용자가 생성을 시작합니다.
- 애플리케이션이 락을 획득합니다.
- 모더레이션 (moderation) 서비스에서 예기치 않은 오류가 발생합니다.
- 요청이 실패합니다.
- 락이 그대로 남아 있습니다.
- 애플리케이션이 여전히 작업이 활성 상태라고 판단하기 때문에 다음 시도가 거절됩니다.
원래의 실패는 일시적이었을 수 있습니다.
하지만 누출된 락 (leaked lock)은 이를 지속적인 제품 문제로 변질시킵니다.
락 이후의 모든 작업에는 정리 경로 (cleanup path)가 필요합니다
이 교훈은 이미지 모더레이션보다 더 광범위했습니다:
일단 락이 획득되면, 이후의 모든 작업은 반드시 정리 경계 (cleanup boundary) 내에서 실행되어야 합니다.
여기에는 다음 단계 중 발생하는 실패가 포함됩니다:
- 모더레이션 (moderation)
- 최종 할당량 검증 (final quota validation)
- 유료 잔액 검증 (paid-balance validation)
- 제공자 업로드 (provider upload)
- 제공자 작업 제출 (provider job submission)
- 로컬 작업 영속화 (local job persistence)
- 허용량 또는 크레딧 소모 (allowance or credit consumption)
단순화된 구조는 다음과 같습니다:
const lock = await acquireActiveJobLock(userKey);
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기