포스트

LLM Agent in DDD

LLM Agent in DDD

이전 글인 Spring AI Tool Calling 설계: Tool과 DDD 계층의 책임 분리에서는 Tool을 LLM 요청을 Application UseCase로 변환하는 Adapter로 정리했다. 이 결론은 개별 Tool Wrapper와 Application UseCase의 경계를 설명하는 데 여전히 유효하다. 이번에는 질문의 범위를 Tool 하나에서 여러 Tool을 선택하고 반복하는 Agent 전체의 제어 흐름으로 넓혀 보았다.

Agent가 검색 결과를 읽고, 다음 Tool을 고르고, 필요하면 다시 검색하는 구조로 커지면서 다른 문제가 보이기 시작했다. Agent Runtime을 Infrastructure에 두면 Application에서 시작한 흐름이 Infrastructure를 거쳐 다시 Application으로 들어왔다. 반대로 Agent Runtime을 Application에 두면 Prompt, Tool Schema와 모델 라우팅 같은 AI 구현이 일반 UseCase와 섞였다.

더 이상 @Tool 클래스를 어느 패키지에 둘 것인가가 아니라, 모델이 다음 실행을 선택하는 흐름을 어느 계층이 소유해야 하는가를 고려해야 했따.

결국 Tool Calling Agent 요청에 한해서 Presentation과 Application 사이에 Intelligence라는 오케스트레이션 계층을 추가했다. 이 글에서는 기존 DDD 계층에 Agent를 넣으며 생긴 문제와, DDD를 조금 비틀어 별도 계층을 선택한 기준을 정리한다.


I. 단발성 LLM 호출 기능 구현

1. Application 계층의 역할

요약·분류·번역·OCR처럼 입력 하나를 모델에 전달하고 결과 하나를 받는 기능을 생각해 보자. Application은 언제 모델을 호출할지 결정하고, 반환된 결과를 다음 비즈니스 흐름에 사용한다.

아래 그림은 단발 LLM 기능의 런타임 호출 흐름이다.

flowchart LR
    P[Presentation] --> A[Application UseCase]
    A --> PORT[LLM Port]
    PORT -->|runtime dispatch| INFRA[Infrastructure<br/>LLM Adapter]
    INFRA --> MODEL[Model API]
    A --> D[Domain]

이 구조에서 모델은 계산을 수행하는 외부 시스템이다. Application이 작업의 목적과 실행 순서를 알고 있으며, Infrastructure Adapter는 모델 SDK와 외부 API 호출을 감춘다.

1
2
3
4
5
6
7
8
9
10
Application이
  ├─ 언제 호출할지
  ├─ 무엇을 입력할지
  ├─ 결과를 어디에 사용할지
  └─ 실패하면 다음 상태를 어떻게 처리할지 결정한다

LLM Adapter가
  ├─ 모델 SDK
  ├─ 요청·응답 변환
  └─ 외부 API 통신을 담당한다.

Port 계약은 Application이 소유하고 Infrastructure가 이를 구현한다. 기존 Port와 Adapter만으로 책임이 자연스럽게 나뉜다. 때문에 LLM을 사용한다는 이유만으로 새로운 계층이 필요하지 않았다.

2. 단발성 호출에서 Tool의 위치

Tool은 모델이 생성한 이름과 JSON 인자를 Application Command로 변환한다. HTTP Controller가 HTTP 요청을 UseCase 호출로 바꾸는 것과 비슷하다.

1
2
HTTP Client  → Controller   → Application UseCase
LLM          → Tool Wrapper → Application UseCase

Tool Wrapper가 모델에 노출할 Tool 이름과 JSON Schema를 가지고, 실제 업무 규칙은 Application과 Domain에 남긴다. 여기까지는 Tool을 LLM 전용 Inbound Adapter로 보면 충분했다.

문제는 Tool 하나가 아니라, Tool을 선택하고 반복 실행하도록 Agent를 구현할 때 시작됐다.


