기술

Asset Studio는 어떻게 구현했나

AAnonymous
10분 읽기

들어가며

지난 글인 게임 제작 계획에서는 어떤 장르를 어떤 순서로 만들고 있는지 설명했습니다. 그 글에서 Asset Studio는 짧게만 등장했습니다. Story Quiz와 Mystery의 캐릭터와 배경 자산을 함께 관리하는 도구라는 정도였습니다.

이번 글에서는 그 문장을 실제 구현 기준으로 풀어보겠습니다. Asset Studio는 단순히 이미지를 뽑는 화면이 아닙니다. 지금 Omnilude에서는 이 화면을 중심으로 자산을 모으고, 프롬프트를 다듬고, 생성 작업을 실행하고, 결과를 검수한 뒤 라이브러리로 등록합니다. 프론트엔드 백오피스는 작업대 역할을 하고, game-service는 자산 도메인을 관리하고, ai-service는 생성 실행기를 맡습니다.

왜 Asset Studio를 따로 만들었나

게임을 실제로 만들기 시작하면 자산은 생각보다 빨리 흩어집니다. Story Quiz에는 캐릭터, 배경, 블록용 이미지가 있고 Mystery에는 시나리오 캐릭터와 장소, 노트용 이미지가 있습니다. 같은 사람이 같은 세계관을 만들더라도 프롬프트, 스타일 프리셋, 생성 결과, 승인 상태, 실제 사용 여부를 각각 다른 화면에서 관리하면 금방 통제가 어려워집니다.

그래서 Asset Studio는 처음부터 갤러리보다 작업실에 가깝게 설계했습니다. 중요한 점은 이 도구가 특정 장르 전용이 아니라는 것입니다. Story Quiz에서 이미 만든 자산과 Mystery 초안에서 끌어온 자산을 같은 방식으로 다루고, 생성된 결과를 검수 가능한 라이브러리로 남기고, 나중에 다른 도메인에서 쓰이고 있는지까지 추적합니다. 이 관점이 없으면 AI 생성은 편해 보여도 운영 자산으로는 남지 않을 것이라 생각했습니다.

백오피스 화면은 어떻게 구성되어 있나

프론트엔드의 백오피스에는 Asset Studio 전용 라우트가 따로 있습니다. 배경과 캐릭터는 각각 다른 엔트리로 열리지만, 내부적으로는 같은 작업대 컴포넌트를 공유합니다. 목록 화면은 트리와 플랫 뷰를 모두 지원하고, 최근 수정한 항목도 함께 보여줍니다. 즉, 디렉터리 중심으로 정리할 수도 있고 최근 작업 중심으로 다시 들어갈 수도 있습니다.

상세 화면으로 들어가면 구조가 더 분명해집니다. 이 화면은 사실상 하나의 에셋 제작 콘솔입니다. 상단에서는 이름과 승인 상태를 관리하고, 본문에서는 임포트 소스, 프롬프트, 생성, 미디어, 이력을 탭 단위로 다룹니다. 구현을 보면 임포트 소스 검색 상태와 생성된 미디어 액션 상태를 페이지 레벨에서 유지하도록 만들어 두었습니다. 탭을 오가더라도 맥락이 끊기지 않게 하려는 의도입니다.

