|

LLM 기능을 서비스에 붙일 때 도메인 로직과 AI 호출은 어떻게 분리해야 할까

LLM 기능 글 대표 이미지
LLM 기능을 기존 서비스에 붙일 때 application service, AI client, prompt template, fallback, audit log, retry 경계를 어떻게 나눌지 실무 구조 중심으로 설명합니다.

LLM 기능을 기존 서비스에 붙일 때 가장 위험한 실수는 AI 호출을 도메인 로직 안에 바로 섞어 넣는 것입니다. 처음에는 빠르지만, 테스트와 장애 대응, 비용 통제가 어려워집니다.

핵심은 도메인 규칙은 결정 가능한 코드로 유지하고, 불확실한 AI 호출은 명확한 adapter 경계 뒤에 두는 것입니다. 그래야 모델 교체, prompt 수정, 실패 대응을 서비스 전체에 퍼뜨리지 않을 수 있습니다.

LLM 기능 서비스 경계 설계 카드
LLM 기능은 도메인 로직과 AI client 경계를 나누고 fallback과 audit log를 함께 설계해야 한다.

LLM 기능과 도메인 로직을 섞으면 생기는 문제

도메인 로직은 보통 입력이 같으면 결과도 같아야 합니다. 반면 LLM 호출은 모델 버전, prompt, temperature, context에 따라 결과가 달라질 수 있습니다. 둘을 같은 함수 안에 섞으면 테스트가 흔들립니다.

  • 단위 테스트가 외부 API와 모델 응답에 의존한다
  • 비용과 latency가 도메인 service 내부에 숨어버린다
  • prompt 변경이 업무 규칙 변경처럼 퍼진다
  • 장애 시 fallback 위치가 애매해진다

AI client를 별도 경계로 둔다

LLM 호출은 AI client나 adapter로 감싸는 편이 좋습니다. application service는 AI client의 인터페이스만 알고, 실제 모델 제공자와 prompt 구성은 내부 구현으로 숨깁니다.

interface SummaryAiClient {
    fun summarize(request: SummaryRequest): SummaryResult
}

class ArticleService(
    private val aiClient: SummaryAiClient
) {
    fun createSummary(articleId: ArticleId): SummaryResult {
        val article = loadArticle(articleId)
        return aiClient.summarize(article.toSummaryRequest())
    }
}

prompt template은 코드 상수보다 정책에 가깝다

prompt는 단순 문자열이 아닙니다. 출력 형식, 금지 문장, tone, domain context, 안전 조건이 들어가는 정책입니다. 그래서 파일, 버전, 테스트 케이스로 관리할수록 운영이 쉬워집니다.

출력 schema를 먼저 정한다

LLM 결과를 바로 화면이나 DB에 넣으면 위험합니다. 먼저 schema를 정하고, 모델 출력이 그 schema를 만족하는지 검증해야 합니다. 구조화된 출력이나 JSON schema를 쓰면 후속 코드가 훨씬 안정적입니다.

{
  "summary": "string",
  "risk_flags": ["string"],
  "confidence": "low | medium | high"
}

fallback과 retry는 도메인 바깥에서 설계한다

LLM 호출은 실패할 수 있습니다. rate limit, timeout, 모델 오류, 안전 정책 거부가 모두 가능합니다. 이때 도메인 규칙이 실패하는 것이 아니라 외부 AI 의존성이 실패한 것으로 다뤄야 합니다.

  • timeout이면 기본 템플릿을 보여준다
  • schema 검증 실패면 재시도하거나 수동 검토로 보낸다
  • 위험 플래그가 있으면 publish를 막는다
  • 모델 오류는 사용자 행동과 분리해 로깅한다

audit log는 나중에 붙이면 늦다

AI 기능은 어떤 입력으로 어떤 prompt를 만들었고, 어떤 모델이 어떤 출력을 냈는지 추적할 수 있어야 합니다. 특히 사용자 데이터나 publish 같은 중요한 작업과 연결된다면 audit log는 필수에 가깝습니다.

application service의 역할

application service는 업무 흐름을 조율합니다. 데이터를 읽고, AI client를 호출하고, 결과를 검증하고, 필요한 경우 도메인 객체에 반영합니다. 하지만 prompt 세부 내용이나 vendor SDK 호출 코드를 직접 품지 않는 편이 좋습니다.