II. Tool Calling 적용으로 인한 계층 변화

1. 모델 호출 한 번으로 끝나지 않는다

Tool Calling Agent는 단순히 모델에 값을 요청하고 결과를 반환하지 않는다. 모델은 현재 대화와 Tool 결과를 보고 다음 행동을 선택한다.

이 글에서는 Agent가 없어도 독립적으로 성립하는 Application UseCase 또는 Port를 Capability라고 부른다.

sequenceDiagram
    actor User
    participant Agent as Agent Runtime
    participant LLM
    participant Tool as Tool Wrapper
    participant App as Application Capability

    User->>Agent: 요청
    Agent->>LLM: Prompt + 사용 가능한 Tool
    LLM-->>Agent: Tool A 호출
    Agent->>Tool: 이름 + JSON 인자
    Tool->>App: Command
    App-->>Tool: Result
    Tool-->>Agent: 모델용 결과
    Agent->>LLM: Tool 결과 전달
    LLM-->>Agent: Tool B 호출 또는 최종 답변
    Note over Agent,LLM: 필요한 동안 반복
    Agent-->>User: 최종 답변

실행 횟수와 순서는 코드에 완전히 고정되어 있지 않다. 모델은 검색 결과가 부족하면 다른 검색어를 만들고, 상세 문맥이 필요하면 추가 Tool을 호출하며, 정보가 부족하면 사용자에게 Clarification을 요청할 수 있다.

LLM이 비즈니스 권한이나 상태 전이의 주체가 된 것은 아니지만, 여러 Capability 중 다음에 무엇을 실행할지는 모델과 Agent Runtime이 결정한다.

2. Agent Runtime은 단순한 Outbound Adapter가 아니다.

단발 호출에서는 Application이 LLM을 호출하고 결과를 받는다. Agent에서는 모델 결과가 다시 Application Capability를 호출하고, 그 결과가 또 모델의 다음 판단으로 이어진다.

1
2
3
4
5
6
7
단발 LLM
Application → LLM → Result

Tool Calling Agent
Agent → LLM → Tool → Application Capability
      ↑                           ↓
      └──────── Tool Result ──────┘

Agent 구성은 모델 호출을 위한 Driven Adapter와 Tool을 통해 Application을 구동하는 Driving Adapter를 함께 조율한다. 이 양방향 성격 때문에 기존 네 계층 중 어디에 놓아도 한쪽 책임이 어색해졌다.


III. 기존 계층에 Agent를 넣어보기

1. Infrastructure가 Agent Loop를 소유하는 경우

가장 먼저 생각할 수 있는 위치는 Infrastructure다. LangGraph나 Agent SDK는 외부 라이브러리이고, 모델 API도 외부 시스템이므로 자연스러워 보인다.

아래는 이 배치의 런타임 호출 흐름이다.

flowchart LR
    P[Presentation] --> CHAT[Application<br/>Chat UseCase]
    CHAT --> RUNTIME[Infrastructure<br/>Agent Runtime]
    RUNTIME <--> LLM[LLM]
    RUNTIME --> TOOL[Infrastructure<br/>Tool Wrapper]
    TOOL --> CAP[Application<br/>Capability]
    CAP --> D[Domain]

정적 import만 보면 의존성 역전 원칙을 지킬 수 있다. Application은 Agent Port만 알고, Infrastructure가 Port를 구현하면 된다.

1
2
3
4
소스 의존성
Application Chat UseCase → Application Agent Port
Infrastructure Agent Runtime ──implements──→ Application Agent Port
Infrastructure Tool Wrapper → Application Capability

따라서 이 구조를 순환 의존성이라고 부르는 것은 정확하지 않다. 문제는 소스 의존성이 아니라 런타임 제어 흐름이다.

1
2
3
4
Application Chat UseCase
  → Infrastructure Agent Runtime
      → Infrastructure Tool Wrapper
          → Application Capability

