이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Spring Boot API 서버와 Python AI worker를 왜 분리하는가?
  • 동기 LLM 호출과 비동기 job 처리는 어떤 기준으로 나누는가?
  • Java 서버와 Python worker 사이의 job 계약에는 무엇이 들어가야 하는가?
  • 인프라팀과 어떤 배포, 관측성, 장애 격리 포인트를 합의해야 하는가?

개요

백엔드 개발자가 AI 기능을 붙일 때 자주 만나는 선택지가 있다. 기존 Spring Boot 서버 안에서 직접 LLM API를 호출할 것인가, Python worker를 따로 둘 것인가.

정답은 하나가 아니다. 짧고 단순한 분류나 요약은 Spring adapter에서 직접 호출해도 된다. 긴 문서 처리, RAG 색인, eval batch, agent workflow는 Python worker로 분리하는 편이 자연스럽다.

왜 백엔드 개발자에게 중요한가

Spring Boot는 인증, 권한, 트랜잭션, API 계약, 운영 안정성에 강하다. Python은 AI SDK, 데이터 처리, RAG, eval, notebook 기반 실험 생태계가 강하다.

둘을 잘 나누면 장점이 합쳐진다.

  • Spring Boot: 사용자 요청, 인증/인가, job 상태, 결과 조회, quota 관리
  • Python worker: 모델 호출, RAG, embedding, eval, 문서 처리
  • Queue: 긴 작업 분리, 재시도, backpressure
  • DB/Object Storage: 상태와 결과 저장

동기와 비동기 판단 기준

동기 처리에 적합한 경우:

  • 3초에서 8초 안에 끝날 가능성이 높다.
  • 결과가 짧다.
  • 실패 시 사용자가 바로 재시도할 수 있다.
  • side effect가 없다.
  • API 서버 리소스 점유가 작다.

비동기 worker에 적합한 경우:

  • 처리 시간이 길거나 변동이 크다.
  • 파일, 문서, 대량 데이터가 들어간다.
  • 재시도와 상태 추적이 필요하다.
  • provider rate limit을 worker concurrency로 제어해야 한다.
  • 실패 후 DLQ와 재처리가 필요하다.

전체 구조

Client
  POST /ai/jobs
Spring Boot API
  인증/인가
  quota 확인
  job 저장
  queue publish
Python Worker
  job consume
  RAG/LLM 호출
  결과 저장
Spring Boot API
  GET /ai/jobs/{id}
  결과 조회

중요한 점은 Python worker가 사용자 권한을 임의로 판단하지 않게 하는 것이다. 권한 검증과 job 생성은 API 서버에서 하고, worker는 권한이 검증된 job만 처리한다. 다만 worker가 문서나 DB를 다시 조회해야 한다면 tenant id와 access scope를 job payload에 명확히 넣어야 한다.

Job 계약

job payload에는 다음이 들어간다.

  • job id
  • idempotency key
  • tenant id
  • user id 또는 actor id
  • 작업 유형
  • 입력 resource id
  • prompt version
  • model profile
  • trace id
  • callback 또는 result 저장 위치
  • 생성 시각과 만료 시각

payload에 민감한 원문을 모두 넣는 것은 피한다. queue message는 여러 시스템을 지나므로 resource id를 넣고 worker가 필요한 시점에 안전하게 조회하는 편이 나을 때가 많다.

코드로 이해하기

Spring Boot에서 job을 발행하는 예시다.

@Service
public class AiJobService {
 
    private final AiJobRepository jobRepository;
    private final AiJobPublisher jobPublisher;
    private final QuotaService quotaService;
 
    public AiJobService(
            AiJobRepository jobRepository,
            AiJobPublisher jobPublisher,
            QuotaService quotaService
    ) {
        this.jobRepository = jobRepository;
        this.jobPublisher = jobPublisher;
        this.quotaService = quotaService;
    }
 
    public AiJob enqueueRagIndex(User user, String documentId) {
        quotaService.checkCanRunIndexing(user.tenantId());
 
        AiJob job = AiJob.create(
                user.tenantId(),
                user.id(),
                "rag-index",
                documentId,
                UUID.randomUUID().toString()
        );
        jobRepository.save(job);
        jobPublisher.publish(AiJobMessage.from(job));
        return job;
    }
}

Python worker는 job 상태를 확인하고 처리한다.

