메인 콘텐츠로 건너뛰기
자주 묻는 질문, 성능 최적화, 오류 처리에 대한 모범 사례를 정리했습니다. 연동 전에 통독하는 것을 권장합니다.

작업 제출과 폴링

제출 엔드포인트는 모두 비동기 작업입니다. 제출 후 task_id를 반환하며, 이후 GET /v1/midjourney/{task_id}를 주기적으로 조회하여 상태를 확인하고 SUCCESS / FAILURE가 될 때까지 기다립니다.
import time, httpx

def wait_task(task_id, timeout=300):
    deadline = time.time() + timeout
    while time.time() < deadline:
        resp = httpx.get(f"{HOST}/v1/midjourney/{task_id}",
                         headers={"Authorization": f"Bearer {API_KEY}"}).json()
        if resp["status"] in ("SUCCESS", "FAILURE"):
            return resp
        if resp["status"] == "MODAL":
            raise RuntimeError(f"task {task_id} 작업을 완료하려면 /modal 호출이 필요합니다")
        time.sleep(3)
    raise TimeoutError(task_id)
  • 폴링 주기: 3~5초에 한 번을 권장합니다. 더 높은 빈도는 의미가 없으며 할당량만 낭비합니다.
  • web 요청 안에서 동기적으로 차단(blocking)하며 작업 완료를 기다리지 마세요. 제출 후 즉시 task_id를 반환하고, 프런트엔드에서 비동기로 폴링하도록 하세요.

프롬프트 설계

좋은 프롬프트:
a serene mountain lake at sunrise, photorealistic, soft golden light,
mist rising from water, snow-capped peaks in distance --ar 16:9 --v 8.1 --s 100
  • 주체를 앞에: 먼저 주체, 다음으로 장면 묘사, 마지막에 수식어 순으로 작성합니다.
  • 구조화 파라미터를 명시적으로: --ar / --v / --s(또는 대응하는 body 필드)를 사용하면 기본값에 의존하는 것보다 더 통제하기 쉽습니다.
  • 모호한 단어 피하기: photorealisticrealistic보다 더 명확합니다.
피해야 할 것: 지나치게 추상적인 표현(“make it good”), 산만한 주체(여러 병렬 객체에 주종 구분이 없는 경우), 단어에 따옴표 붙이기(리터럴 값으로 처리됨). Niji 애니메이션: niji: true + version: "7"을 전달하면 플랫폼이 --niji 7로 정규화하며, 과금은 midjourney@imagine-niji7로 처리됩니다.

참조 이미지 모범 사례

출처권장 방법주의
사용자 업로드먼저 자체 OSS / CDN에 저장한 뒤, 제출 시 해당 URL을 전달base64를 직접 전달하지 마세요(대역폭 낭비)
공개 URL직접 전달SSRF(공개 접근 가능해야 함) 및 12 MiB 제한 주의
서드파티 / 기타 산출물먼저 자체 OSS로 전송서드파티 URL은 만료될 수 있음
  • 5 MiB 미만으로 압축: 플랫폼 상한은 12 MiB이지만, 작은 이미지가 전송 / 처리 모두 더 빠릅니다.
  • 형식은 PNG / JPG / WebP 모두 가능하며, 고품질 JPG를 권장합니다.
  • 해상도는 1024~2048 px로 충분하며, 그 이상은 낭비입니다.
  • 참조 이미지 가중치 iw(0~3, 기본 1): >1이면 원본에 더 가깝고, <1이면 더 자유롭습니다.

오류 처리와 재시도 전략

code의미재시도 전략
1 / 200성공
4 VALIDATION_ERROR파라미터 오류❌ 재시도하지 말고 파라미터를 수정
3 NOT_FOUND사용 가능한 인스턴스 없음 / task_id 미존재인스턴스 불가 시 잠시 후 재시도 가능; task_id 미존재 시 재시도하지 말 것
9 FAILURE서비스 거부 / 내부 오류⏳ 재시도 가능, 지수 백오프(1s, 4s, 16s)
21 MODAL비종료 상태✅ 계속 /modal 호출
24 BANNED_PROMPT금칙어❌ 재시도하지 말고 prompt 수정; 자동 환불됨
429레이트 리밋⏳ 지수 백오프 + jitter
5xx / 네트워크 오류서버 / 네트워크⏳ 지수 백오프, 네트워크 오류는 즉시 1회 재시도 가능
import time, random, httpx