이 배치에서는 Infrastructure가 외부 기술 구현을 넘어 Agent UseCase의 실행 순서까지 소유한다. 어떤 Tool을 노출하고, 언제 다시 모델을 호출하며, 언제 답변을 끝낼지 결정한다.

Application만 읽어서는 전체 UseCase의 실행 흐름이 보이지 않는다. 핵심 오케스트레이션은 Infrastructure 안에 숨어 있고, Infrastructure가 다시 Application의 여러 기능을 구동한다.

Hexagonal Architecture 관점에서는 Agent를 Driving Adapter로 해석할 수도 있다. 그 해석이 틀린 것은 아니지만, Agent 오케스트레이션과 모델·Tool Adapter의 책임이 한 모듈에 함께 모이면서 크기가 빠르게 커지는 것이 불편했다.

2. Application이 Agent Loop를 소유하는 경우

제어 흐름을 Application에 드러내는 방법도 있다. Application이 Framework-native Agent Loop를 직접 실행하고 Tool을 조합하면 전체 UseCase가 한 곳에서 보인다.

1
2
3
4
5
6
Application
  ├─ Agent UseCase
  ├─ Graph와 실행 Loop
  ├─ Prompt 조합
  ├─ Tool Schema
  └─ Model Routing

이번에는 반대 문제가 생긴다. 이 구조에서는 Application이 업무 UseCase뿐 아니라 Agent Framework의 Graph·Message·Tool 타입을 알아야 한다.

Prompt 문구나 JSON Schema, 모델별 라우팅과 Tool Call 반복 방식은 업무 규칙과 변경 이유가 다르다. 그럼에도 같은 계층에 놓이면 일반 Application Service와 AI 오케스트레이션의 경계가 흐려진다.

Port를 추가하더라도 Port의 입력과 출력이 특정 Framework의 Message나 Tool 타입이라면 분리는 이름뿐이다. Application까지 AI Framework 교체의 영향을 받는다.

3. Application이 Tool 요청을 반환하는 경우

Application이 다음 Tool 요청을 값으로 반환하고, 바깥쪽 Adapter가 실행한 뒤 Application을 다시 호출하는 구조도 검토할 수 있다.

1
2
3
Application → ToolRequest 반환
Adapter     → Tool 실행
Application → ToolResult로 재개

겉으로는 실행 주도권을 Application에 남길 수 있다. 하지만 실제로 적용하면 pause, resume, 병렬 Tool Call, 재시도와 중간 상태 저장 규칙을 직접 설계해야 한다.

Agent Framework가 이미 제공하는 실행 Loop를 다시 구현하는 셈이다. 명시적인 사람 승인이나 장시간 중단이 필요한 흐름이라면 의미가 있지만, 모든 Tool Call에 적용하기에는 복잡도가 컸다.


IV. 계층을 나누는 기준을 바꿨다

1. LLM을 사용하는지가 기준은 아니다

처음에는 “LLM 코드는 Infrastructure에 둔다”는 규칙으로 접근했다. 하지만 같은 LLM 호출이라도 시스템에서 맡는 역할은 달랐다.

기능실행 흐름의 소유자권장 위치
요약·분류·번역·OCRApplication이 입력과 다음 단계를 결정Application Port + Infrastructure 모델 Adapter
순서가 고정된 업무 WorkflowApplication ServiceApplication
모델이 여러 Capability를 선택·반복Agent RuntimeIntelligence 후보
Tool을 통한 실제 상태 변경Application과 DomainApplication Capability

단발인지 다단계인지도 절대적인 기준은 아니다. 코드로 정해진 세 단계를 실행한다고 해서 반드시 Intelligence가 필요한 것은 아니다.

중요한 질문은 다음 두 가지였다.

  1. 다음 실행을 누가 선택하는가?
  2. 그 선택은 업무 규칙인가, 모델 주도 오케스트레이션인가?