def process_job(message, job_store, document_store, rag_indexer):
    job = job_store.get(message["job_id"])
    if job.status in {"completed", "cancelled"}:
        return
 
    if not job_store.mark_running_once(
        job_id=job.id,
        idempotency_key=job.idempotency_key,
    ):
        return
 
    try:
        document = document_store.load(
            tenant_id=job.tenant_id,
            document_id=job.resource_id,
        )
        result = rag_indexer.index(
            tenant_id=job.tenant_id,
            document=document,
            trace_id=job.trace_id,
        )
        job_store.complete(job.id, result.summary)
    except Exception as exc:
        job_store.fail(job.id, reason=type(exc).__name__)
        raise

핵심은 idempotency, tenant boundary, trace id다. 언어가 달라도 이 계약이 맞으면 운영이 쉬워진다.

장애 상황과 대응

장애 유형:

  • queue 적체
  • worker crash loop
  • provider rate limit
  • document store 조회 실패
  • job 중복 실행
  • 결과 저장 실패
  • Spring API와 worker의 schema version 불일치

대응:

  • queue depth와 job age 알림
  • worker release와 API release의 호환성 관리
  • DLQ 재처리 절차
  • idempotency key 기반 중복 방지
  • job schema version 추가
  • trace id로 API 요청과 worker 처리 연결

인프라 협업 포인트

인프라팀과는 다음을 합의한다.

  • worker autoscaling 기준
  • queue와 DLQ 운영
  • Spring 서버와 worker의 배포 순서
  • secret 분리
  • network egress와 provider endpoint
  • batch worker와 online worker 분리
  • GPU나 local model serving을 쓸 경우 resource scheduling

인프라팀의 고충은 “AI 작업이 예측하기 어렵다”는 데 있다. 입력 길이, 모델 latency, 재시도, batch 작업이 모두 변동성을 만든다. 백엔드 개발자는 작업 유형별 비용과 latency 추정치를 제공해야 한다.

개인 프로젝트 최소 기준

  • 처음에는 Spring에서 직접 호출하되 timeout과 usage log를 둔다.
  • 긴 작업이 생기면 간단한 queue나 background worker로 분리한다.
  • job status를 DB에 저장한다.
  • worker 재시작 후에도 job 상태가 복구되게 한다.

기업 운영 수준 기준

  • API 서버와 worker를 독립 배포한다.
  • job schema version과 backward compatibility를 관리한다.
  • queue depth, job age, DLQ, worker error rate를 모니터링한다.
  • tenant별 concurrency와 quota를 둔다.
  • batch와 online workload를 분리한다.

주니어 관점

주니어는 먼저 Controller에서 모든 것을 처리하지 않는 감각을 익혀야 한다. AI 호출은 느릴 수 있고, 실패할 수 있고, 비용이 든다. 따라서 service, client, job, worker라는 경계를 이해하는 것이 중요하다.

시니어 관점

시니어는 런타임 선택보다 운영 경계를 봐야 한다. Java냐 Python이냐보다 job 계약, idempotency, 배포 호환성, 관측성, quota, 장애 격리가 더 중요하다. AI 기능이 기존 백엔드 SLO를 망치지 않게 격리해야 한다.

실전 팁

  • AI worker가 DB를 직접 쓰더라도 권한과 tenant boundary를 명확히 전달한다.
  • queue message에는 원문보다 resource id를 넣는 편이 안전한 경우가 많다.
  • worker concurrency는 provider rate limit보다 낮게 시작한다.
  • API와 worker가 공유하는 payload schema는 문서화하고 version을 둔다.

위험 신호!

  • API 서버 thread가 긴 AI 작업을 계속 붙잡고 있다.
  • worker job에 idempotency key가 없다.
  • Spring과 Python 사이 payload schema가 문서화되어 있지 않다.
  • worker가 사용자 권한을 우회해 문서를 직접 읽는다.
  • batch 작업과 고객-facing 작업이 같은 worker pool을 쓴다.

확인 질문

확인 질문

  • Spring Boot와 Python worker를 함께 쓰는 이유는 무엇인가?
    • Spring의 백엔드 운영 강점과 Python의 AI 생태계 강점을 함께 활용하고, 긴 AI 작업을 API 서버에서 격리하기 위해서다.
  • job payload에 trace id가 필요한 이유는 무엇인가?
    • 사용자 요청, queue 메시지, worker 처리, provider 호출, 결과 저장을 하나의 흐름으로 추적하기 위해서다.
  • worker 분리에서 가장 중요한 안정성 장치는 무엇인가?
    • idempotency, 상태 저장, DLQ, schema version, concurrency 제한이다.

Spring AI 적용 연결

참고 문서