def submit_with_retry(payload, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            r = httpx.post(f"{HOST}/v1/midjourney/generations/imagine",
                           json=payload,
                           headers={"Authorization": f"Bearer {API_KEY}"},
                           timeout=30)
            data = r.json()
            if r.status_code == 200 and data["code"] in (1, 200):
                return data
            if data["code"] in (4, 24):
                raise ValueError(data["description"])      # 재시도 불가
            if data["code"] == 3 and "task" in data["description"]:
                raise ValueError(data["description"])      # task_id 미존재
            # 그 외(9 / 429 / 5xx)는 재시도 가능
        except httpx.RequestError:
            pass
        time.sleep((4 ** attempt) + random.uniform(0, 1))  # 1s / 4s / 16s ...
    raise RuntimeError(f"최대 재시도 횟수 {max_attempts}에 도달")

2차 작업 플로우

# imagine → 폴링 → upscale
imagine_id = submit({"prompt": "a cat"})["data"][0]["task_id"]
result = wait_task(imagine_id)           # grid_image_url + 이미지 4장 image_urls + buttons
upscale_id = submit_to("/upscale", {"task_id": imagine_id, "index": 2})["data"][0]["task_id"]
final = wait_task(upscale_id)            # upscale 로컬 합성, 1~2s
single_image = final["image_urls"][0]
부분 리페인트(inpaint → modal 2단계):
imagine_id = submit({"prompt": "a portrait"})["data"][0]["task_id"]; wait_task(imagine_id)
upscale_id = submit_to("/upscale", {"task_id": imagine_id, "index": 1})["data"][0]["task_id"]; wait_task(upscale_id)

inpaint_id = submit_to("/inpaint", {"task_id": upscale_id})["data"][0]["task_id"]  # status=modal
# 프런트엔드에서 mask 그리기(흰색=리페인트 영역), 자체 OSS에 업로드하여 mask_url 획득
final = submit_to("/modal", {
    "task_id": inpaint_id,
    "prompt": "replace the eyes with cybernetic blue eyes",
    "mask_url": "https://your-oss.com/mask.png"
})
wait_task(final["data"][0]["task_id"])
⚠️ inpaint가 MODAL 상태에 진입하면 30분 이내에 반드시 /modal을 호출해야 합니다. 그렇지 않으면 백그라운드에서 자동으로 CANCEL + 환불됩니다.

video 과금 제어

  • 단일 클립: batch_size: 1 → 1 × midjourney@video 차감
  • 배치 4클립: batch_size: 4 → 4 × midjourney@video 차감
  • 고화질 단일 클립: video_type: "vid_1.1_i2v_720" + batch_size: 1 → 1 × midjourney@video-720p 차감
권장: 결과물이 1클립만 필요하면 batch_size=1을 사용하고, 비교용 배치일 때만 4를 사용하세요. 기본값으로 4를 켜지 마세요(비용이 N배로 증가).

동시성과 처리량

import asyncio
sem = asyncio.Semaphore(10)  # 클라이언트 최대 10개 동시 제출

async def submit_one(prompt):
    async with sem:
        return await submit({"prompt": prompt})
  • 플랫폼은 분당 제출 수에 상한이 있으며, 초과 시 429를 반환하므로 백오프 재시도가 필요합니다.
  • 실제 생성 동시성은 시스템 용량에 따라 결정되며, 초과 시 대기열에 들어갑니다. 작업이 오랫동안 SUBMITTED에 멈춰 있으면 보통 대기 중인 상태입니다.
  • 폴링 시 반드시 sleep을 넣고, sleep 없는 무한 루프를 돌리지 마세요.

모니터링 권장 사항

지표참고 임계값의미
작업 SUCCESS 비율(최근 1h)> 95%낮으면 서비스 / 네트워크 이상
평균 완료 소요 시간< 90s높으면 대기 중
MODAL 잔류 작업 수0에 근접많으면 클라이언트가 /modal을 호출하지 않은 것
code=24 비율< 5%높으면 prompt가 자주 금칙어를 유발하는 것

문제 해결 체크리스트

현상점검 방향
작업이 오랫동안 SUBMITTED시스템 대기 중, 잠시 후 다시 조회
작업이 오랫동안 NOT_START플랫폼이 잠시 후 자동으로 타임아웃 환불하므로 수동 처리 불필요
작업이 MODAL 30분 초과클라이언트가 /modal을 호출하지 않음, 이미 자동 CANCEL + 환불됨
prompt 필드가 비어 있음describe 작업의 텍스트 결과는 description 필드에 있음
image_urls가 한 장 부족콘텐츠 심사가 일부 이미지를 차단한 것, fail_reason 확인
과금이 예상보다 큼quota 필드 확인; video는 × batch_size를 잊지 말 것