Application이 LLM을 계산 도구로 호출한다면, 모델 SDK와 호출 구현은 Infrastructure Adapter에 둘 수 있다. 반면 LLM이 여러 Application Capability의 실행 순서를 선택하는 순간, 전체 Agent를 단순한 Outbound Adapter로만 설명하기 어려워진다.

2. AI 코드와 업무 코드를 분리하는 기준

Prompt를 사용한다고 모든 코드를 Intelligence에 배치하지는 않았다. 모델을 사용하더라도 Application이 흐름을 소유하는 단발 변환은 계속 Infrastructure Adapter로 남겼다.

반대로 다음 항목은 Intelligence의 변경 이유로 분류했다.

  • 대화와 요청에 따른 Tool 선택
  • 모델 라우팅
  • Prompt 조합
  • Tool Schema와 결과 축약
  • Tool Call 반복과 종료 조건
  • 추가 문맥의 필요 여부를 판단하고 Clarification Turn 구성
  • 인용·모델 출력 정규화처럼 업무 규칙이 아닌 후처리

업무 Capability, 권한 검증, 트랜잭션과 Aggregate의 불변식은 여전히 Application과 Domain의 변경 이유다.


V. Agent 경로에만 둔 Intelligence 경계

1. 일반 요청과 Agent 요청을 분리했다

Intelligence는 모든 요청이 통과하는 새로운 표준 계층이 아니다. 모델 주도의 Tool Calling이 필요한 Agent 요청에만 적용한다.

아래 그림은 런타임 호출 순서가 아니라 소스 코드의 의존 방향을 나타낸다.

flowchart TB
    P[Presentation] --> INTEL[Intelligence]
    INTEL --> APP[Application]
    P --> APP
    APP --> DOMAIN[Domain]
    APP --> PORT[Application-owned Port]
    INFRA[Infrastructure] -. implements · imports .-> PORT

소스 의존성은 다음 방향을 유지한다.

1
2
3
4
5
Presentation → Intelligence → Application → Domain
Presentation ───────────────→ Application → Domain

Infrastructure → Application이 정의한 Port
Composition Root → 모든 구체 구현을 조립

Intelligence는 호출 경로상 Application보다 앞에 있지만 비즈니스 규칙의 상위 계층이라는 뜻은 아니다. Agent 요청을 받아 여러 Application Capability를 조율하는 AI 전용 Driving Orchestration Layer다.

Intelligence라는 이름은 Azure AI Workload Architecture가 라우팅, Agent와 오케스트레이션을 별도 관심사로 구분한 데서 힌트를 얻었다. 다만 Azure의 논리 계층을 DDD 패키지 구조에 일대일로 옮긴 것은 아니며, 여기서는 Tool Calling Agent를 위한 경계로 범위를 더 좁혔다.

2. Agent 내부의 실행 흐름

Intelligence 내부에서는 Graph·Message·Tool처럼 Agent Framework의 실행 타입을 사용할 수 있다. 대신 Provider SDK와 HTTP Client는 Infrastructure에 남기고, Agent Framework 타입도 Application과 Domain으로 넘어가지 않도록 경계를 닫는다.

런타임에서는 II장의 Sequence처럼 Agent와 LLM, Tool과 Capability 사이를 반복해서 오간다. 필요하면 Application Port를 통해 Infrastructure Adapter와 외부 시스템도 호출한다.

하지만 런타임 호출 방향과 소스 의존성은 다르다. Agent가 외부 시스템을 호출한다고 해서 Intelligence가 해당 Infrastructure 구현을 직접 import해서는 안 된다. Presentation → Intelligence → Application → Domain은 소스 의존성과 상위 제어 흐름을 표현하며, Agent 내부 실행까지 단방향이라는 뜻은 아니다.


VI. Tool과 Capability를 분리한다

1. Tool Wrapper는 LLM 프로토콜을 번역한다