여기서 중요한 분기점이 하나 있습니다. 에셋 목록 조회, 디렉터리 수정, 생성 결과 등록 같은 자산 관리 API는 모두 /api/asset-studio/*를 통해 game-service로 갑니다. 반면 실제 생성 요청은 Asset Studio API가 아니라 /api/media-generation/generate로 빠져서 ai-service로 전달됩니다. 이 분리는 의도적입니다. 자산 관리와 생성 실행은 같은 화면 안에 있지만 같은 책임이 아닙니다.

실제 생성 흐름은 어떻게 흘러가나

실제 사용자 경험은 꽤 단순합니다. 에셋 하나를 열고, 기존 자산이나 Story Quiz/Mystery 소스를 불러오고, 프롬프트를 조정한 뒤 생성 버튼을 누릅니다. 하지만 내부 흐름은 다음처럼 나뉘어 있습니다.

  1. 백오피스 상세 화면에서 현재 assetItem.generationConfig를 읽습니다.
  2. 생성 요청은 /api/media-generation/generate로 전송되고, 프론트는 즉시 jobId를 받습니다.
  3. 프론트는 그 jobId로 backbone SSE 스트림을 구독해 진행률을 받습니다.
  4. ai-service는 실제 프로바이더에서 결과를 만들고 storage에 업로드합니다.
  5. 프론트는 SSE에서 outputUrlsourceUrl을 받아 미리보기를 보여줍니다.
  6. 사용자가 괜찮다고 판단하면 그때 /api/asset-studio/asset-items/{id}/media로 등록합니다.

이 마지막 단계가 중요합니다. 생성이 끝났다고 해서 바로 Asset Studio 라이브러리에 저장하지 않습니다. 생성 결과는 먼저 검토 대상입니다. 운영 자산으로 채택하는 순간은 사용자가 등록 버튼을 눌렀을 때입니다. 구현상으로도 이 흐름은 분명합니다. 생성 훅은 SSE 결과를 받아 프리뷰 상태를 만들고, 별도 액션이 createGeneratedMediaApi를 호출해 라이브러리에 편입합니다.

이 구조 덕분에 Asset Studio는 프롬프트 박스가 아니라 검수 가능한 제작 파이프라인이 됩니다. 실패한 결과, 애매한 결과, 파이프라인 후처리 결과를 모두 임시 상태로 다뤘다가 선택적으로 등록할 수 있기 때문입니다.

game-service는 무엇을 책임지나

game-service의 Asset Studio는 단순 CRUD 묶음이 아닙니다. 실제로는 자산 라이브러리의 주인에 가깝습니다. 컨트롤러 수준에서도 디렉터리, 아이템, 임포트, export/import portability, generated media, winner 지정, reorder, bulk move, import source 조회 등이 한 도메인으로 묶여 있습니다.

이 서비스가 특히 중요한 이유는 세 가지입니다.

1. 기존 게임 도메인에서 소스를 끌어온다

AssetStudioImportService는 Mystery의 DraftLocation, DraftCharacter, Story Quiz의 StoryQuizBackgroundAsset, StoryCharacter를 읽어서 임포트 소스 카드로 변환합니다. 이때 단순 이름만 가져오지 않고 prompt, provider, stylePreset, 메타 정보까지 함께 엮습니다. 그래서 Asset Studio는 새로 모든 자산을 처음부터 만드는 곳이 아니라, 이미 있는 게임 데이터에서 자산 제작을 이어받는 곳이 됩니다.

2. 생성 결과를 운영 자산으로 다듬는다

AssetGeneratedMediaService는 생성된 결과를 라이브러리 레코드로 저장하고, winner를 지정할 수 있게 합니다. 이미지 타입 winner가 바뀌면 아이템 썸네일까지 함께 갱신됩니다. 반대로 실제 사용 중인 자산의 대표 미디어는 함부로 지울 수 없게 막아둡니다. 생성 결과를 쌓아두는 것과 운영 자산을 채택하는 것을 분리한 셈입니다.

3. 실제 사용 여부를 추적한다

AssetReferenceSyncService는 Story Quiz와 Mystery 안에서 URL이 실제로 사용 중인지 검사해 isReferenced를 동기화합니다. Story Quiz 쪽에서는 커버, 카드, 캐릭터 기본 이미지, 감정 이미지, 배경, 블록 미디어를 훑고, Mystery 쪽에서는 시나리오 썸네일, 캐릭터 아바타, 노트, 오브젝트 이미지까지 확인합니다. 이 로직이 있기 때문에 Asset Studio는 예쁜 자산 저장소가 아니라, 라이브 콘텐츠와 연결된 자산 관리 도구가 됩니다.

ai-service는 무엇을 책임지나

ai-service 쪽 책임은 더 명확합니다. 이 서비스는 에셋 라이브러리를 모릅니다. 대신 생성 실행을 책임집니다. 백오피스가 보내는 generationConfig를 해석하고, 어떤 프로바이더와 워크플로우를 쓸지 결정하고, 비동기 작업으로 실행하고, SSE로 진행률을 흘려보냅니다.

구현을 보면 MediaGenerationService는 먼저 계정별 동시 실행 슬롯을 확인합니다. 그다음 GenerationConfigParser가 설정을 파싱합니다. 여기서 파싱 우선순위는 꽤 중요합니다. generationConfig.media[mediaType].providers[providerCode]가 최우선이고, 없으면 default, 마지막으로 루트 레벨이 fallback입니다. 이 구조 덕분에 하나의 에셋이 미디어 타입별, 프로바이더별로 다른 설정을 가질 수 있습니다.

파싱이 끝나면 ai-service는 바로 생성하지 않고 DTE Job으로 큐에 넣습니다. 프론트가 받는 jobId는 이후 SSE 구독 키와 동일하게 유지됩니다. 즉, 백오피스는 긴 요청을 붙잡고 기다리지 않고, 작업 ID를 중심으로 비동기 생성 과정을 추적합니다.

ComfyUI 연결이 중요한 이유

지금 구현 기준으로 Asset Studio가 흥미로운 지점은 ComfyUI 연결입니다. ComfyUIProvider는 코드 안에 워크플로우를 박아두지 않습니다. 런타임 설정은 provider 설정에서 읽고, 실행 워크플로우는 DB의 workflow 레코드에서 가져옵니다. 그리고 단순 prompt-to-image만이 아니라 media-to-media 파이프라인도 지원합니다.

이게 왜 중요하냐면, Asset Studio의 생성 설정이 단순 prompt 문자열이 아니기 때문입니다. 사용자는 workflowId, workflowPipeline, seed, width, height, 입력 이미지 URL 같은 런타임 오버라이드를 함께 보낼 수 있습니다. 실제 구현에서도 파이프라인 워크플로우를 선택하면 기존 generated media를 입력으로 넣어 후처리하거나 변형할 수 있게 되어 있습니다.

결국 이 구조는 AI 생성 화면을 범용 프롬프트 입력창으로 두지 않고, 데이터 기반 워크플로우 실행기로 바꿉니다. 같은 캐릭터라도 다른 provider, 다른 workflow, 다른 입력 이미지로 다시 돌릴 수 있고, 그 결과를 검수 후 라이브러리에 축적할 수 있습니다.

향후 ComfyUI 자체를 더 자세히 다뤄보는 글도 괜찮을 것 같습니다.

왜 game-service와 ai-service를 나눴나

이 분리는 단순한 MSA 취향 문제가 아닙니다. 자산 도메인과 생성 실행기는 실패 방식이 다르고, 수명 주기도 다르고, 운영자가 보는 관점도 다릅니다.

game-service는 디렉터리 구조, 승인 상태, 실제 사용 여부, portability, history처럼 오래 남아야 하는 정보를 다룹니다. 반면 ai-service는 어느 프로바이더가 살아 있는지, 어떤 workflow를 선택할지, 현재 몇 개 작업을 돌릴 수 있는지, 생성이 어느 단계까지 왔는지를 다룹니다. 둘을 합치면 구현은 잠깐 편할 수 있어도 경계가 금방 흐려집니다.

지금 구조에서는 Asset Studio가 이 둘을 연결합니다. 백오피스는 한 화면에서 작업하지만, 저장과 실행을 구분해서 다룹니다. 이 구분 덕분에 앞으로 Story Quiz와 Mystery뿐 아니라 다른 장르가 늘어나도 자산 라이브러리와 생성 인프라를 별도로 진화시킬 수 있습니다.

마무리

지난 글에서 Asset Studio는 잠깐 지나가는 문장이었습니다. 하지만 실제 구현을 따라가 보면 이것은 이미지 생성 기능 하나를 뜻하지 않습니다. Omnilude에서 Asset Studio는 게임 자산을 모으고, 기존 도메인에서 소스를 끌어오고, AI 생성 작업을 실행하고, 결과를 검수하고, 실제 사용 여부까지 추적하는 제작 시스템입니다.

이런 이유로 저는 Asset Studio를 게임 개발의 부속 화면으로 보지 않습니다. 오히려 Story Quiz와 Mystery를 포함한 전체 제작 파이프라인이 만나는 접점에 가깝습니다. 다음 글에서는 이 자산 시스템이 실제 출판 파이프라인이나 특정 장르의 제작 흐름 안에서 어떻게 쓰이는지 더 좁혀서 설명해보겠습니다.

이 글은 리뷰어인 제가 읽어도 내용이 복잡하고 이해하기 어려운 암호 같은 글이 되어버렸습니다.

이미지 첨부를 지원하도록 블로그를 업데이트하고, 기존 글들을 포함해 전체 가독성을 높이는 작업도 조만간 진행하겠습니다.

힘들게 작성한 글인 만큼, 일단 게시하겠습니다.