지난 11월, 저는 본업을 마친 후 밤 11시에 휴대폰으로 주력(day job)을 하면서 에셋 40개 업로드를 진행했습니다. 작업 도중 연결이 끊겼고, 아침에 스크립트를 재시작하면서 파일 17개를 중복으로 다시 올렸습니다. Adobe는 중복 제출 건에 대해 계정을 플래그 지정(flags accounts)하기 때문에, 이 문제를 해결하는 데 원래 배치 작업을 진행할 때보다 더 많은 시간이 걸렸습니다. 그날 밤 저는 스케줄링이 지루하고, 자동적이며, 수동으로 실수할 수 없어야겠다고 결심했습니다.
지금 저에게 실제로 효과적인 방법은 1 vCPU, 2 GB RAM 서버(GPU 없음)를 사용하면서 풀타임 직장과 병행하여 주간 배치 작업을 실행하는 것입니다.
하나의 cron 창, 하나의 프로세스
저는 매주 일요일 저녁에 전체 배치가 처음부터 끝까지 돌아가도록 하나의 주간 시간대를 예약합니다. 10분이라도 시간이 날 때마다 임시로 업로드하는 방식이 아닙니다. 이유는 간단합니다. 시작했다가 멈추는 배치는 중복으로 실행되는 배치이기 때문입니다. 오케스트레이터(Hermes Agent, 무료)가 작업을 시작하고, 작업 내부에는 한 번에 정확히 하나의 프로세스만 존재합니다. 코어 1개짜리 서버에서 이미지 작업을 병렬로 분산시키는 것은 아무것도 더 빠르게 만들지 못하며, 단지 OOM kill을 유발할 뿐입니다. 모든 과정은 순차적입니다: 생성(generate) → 렌더링(render) → 패키징(package) → 메타데이터 작성(write metadata) → 원장 업데이트(update the ledger).
만약 한 단계가 실패하면, 오케스트레이터는 전체 배치를 재실행하는 것이 아니라 해당 단계를 재시도합니다. 이 차이점이 저를 주차장에서의 사고 반복으로부터 구해주었습니다.
원장이 모든 안전망이다
제 파이프라인에서 가장 중요한 스크립트는 화려한 스크립트가 아닙니다. 바로 upload_tracker.py라는, 완료된 모든 에셋을 기록하는 평면적인 원장(flat ledger)입니다. 업로드 단계가 실행되기 전에, 배치 작업은 이 원장에게 무엇이 보류 중인지 묻습니다. 이미 기록된 것은 아무리 자주 작업을 재실행해도 건너뜁니다.
python upload_tracker.py pending items.txt
python upload_tracker.py done
## 메타데이터 헤더는 스타일이 아니라 통과/실패 여부
제가 운영하는 세 채널 모두 잘못된 CSV 헤더를 즉시 거절합니다. Adobe는 5개 열 형식을 요구하고, Vecteezy는 4개 열을, Dreamstime은 15개 열을 요구합니다. 저는 이걸 수동으로 여러 번 틀렸고, 각각의 거절이 검토 주기(review cycle)를 소모했으며, 특히 Dreamstime에서는 이게 며칠씩 기다림을 의미할 수 있습니다. 이제 `make_metadata.py`는 각 플랫폼이 요구하는 정확한 헤더만 작성하고 그 외에는 아무것도 쓰지 않습니다:
python make_metadata.py ./my-assets --platform adobe --out metadata.csv --ext .jpg
python make_metadata.py ./my-assets --platform vecteezy --out vecteezy.csv --ext .jpg
기본적으로 파일명을 제목으로 대문자 처리(title-case)하여 첫 번째 작업에는 괜찮습니다. 하지만 어떤 것을 제출하기 전에 `titles.json`에서 제목과 키워드를 교체합니다. 왜냐하면 파일명 기반의 제목은 검토 과정에서 스팸처럼 보일 가장 빠른 방법이기 때문입니다.
## 2GB 메모리 관리 습관
이미지 생성부터 패키징까지 전체 파이프라인을 2GB RAM 내에서 실행합니다. 이것이 작동하는 것은 하드웨어 덕분이 아니라, 습관 덕분입니다:
- 무거운 렌더링 단계에서는 서브프로세스당 하나의 파일만 사용하므로, 단계가 끝나면 메모리가 실제로 OS에 반환됩니다.
- 모든 에셋 처리 후 `gc.collect()`를 실행합니다. RSS(Resident Set Size)가 40개 에셋 배치에서 올라가다가 30번째 에셋에서 작업이 중단되는 것을 보면 미신처럼 들리지만 효과적입니다.
- 평균값 대신 최대값을 측정해야 합니다. `resource.getrusage(RUSAGE_SELF).ru_maxrss`를 사용하세요. 평균값은 당신을 죽이는 한계치에 대해 아무것도 알려주지 않습니다.
- 검토 시에는 60 dpi로 미리보기를 렌더링하고, 최종 출력물일 때만 300 dpi를 사용합니다.
AI 이미지 생성 자체는 같은 창에서 진행하며, 이미지당 비용이 충분히 작아서 실제 병목 현상은 돈이 아니라 저의 저녁 시간이었습니다. 이 창을 자동화한 것이 문제를 해결했습니다.
## 패키징 단계에서 제품이 실제로 만들어진다
실제 커버가 있는 zip 파일은 팔리지만, 내용물만 담긴 파일은 그렇지 못합니다. 배치(batch)가 원장(ledger) 및 메타데이터 검사를 통과하면, `pack_product.py` 스크립트가 각 제품을 묶습니다. 이 스크립트는 파일을 압축하고 단 하나의 명령으로 1280x720 커버와 600x600 썸네일을 렌더링합니다. 커버는 Pillow에서 가져오며, PDF 제품은 `md2pdf.py`를 통해 생성되는데, 이 스크립트는 Markdown 파일을 reportlab을 사용하여 스타일이 적용된 PDF로 변환합니다. 이 두 가지가 전체 파이프라인에 사용되는 유일한 외부 패키지입니다.
폴더 구조는 고정되어 있고 지루합니다: 원본 출력물은 `assets/`, 제출 준비 파일 및 CSV는 `output/`, 완성된 zip과 커버는 `packs/`, 메모와 로그는 `research/`를 사용합니다. `research/` 폴더는 영원히 보관하세요. 저는 한 번 "정리"하려고 이 폴더를 삭제했다가, 다시 만들지 않은 세 가지 제품의 원자료를 잃어버린 적이 있습니다.
## 현재 일주일 루틴
주중에는 아무것도 하지 않습니다. 준비되는 새로운 자산은 필요할 때마다 `assets/`에 던져 넣습니다. 일요일이 되면 크론(cron) 작업 시간이 돌아옵니다: 대기 중인 모든 것을 생성하고, 렌더링하며, 패키징하고, 세 개의 CSV 파일을 만들고, 원장에 표시한 후, 월요일 아침 커피와 함께 제가 검토할 수 있도록 모든 것을 준비해 놓습니다. 검토 및 제출에는 수동으로 20분이 걸립니다. 플랫폼 리뷰어가 보기 전에 제목에 사람의 눈이 필요하기 때문입니다. 이 시스템은 그 작은 서버에서 약 19,000줄의 파이프라인 코드와 규칙을 뒷받침하는 300페이지 이상의 연구 자료를 바탕으로 39개 제품, 약 856MB의 결과물을 패키징했습니다.
이 네 개의 스크립트는 제가 주차장에서 그날 밤에 가졌으면 좋았을 패키징 계층입니다. 저는 이들을 제가 실제로 실행하는 방식 그대로 묶어 파이프라인 스타터 키트(pipeline starter kit)로 만들었습니다. 만약 이 게시물에서 다시 구축하는 대신 정확한 원장, 메타데이터 빌더, PDF 변환기, 그리고 패커를 원한다면, [여기를 확인하세요](https://revanzart-shop.revanzart.workers.dev/go/pipeline-starter-kit?src=devto).
_디지털 제품을 제작 중이신가요? [디지털 제품 판매자를 위한 무료 가이드 및 키트](https://revanzart-shop.revanzart.workers.dev/guides)를 확인하세요._
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28