Intelligence의 Tool은 실제 업무 기능이 아니다. 모델이 이해하는 계약을 Application이 이해하는 Command와 Result로 변환하는 Wrapper다.

Tool WrapperApplication Capability
Tool 이름과 설명업무 행위와 UseCase
JSON SchemaCommand와 Result
요청 범위 컨텍스트 바인딩권한과 소유권 검증
모델에 줄 결과 형태와 크기 조절업무 규칙과 불변식
Tool 오류의 안전한 표현트랜잭션과 상태 변경

이 글은 특정 Framework나 언어보다 경계 자체를 다루므로, 예시는 Python 형태로 단순화했다.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from dataclasses import dataclass
from typing import Protocol


@dataclass(frozen=True)
class SearchDocumentsCommand:
    actor_id: str
    query: str


@dataclass(frozen=True)
class SearchDocumentsResult:
    documents: tuple[str, ...]


class SearchDocuments(Protocol):
    async def execute(
        self,
        command: SearchDocumentsCommand,
    ) -> SearchDocumentsResult:
        ...


class SearchDocumentsTool:
    """Intelligence: LLM 계약을 Application 계약으로 변환한다."""

    def __init__(self, search_documents: SearchDocuments, actor_id: str):
        self._search_documents = search_documents
        self._actor_id = actor_id  # 모델이 아니라 서버가 바인딩한다.

    async def __call__(self, query: str) -> dict[str, object]:
        result = await self._search_documents.execute(
            SearchDocumentsCommand(
                actor_id=self._actor_id,
                query=query,
            )
        )

        return {
            "documents": list(result.documents[:5]),
            "truncated": len(result.documents) > 5,
        }

Tool Wrapper는 actor_id를 모델 인자로 받지 않는다. 인증이 끝난 요청 컨텍스트에서 서버가 바인딩한다.

하지만 이 바인딩만으로 권한 검증이 끝나는 것은 아니다. 해당 Capability를 소유한 신뢰 경계인 로컬 Application 또는 대상 시스템이 실제 접근 권한을 다시 검증해야 한다.

2. Repository와 Aggregate는 Intelligence에서 다루지 않는다

Intelligence가 Application을 건너뛰고 Repository를 직접 사용하면 Tool Wrapper가 곧 업무 구현이 된다. 이 구조는 피했다.

1
2
3
4
5
6
7
8
Intelligence에서 허용
  Tool Schema · Prompt · Model Routing · 컨텍스트 변환
  Application Command 생성 · Result 축약

Intelligence에서 금지
  Repository · DAO · ORM · Unit of Work 직접 접근
  Aggregate 상태 변경 · 업무 권한 최종 판단
  Infrastructure Adapter 구체 구현 import

같은 Bounded Context의 기능은 내부 HTTP API를 새로 만들지 않고 Application Capability를 직접 호출한다. 다른 Bounded Context나 외부 시스템의 기능은 Application Port 뒤에 Infrastructure Adapter를 둔다.

Tool이 어떤 경로로 호출되더라도 업무 기능은 하나만 유지한다. REST와 Agent가 서로 다른 비즈니스 로직을 가지지 않게 하는 것이 핵심이다.


VII. 트랜잭션은 Agent Loop와 분리한다

Tool Calling은 모델과 외부 API의 응답을 여러 번 기다릴 수 있다. Agent 전체를 하나의 트랜잭션으로 묶으면 LLM 응답을 기다리는 동안 DB Connection과 Lock을 오래 점유할 수 있다.