실무 체크리스트

  1. LLM이 결정해도 되는 영역과 코드가 결정해야 하는 영역을 나눈다
  2. AI client interface를 만들고 provider 구현을 숨긴다
  3. prompt template과 출력 schema를 버전 관리한다
  4. timeout, retry, fallback, human review 경로를 둔다
  5. 입력, prompt version, model, output, 검증 결과를 audit log로 남긴다

정리

LLM 기능은 기존 서비스에 강력한 기능을 더할 수 있지만, 도메인 로직 안에 직접 섞이면 운영이 어려워집니다. AI 호출은 외부 의존성으로 보고 adapter, schema, fallback, audit log 경계를 먼저 잡는 편이 안전합니다.

구조화된 출력 기준은 OpenAI Structured Outputs 문서를 참고할 수 있습니다. 에이전트 운영 예시는 AI 에이전트 설계 예제 글과 함께 보면 좋습니다.


보강: LLM 기능을 하나의 use case로 감싸기

LLM 기능을 붙일 때는 먼저 사용 사례를 좁혀야 합니다. 예를 들어 ‘고객 문의 답변 생성’이라고만 쓰면 너무 큽니다. 실제로는 문의 분류, 관련 문서 검색, 답변 초안 생성, 금칙어 검사, 상담원 승인처럼 여러 단계로 나눠야 합니다.

CustomerQuestion
  -> classify intent
  -> retrieve knowledge
  -> draft answer
  -> validate policy
  -> human approval
  -> send or store

이렇게 나누면 LLM이 맡는 단계와 일반 코드가 맡는 단계가 분명해집니다. 특히 승인과 저장은 모델이 아니라 application service가 결정하게 두는 편이 안전합니다.

도메인 객체에 LLM 응답을 바로 넣지 않는다

LLM 출력은 검증 전에는 외부 입력과 비슷하게 다뤄야 합니다. 사용자가 입력한 문자열을 바로 DB에 넣지 않듯, 모델이 만든 결과도 schema 검증과 정책 검사를 거친 뒤 반영해야 합니다.

data class AiDraft(
    val body: String,
    val riskFlags: List<String>,
    val confidence: Confidence
)

fun approveDraft(draft: AiDraft): ApprovedAnswer {
    require(draft.riskFlags.isEmpty()) {
        "Risky AI output requires human review"
    }
    return ApprovedAnswer(draft.body)
}

비용과 latency를 application service에서 보이게 한다

AI 호출은 일반 함수 호출처럼 보여도 실제로는 외부 네트워크, 모델 비용, rate limit을 포함합니다. 그래서 service 경계에서 호출 횟수, timeout, token 사용량, 실패율을 볼 수 있어야 합니다.

  • 요청당 AI 호출 횟수
  • 평균/상위 latency
  • token 사용량 또는 추정 비용
  • schema validation 실패율
  • fallback 발생률

fallback은 기능마다 다르게 설계한다

LLM 실패 시 모든 기능이 같은 fallback을 가져야 하는 것은 아닙니다. 검색 보조 기능은 결과 없이 진행할 수 있지만, 자동 발행이나 고객 안내처럼 위험한 기능은 중단하고 사람 검토로 보내야 합니다.

  1. 없어도 되는 보조 기능인지 확인한다
  2. 기본 템플릿으로 대체 가능한지 본다
  3. 이전 성공 결과를 캐시해도 되는지 검토한다
  4. 사람 검토 queue로 넘겨야 하는지 정한다
  5. 사용자에게 실패를 어떻게 설명할지 정한다

prompt 변경을 배포처럼 다루기

prompt 한 줄 변경이 서비스 결과를 크게 바꿀 수 있습니다. 그래서 중요한 LLM 기능에서는 prompt 변경도 코드 배포처럼 리뷰, 테스트, rollback 경로를 가져야 합니다.

  • prompt version을 명시한다
  • 변경 전후 샘플 질문 결과를 비교한다
  • 금지 사례 regression set을 둔다
  • 운영 로그에서 version별 품질을 본다
  • 문제가 생기면 이전 prompt로 되돌릴 수 있게 한다

권한과 개인정보 경계

LLM에 보내는 context에는 개인정보나 내부 문서가 섞일 수 있습니다. 도메인 service가 이미 권한을 확인했다고 해도, AI context packing 단계에서 다시 최소 정보 원칙을 적용해야 합니다.

  • 사용자가 볼 수 있는 문서만 retrieval 후보에 넣는다
  • 필요 없는 개인정보는 prompt에서 제거한다
  • 민감 필드는 마스킹하거나 요약한다
  • 외부 모델 전송 가능 데이터와 불가능 데이터를 정책으로 나눈다

함께보면 좋은 글