sequenceDiagram
    participant Intel as Intelligence
    participant App as Application
    participant DB
    participant LLM

    Intel->>App: 실행 준비 요청
    App->>DB: 필요한 상태 조회·저장
    DB-->>App: Commit
    App-->>Intel: 실행 입력

    Note over Intel,LLM: LLM·외부 I/O를 기다리는 동안 열린 Transaction 없음
    Intel->>LLM: Agent 실행
    LLM-->>Intel: Tool Call

    Intel->>App: Capability 호출
    App->>DB: 필요한 조회·상태 변경
    DB-->>App: 짧은 Unit of Work Commit
    App-->>Intel: Tool Result
    Intel->>LLM: Tool Result 전달
    LLM-->>Intel: 다음 Tool Call 또는 최종 결과

    Intel->>App: 결과 반영 요청
    App->>DB: 상태 변경·저장
    DB-->>App: Commit

Application은 실행 전후의 상태 전이를 짧은 Unit of Work로 처리한다. Intelligence는 DB Session을 들고 모델이나 외부 API를 기다리지 않는다.

상태 변경 Tool도 각 Application Capability가 자신의 트랜잭션을 가진다. 하나의 Agent 요청에서 같은 Tool이 여러 번 호출될 수 있으므로, 쓰기 Tool에는 Idempotency, 재시도, 감사와 사람 승인 여부도 별도로 정의해야 한다.


VIII. DDD를 비튼 것인가

1. DDD는 네 개의 폴더 이름이 아니다

DDD와 함께 자주 사용하는 계층형 구조에는 Presentation, Application, Domain, Infrastructure가 있다. 하지만 DDD의 목적이 반드시 이 네 개의 폴더만 유지하는 것은 아니다.

정확히 말하면 이 네 계층은 DDD 자체가 규정한 고정 구조라기보다, Domain Model을 외부 기술에서 보호하기 위해 함께 사용한 Layered·Hexagonal Architecture의 구조다. 여기서 DDD를 비틀었다는 표현은 DDD의 원칙을 바꿨다는 뜻이 아니라, 프로젝트에서 사용하던 계층 구조를 확장했다는 뜻이다.

Domain의 언어와 규칙을 중심에 두고, 외부 기술이 그 모델을 침범하지 않도록 의존성을 관리하는 것이 더 중요하다. Hexagonal Architecture 역시 Domain과 Application Core를 Port와 Adapter로 외부 기술에서 분리하는 데 초점을 둔다.

이번 구조는 Domain과 Application의 책임을 줄이거나 LLM에 넘기지 않았다. 오히려 Infrastructure 안에 숨어 있던 모델 주도 오케스트레이션을 별도 이름으로 드러냈다.

1
2
3
4
5
6
7
변하지 않은 것
  Domain       → 업무 규칙과 불변식
  Application  → UseCase, 권한, 상태 전이와 트랜잭션
  Infrastructure → 외부 시스템과 기술 구현

추가한 것
  Intelligence → 모델 주도의 Agent 실행과 Tool 조율

그래서 “DDD를 깨뜨렸다”기보다 기존 네 계층만으로 설명하기 어려운 제어 흐름을 한 칸 확장했다고 보는 편이 맞다.

2. 프로젝트 안에서만 유효한 경계다

Intelligence는 새 Bounded Context가 아니다. 모든 AI 기능을 모아두는 공통 폴더도 아니며, 다른 프로젝트가 그대로 따라야 하는 표준 계층도 아니다.

Agent를 Infrastructure의 Driving Adapter로 두는 구조도 충분히 가능하다. Application에 프레임워크 중립적인 Workflow Engine을 두는 선택도 가능하다.

이번 선택은 다음 조건이 동시에 존재했기 때문에 의미가 있었다.

  • 모델이 여러 Application Capability를 반복해서 선택한다.
  • Agent의 Prompt, Tool과 라우팅 정책이 독립적으로 자주 변경된다.
  • 일반 Application Service에서 Agent Framework 타입을 제거하고 싶다.
  • 전체 오케스트레이션이 Infrastructure 내부에 숨지 않도록 드러내고 싶다.

이 조건이 없다면 새로운 계층은 불필요한 복잡도일 가능성이 높다.


IX. 추가한 만큼 비용도 생긴다

1. 만능 AI 계층이 될 수 있다

이름이 넓기 때문에 AI와 관련된 코드를 모두 Intelligence에 넣기 쉽다. 이를 막기 위해 적용 범위를 Tool Calling Agent로 제한했다.

단발 LLM Adapter와 모델 SDK는 계속 Infrastructure에 두고, 결정론적인 Workflow와 업무 규칙은 Application과 Domain에 남긴다. 모델이 Tool을 선택하는 Agent Workflow만 Intelligence의 대상이다.

2. Application 계약이 Agent 중심으로 오염될 수 있다

Agent가 필요하다는 이유로 SearchForAgent, AgentResult 같은 계약을 계속 만들면 Application이 다시 Agent의 언어를 알게 된다.

Port와 Result는 가능하면 Agent가 없어도 성립하는 업무 Capability의 언어로 정의한다. 반대로 Tool 이름, Tool 설명과 모델 전용 Metadata는 Intelligence 안에 남긴다.

3. 조립과 테스트 대상이 늘어난다

계층 하나를 추가하면 Composition Root의 연결과 경계 테스트도 늘어난다. 특히 다음 규칙은 정적 의존성 검사로 확인하는 편이 좋다.

  • Application과 Domain은 Intelligence나 Agent Framework를 import하지 않는다.
  • Intelligence는 Infrastructure, Repository와 ORM 구현을 import하지 않는다.

나머지 실행 경계는 동작 테스트로 확인한다.

  • Tool Wrapper는 Application Capability를 통해 업무 기능을 호출한다.
  • 일반 LLM Adapter는 Intelligence를 거치지 않는다.
  • 모델과 외부 I/O를 기다리는 동안 DB Transaction을 유지하지 않는다.

새 계층은 복잡도를 없애지 않는다. 여러 곳에 흩어질 복잡도를 하나의 명시적인 경계 안으로 모은다.


X. 결과

처음에는 LLM도 다른 외부 시스템처럼 Port와 Adapter 뒤에 두면 충분하다고 생각했다. 단발 호출과 단순한 Tool Calling에서는 실제로 충분했다.

Agent가 여러 Capability를 선택하고 반복 실행하면서 상황이 달라졌다. Infrastructure에 Agent를 두면 핵심 오케스트레이션이 Adapter 내부에 숨었고, Application에 두면 AI Framework와 업무 UseCase가 섞였다.

최종적으로 Agent 요청에 한해서 다음 구조를 선택했다.

1
2
3
4
5
일반 요청
Presentation → Application → Domain

Agent 요청
Presentation → Intelligence → Application → Domain

Intelligence는 Application을 대체하지 않는다. 권한, 업무 규칙, 상태 변경과 트랜잭션은 계속 Application과 Domain이 소유한다.

Tool 역시 업무 기능 그 자체가 아니다. 모델의 요청을 기존 Application Capability로 변환하는 경계일 뿐이다.

결국 LLM 기능의 위치를 정할 때는 “LLM을 사용하는가”보다 다음 질문이 먼저다.

LLM은 Application이 호출하는 계산 도구인가? 아니면 LLM의 선택을 중심으로 Agent Runtime이 여러 Application Capability를 조율하는가?

후자라면 기존 계층 안에 억지로 숨기는 것보다, 제한된 책임과 명확한 의존 규칙을 가진 경계로 드러내는 것이 나을 수 있다. 이번 Intelligence 계층은 DDD를 포기한 결과가 아니라, 실제 제어 흐름을 구조에 조금 더 솔직하게 표현하기 위한 선택이었다.


XI. 참고

  1. Spring AI Tool Calling 설계: Tool과 DDD 계층의 책임 분리
  2. Microsoft Azure Well-Architected Framework - Application design for AI workloads
  3. LangGraph - Workflows and agents
  4. AWS Prescriptive Guidance - Building hexagonal architectures on AWS
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.