Issue 01중국 AI
AC POST
중국 AI 목록
掘金2026년 11월 3일 19:45중국어 → 한국어

LangChain4j와 Spring Boot 3.5로 9개 모델 서비스 연동

자바 대형 모델 앱에서 모델을 바꿀 때마다 API 주소와 파라미터 명명, 스트리밍 프로토콜이 달라 접착 코드를 다시 써야 하는 문제를, 세 줄 설정 변경으로 해결하는 방법을 다룬다.

중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.

9개 모델 서비스를 로컬 Ollama를 포함해 모두 동일한 OpenAI 호환 프로토콜 아래로 통합했으며, 모델을 전환할 때는 base-url, api-key, model-name 세 줄의 설정만 바꾸면 된다.

전체 코드는 이미 오픈소스로 공개되어 있어 Gitee 저장소에서 바로 복사해 사용할 수 있다.

Java로 대규모 모델 애플리케이션을 만들 때 진짜 골치 아픈 것은 대개 프롬프트를 작성하는 일이 아니라 "모델을 한 번 바꾸면 코드를 다시 손봐야 한다"는 점이다. API 주소가 다르고, 파라미터 이름이 다르고, 스트리밍 프로토콜도 다르기 때문에 모델이 바뀌면 이전에 작성해 둔 글루 코드도 함께 다시 작성해야 한다.

이 글에서 뜯어볼 LangChain4j-Chat 프로젝트가 바로 이 길을 가고 있으며, 그 위에 스트리밍 응답, 멀티턴 대화 기억, 선언형 AI 서비스까지 함께 넣었다. 기술 기반은 LangChain4j + Spring Boot 3.5다.

아래에서는 프로젝트 정체성부터 시작해, 어떻게 실행하는지, 문제를 만났을 때 어디를 봐야 하는지까지 이야기한다.

一. 프로젝트 정체성: "모델 교체 가능"을 아키텍처에 넣기

프로젝트 이름은 아주 직관적이며, 그대로 LangChain4j-Chat이다. 한 문장으로 그 능력을 요약하면 다음과 같다.

LangChain4j + Spring Boot 3.5 기반의 멀티 모델 AI 채팅 애플리케이션으로, 9개 모델 서비스가 전부 OpenAI 호환 프로토콜을 통해 연동된다.

이 프로젝트가 해결하는 것은 모델 교체 가능성 문제다. AI 기능을 만들 때 처음부터 한 번에 공급자를 정하는 경우는 드물다. 먼저 한 업체로 프로토타입을 돌려보고, 이후 비용이나 규정 준수, 또는 프라이빗 배포 필요에 따라 모델을 바꾸는 일은 흔하다. 호출 로직이 비즈니스 코드 안에 고정되어 있으면 모델을 한 번 바꾸는 것이 곧 소규모 리팩터링이 되지만, 모델 연동을 별도로 분리해 설정 주도로 만들면 모델 교체는 설정을 바꾸고 재시작하는 두 단계만 남는다.

공학 구조로 보면 표준적인 Spring Boot 단일 애플리케이션이며, 각 책임은 독립된 클래스로 분리되어 있다.

langchain4j-chat/ ├── src/main/java/com/song/langchain4j/ │ ├── config/ │ │ └── ChatMemoryConfig.java # 대화 기억 설정 │ ├── controller/ │ │ └── ChatController.java # REST API 컨트롤러 │ ├── dto/ │ │ └── ChatRequest.java # 요청 DTO │ ├── service/ │ │ └── ChatAssistant.java # AI 서비스 인터페이스(@AiService) │ └── ChatApplication.java # Spring Boot 시작 클래스 ├── src/main/resources/ │ └── application.yml # 애플리케이션 설정 파일 └── pom.xml # Maven 의존성 설정

설정 클래스는 기억을 담당하고, 컨트롤러는 라우팅을 담당하고, 서비스 인터페이스는 AI 호출을 담당한다. 계층이 명확하며, 이 뼈대를 따라 자신의 비즈니스로 옮기기도 쉽다.

프로젝트가 대외적으로 내건 특성은 모두 다섯 가지로, 채팅 백엔드를 만들 때 가장 흔히 필요한 능력을 대체로 아우른다.

- 멀티 모델 지원 : OpenAI, DeepSeek, 通义千问(큐원), 豆包(더우바오), 文心一言(원신옌), 智谱(즈푸), Kimi, 混元(훈위안), 로컬 Ollama와 호환되며, 전부 OpenAI 호환 프로토콜을 사용한다;

- 스트리밍 응답 : Server-Sent Events(SSE) 실시간 스트리밍 출력을 지원한다;

- 대화 기억 : 슬라이딩 윈도 기반의 멀티턴 대화 기억 관리를 제공한다;

- 선언형 API : @AiService 애노테이션을 사용해 간결하고 우아하게 서비스를 정의한다;

- 유연한 설정 : 설정 파일을 통해 서로 다른 모델을 손쉽게 전환한다.

二. 기술 스택: Java 17 + Spring Boot 3.5 + LangChain4j

프로젝트의 기술 스택은 다음과 같다.

- Java 17+

- Spring Boot 3.5.16

- LangChain4j 1.20.2-beta30

- Spring WebFlux(스트리밍 응답에 사용)

- Maven

< dependencies > < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-web </ artifactId > </ dependency > < dependency > < groupId > org.springframework.boot </ groupId > < artifactId > spring-boot-starter-webflux </ artifactId > </ dependency > ​ <!-- OpenAI 호환 프로토콜 starter: DeepSeek, DashScope 등이 이를 통해 연동된다 --> < dependency > < groupId > dev.langchain4j </ groupId > < artifactId > langchain4j-open-ai-spring-boot-starter </ artifactId > < version > 1.20.2-beta30 </ version > </ dependency > ​ <!-- 선언형 AI Service starter: @AiService 등의 자동 설정을 제공한다 --> < dependency > < groupId > dev.langchain4j </ groupId > < artifactId > langchain4j-spring-boot-starter </ artifactId > < version > 1.20.2-beta30 </ version > </ dependency > ​ <!-- 스트리밍 대화 --> < dependency > < groupId > dev.langchain4j </ groupId > < artifactId > langchain4j-reactor </ artifactId > < version > 1.20.2-beta30 </ version > </ dependency > </ dependencies >

두 의존성에 대해 몇 마디 더 하자면, LangChain4j는 Java 생태계의 LLM 애플리케이션 프레임워크로, 이 프로젝트는 이것의 OpenAI 호환 클라이언트와 @AiService 애노테이션을 통해 모델 호출을 인터페이스 메서드로 단순화한다. Spring WebFlux는 스트리밍 출력의 밑바탕이다. SSE로 글자 단위 반환을 하는데, 이는 "생성하면서 동시에 푸시"하는 반응형 스트림 모델과 정확히 맞아떨어지며, 타자기 효과도 바로 이것 덕분이다.

Spring Boot 3.5는 애플리케이션 전체의 인프라 스타일을 결정한다. 자동 구성, 애노테이션 기반, application.yml을 통한 중앙 집중식 설정 관리가 그것이다. Spring에 익숙한 개발자라면 진입 장벽이 매우 낮다.

덧붙여, 스트리밍 응답에 WebSocket을 선택하지 않은 이유를 말하자면, SSE는 단방향 서버 푸시 채널로, 서버가 새로 생성된 텍스트를 계속 클라이언트로 밀어 보내고 클라이언트는 받기만 하면 되며 양방향 통신이 필요 없다. "AI가 글자를 하나씩 내뱉는" 이런 시나리오에서는 SSE가 더 가볍고 HTTP 의미 체계에 더 부합하며, 브라우저와 각종 HTTP 클라이언트에서 소비하기도 더 쉽다. 상호작용이 정말로 양방향 실시간 통신을 필요로 할 때에야 WebSocket이 등판할 차례가 된다. 이 프로젝트가 SSE를 선택한 것은 그것이 충분히 쓰임새가 있고 또 단순하기 때문이다.

三、9개 모델 서비스, 하나의 OpenAI 호환 프로토콜로 해결

이것이 프로젝트에서 가장 흥미로운 설계다. 9개 모델 서비스가 전부 OpenAI 호환 프로토콜로 접속된다. 당신의 모델 서비스가 OpenAI 호환 인터페이스를 제공하기만 한다면, 접속 방식은 OpenAI에 접속하는 것과 전혀 다르지 않으며, 로컬 Ollama도 예외가 아니다. Ollama는 http://localhost:11434/v1 에서 동일한 /chat/completions 엔드포인트를 노출한다. 프로젝트가 지원하는 9개 모델 서비스는 다음과 같다(모델명은 각 플랫폼 콘솔의 최신 목록을 기준으로 한다).

모델 | 제공사 | 접속 방식 | 비고 OpenAI | OpenAI | OpenAI 프로토콜 | gpt-4o, GPT-4, GPT-3.5-turbo DeepSeek | DeepSeek | OpenAI 프로토콜 | deepseek-chat 通义千问 | 알리클라우드 | OpenAI 프로토콜 | qwen-plus, qwen-max, qwen-flash 豆包 | 바이트댄스 | OpenAI 프로토콜 | doubao-pro-32k, doubao-1.5-pro-32k 文心一言 | 바이두 | OpenAI 프로토콜 | ernie-4.0-8k, ernie-3.5-8k 智谱 AI | 智谱 | OpenAI 프로토콜 | glm-4, glm-3-turbo Kimi | Moonshot | OpenAI 프로토콜 | kimi-k3.1-pro, kimi-k2.6 混元 | 텐센트 | OpenAI 프로토콜 | hunyuan-turbos-latest, hunyuan-lite Ollama | 로컬 배포 | OpenAI 프로토콜 | llama3, qwen2.5 등

주의: 豆包, 文心, Kimi의 모델명에는 규격 접미사(예: -32k, -8k)가 붙어 있어, 순수 이름만 입력하면 model not found가 발생한다. 상용화 전에 각 플랫폼 콘솔의 모델 목록을 기준으로 확인하기 바란다.

모델을 전환할 때는 세 가지만 바꾸면 된다. base-url(모델의 API 주소), api-key(키), model-name(모델명)이다. 각 서비스의 Base URL은 다음 표를 참고하면 된다.

모델 | Base URL OpenAI | https://api.openai.com/v1 DeepSeek | https://api.deepseek.com/v1 通义千问 | https://dashscope.aliyuncs.com/compatible-mode/v1 豆包 | https://ark.cn-beijing.volces.com/api/v3 文心一言 | https://qianfan.baidubce.com/v2 智谱 AI | https://open.bigmodel.cn/api/paas/v4 Kimi | https://api.moonshot.cn/v1 混元 | https://api.hunyuan.cloud.tencent.com/v1 Ollama | http://localhost:11434/v1

다른 서비스를 설정할 때 이 표대로 하면 되고, 이후 어느 모델을 설정하든 같은 절차다.

四、빠른 시작: 세 단계로 프로젝트 실행하기

1. 사전 조건

시작하기 전에 환경이 갖춰졌는지 확인한다:

- JDK 17 이상;

- Maven 3.8+;

- 최소 하나의 사용 가능한 대형 모델 API Key(또는 로컬 Ollama 사용, Key 불필요).

2. 프로젝트 클론

git clone <your-repo-url> cd langchain4j-chat

3. 모델 설정

프로젝트의 모델 설정은 src/main/resources/application.yml에 모여 있다. 아래에 네 가지 전형적인 설정을 제시하니 필요에 따라 사용하면 된다.

네 가지 설정의 구조는 완전히 동일하다. 각 설정은 chat-model(일반 대화)과 streaming-chat-model(스트리밍 대화)을 동시에 선언하며, 두 가지의 api-key, model-name, base-url은 반드시 동일하게 유지해야 하고, 다만 전자는 동기 반환, 후자는 글자 단위 푸시다. 이하 지면을 아끼기 위해 streaming-chat-model 부분은 핵심 필드만 기재한다.

설정 1: Ollama로 로컬 모델 실행(돈 들지 않고, 오프라인 가능):

langchain4j: open-ai: chat-model: api-key: unused # Ollama 不需要 API Key model-name: qwen2.5:3b base-url: http://localhost:11434/v1 temperature: 0.7 # streaming-chat-model 与 chat-model 字段完全相同(api-key/model-name/ # base-url/temperature),实际配置时去掉注释写全,否则流式接口无法启动: # streaming-chat-model: # api-key: unused # model-name: qwen2.5:3b # base-url: http://localhost:11434/v1 # temperature: 0.7

설정 2: 퉁이치엔원(DashScope) 사용:

langchain4j: open-ai: chat-model: api-key: ${DASHSCOPE_API_KEY:sk-xxxxxxxxxxxxxxxx} model-name: qwen-plus base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 temperature: 0.7 streaming-chat-model: # streaming-chat-model 段略:字段与 chat-model 完全相同,实际配置时必须两段都写

설정 3: DeepSeek 사용:

langchain4j: open-ai: chat-model: api-key: ${DEEPSEEK_API_KEY:sk-xxxxxxxxxxxxxxxx} model-name: deepseek-chat base-url: https://api.deepseek.com/v1 temperature: 0.7 streaming-chat-model: # streaming-chat-model 段略:字段与 chat-model 完全相同,实际配置时必须两段都写

설정 4: OpenAI 사용:

langchain4j: open-ai: chat-model: api-key: ${OPENAI_API_KEY:sk-xxxxxxxxxxxxxxxx} model-name: gpt-4o base-url: https://api.openai.com/v1 temperature: 0.7 streaming-chat-model: # streaming-chat-model 段略:字段与 chat-model 完全相同,实际配置时必须两段都写

설정에서 api-key는 모두 ${ENV_VAR:default_value} 형식으로 작성했는데, 이는 바로 키를 코드베이스에서 분리하기 위해서입니다. 환경 변수로 민감 정보를 감싸는 구체적인 방법은 뒤의 7절에서 다시 펼쳐 설명합니다.

4. 애플리케이션 실행

패키징 후 실행:

mvn clean package java -jar target/langchain4j-chat-1.0-SNAPSHOT.jar

개발 단계에서는 Maven으로 바로 실행할 수도 있습니다:

mvn spring-boot:run

5. 인터페이스 호출: 일반 대화, 스트리밍 대화 및 컨트롤러 구현

애플리케이션이 뜨면 외부에 노출되는 것은 REST 스타일 인터페이스입니다. 프로젝트에는 세 가지 전형적인 사용법이 제공되며, 이 절 끝에 이들을 담당하는 컨트롤러 구현을 첨부합니다.

1. 일반 대화(비스트리밍)

완전한 답변을 한 번에 받으며, 백엔드 서비스 호출에 적합합니다:

curl -X POST http: //localhost:8080/ai/chat \ -H "Content-Type: application/json" \ -d '{ "memoryId" : "user123" , "message" : "你好,请介绍一下你自己" }'

응답은 완전한 텍스트 한 단락입니다:

"你好!我是你的 AI 助手,很高兴为你提供帮助。有什么问题都可以问我!"

2. 스트리밍 대화(SSE)

"실시간감"을 가장 잘 보여 주는 인터페이스입니다. 요청 시 curl -N으로 버퍼링을 끄면 글자가 하나씩 반환되는 효과를 볼 수 있습니다:

curl -N "http://localhost:8080/ai/stream?memoryId=user123&message=请写一首关于春天的诗"

반환되는 것은 SSE 이벤트 스트림이며, 문자마다 하나의 data: 이벤트입니다:

data: 春 data: 天 data: 来 data: 了 data: , data: 万 data: 物 data: 复 data: 苏 ...

브라우저에서 접속하면 글자가 하나씩 튀어나오는 타자기 효과를 볼 수 있습니다. 프런트엔드 입장에서는 SSE 이벤트 스트림을 소비하기만 하면 됩니다.

3. 다중 턴 대화

다중 턴 대화는 요청에 담긴 memoryId에 의존합니다. 같은 memoryId는 대화 이력을 공유하고, 다른 memoryId는 각자 대화하며 서로 간섭하지 않습니다:

第一轮 curl -X POST http : //localhost:8080/ai/chat \ -H "Content-Type: application/json" \ -d ' { "memoryId" : "user123" , "message" : "我叫张三" } ' # 响应: "你好张三,有什么可以帮助你的吗?" # 第二轮(AI 会记得你的名字) curl -X POST http : //localhost:8080/ai/chat \ -H "Content-Type: application/json" \ -d ' { "memoryId" : "user123" , "message" : "我叫什么名字?" } ' # 响应: "你叫张三"

두 번째 라운드에서는 모델에게 이름을 알려주지 않았는데도 여전히 "당신의 이름은 장싼(张三)입니다"라고 답했는데, 이는 같은 memoryId 아래의 이전 메시지들이 자동으로 컨텍스트에 포함되었기 때문이다.

4. 컨트롤러: AI 기능을 REST 인터페이스로 노출하기

ChatController.java의 핵심 구현은 다음과 같으며, 핵심은 스트리밍 엔드포인트의 produces = TEXT_EVENT_STREAM_VALUE이다. 이것이 없으면 브라우저는 응답을 SSE 이벤트 스트림으로 소비하지 않는다:

@RestController @RequestMapping ( "/ai" ) public class ChatController { private final ChatAssistant assistant ; public ChatController (ChatAssistant assistant) { this .assistant = assistant ; } /** * 일반 대화 인터페이스(비스트리밍) * * @param request 채팅 요청, memoryId와 message 포함 * @return AI 전체 응답 */ @ PostMapping ( "/chat" ) public String chat ( @RequestBody ChatRequest request) { return assistant .chat (request. getMemoryId (), request. getMessage ()); } /** * 스트리밍 대화 인터페이스(SSE) * * @param memoryId 대화 기억 ID * @param message 사용자 메시지 * @return 스트리밍 응답 데이터 스트림 */ @ GetMapping (value = "/stream" , produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux < String > stream ( @RequestParam (value = "memoryId" ) String memoryId, @RequestParam (value = "message" ) String message) { return assistant .stream (memoryId, message); } }

6. 핵심 코드 분석: 선언적 AI 서비스와 대화 기억

프로젝트 코드 분량은 많지 않고, 핵심 설계는 두 가지에 집중되어 있다:

1. 선언적 AI 서비스: 애노테이션 하나로 인터페이스에 구현이 생긴다

src/main/java/com/song/langchain4j/service/ChatAssistant.java는 @AiService 애노테이션으로 AI 호출을 자동 구현한다:

@AiService public interface ChatAssistant { @SystemMessage ( "당신은 남을 돕는 것을 좋아하는 AI 어시스턴트이며, 답변은 간결하고 정확합니다." ) String chat ( @MemoryId String memoryId, @UserMessage String message); @SystemMessage ( "당신은 남을 돕는 것을 좋아하는 AI 어시스턴트이며, 답변은 간결하고 정확합니다." ) Flux<String> stream ( @MemoryId String memoryId, @UserMessage String message); }

- @SystemMessage는 시스템 프롬프트를 정의하는 것으로, AI에게 역할을 설정하는 것이다;

- @MemoryId는 대화 기억의 소속을 표시하며, 요청에 있는 memoryId에 대응한다;

- @UserMessage는 사용자 입력을 표시한다;

- String을 반환하면 동기식 일회성 응답이고, Flux<String>을 반환하면 반응형 스트림이며, 이는 바로 위의 일반 대화와 스트리밍 대화 두 가지 인터페이스에 정확히 대응한다.

한 가지 쉽게 빠지는 함정을 덧붙이자면, Flux<String>을 반환하는 것은 langchain4j-reactor 모듈의 어댑터 지원에 의존하므로, 이 의존성을 빠뜨리면 애플리케이션이 시작 시 "지원하지 않는 반환 타입" 오류를 보고한다. Reactor를 도입하고 싶지 않다면 LangChain4j가 기본 제공하는 TokenStream 반환 타입을 대신 사용하고, Controller에서 수동으로 SSE 이벤트 스트림으로 변환해도 된다.

각 Controller에서 HTTP 요청을 직접 작성하고 JSON을 조합하고 응답을 수동으로 파싱하는 것보다, 이렇게 "AI 호출을 인터페이스로 선언하는" 작성 방식이 가독성과 유지보수성 모두 한층 뛰어나다.

2. 대화 기억: 슬라이딩 윈도우로 컨텍스트 길이 제어

다중 턴 대화의 기억 관리는 src/main/java/com/song/langchain4j/config/ChatMemoryConfig.java에 설정되어 있으며, 슬라이딩 윈도우 전략을 사용한다:

@Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId -> MessageWindowChatMemory .builder () .id (memoryId) .maxMessages ( 20 ) // 최대 20개 메시지 유지 .build (); }

기억은 memoryId별로 격리되며, ChatMemoryProvider는 ID를 받으면 해당 기억을 반환하므로 서로 다른 사용자가 간섭하지 않는다; maxMessages(20)는 각 세션이 몇 개의 메시지를 유지할지 제한한다. 윈도우 크기는 연속성과 비용 사이의 절충이다: 크게 늘리면 모델이 기억하는 이력이 많아지고 매 요청에 포함되는 컨텍스트도 길어진다; 작게 줄이면 응답이 절약되지만 대화가 쉽게 "건망증"을 겪는다. 20개는 단지 기본값이며, 업무 리듬에 맞춰 조정하면 된다.

기억 길이를 조정하고 싶다면 .maxMessages(20) 이 파라미터만 바꾸면 된다.

7. 자주 묻는 질문 빠른 참고

프로젝트 README에는 자주 나오는 몇 가지 질문이 정리되어 있으며, 실제로 손을 대다 보면 대부분 마주치게 된다.

Q1: 서로 다른 모델은 어떻게 전환하나?

application.yml의 base-url, api-key, model-name 세 가지 설정 항목을 수정한 후 애플리케이션을 재시작하면 된다.

Q2: 중국어가 깨져 나오면 어떻게 하나?

프로젝트는 이미 UTF-8 인코딩이 기본 활성화되어 있습니다. 그래도 글자가 깨진다면 두 가지를 확인하세요. 터미널/브라우저가 UTF-8 인코딩을 사용하는지, 설정 파일이 UTF-8 포맷으로 저장되었는지입니다.

Q3: 대화 기억 길이는 어떻게 조정하나요?

ChatMemoryConfig.java의 .maxMessages(20) 파라미터를 수정하면 됩니다.

Q4: 스트리밍 응답이 적용되지 않나요?

세 가지를 순서대로 점검하세요. streaming-chat-model이 설정되어 있는지, 클라이언트가 SSE(Server-Sent Events)를 지원하는지, 콘솔 로그에 오류가 있는지입니다.

Q5: 로컬 Ollama는 어떻게 사용하나요?

- Ollama를 설치하고 실행합니다(공식 사이트 ollama.com).

- 모델을 받습니다: ollama pull qwen2.5:3b

- 설정의 base-url을 http://localhost:11434/v1로 변경합니다.

- api-key는 임의의 값(예: unused)을 넣어도 됩니다.

키 보안에 관하여: 프로덕션 환경에서는 민감한 설정을 환경 변수로 관리하고, 설정 파일에 하드코딩하지 않는 것을 권장합니다.

Linux/Mac export OPENAI_API_KEY = "sk-xxxxxxxxxxxxxxxx" export DASHSCOPE_API_KEY = "sk-xxxxxxxxxxxxxxxx" export DEEPSEEK_API_KEY = "sk-xxxxxxxxxxxxxxxx" # Windows set OPENAI_API_KEY =sk-xxxxxxxxxxxxxxxx

그다음 application.yml에서 ${ENV_VAR:default_value} 형식으로 참조하면 되는데, 이는 앞선 설정 예시에 있던 ${...} 표기가 바로 이 방식입니다.

Q6: Hunyuan을 연결할 때 주의할 점은 무엇인가요?

텐센트 클라우드의 대형 모델 서비스는 TokenHub 플랫폼으로 업그레이드되었으며, 본문에서 사용한 Hunyuan의 OpenAI 호환 주소(https://api.hunyuan.cloud.tencent.com/v1)는 기존 사용자를 위해 유지됩니다. 신규 사용자는 TokenHub 콘솔에서 직접 접속 주소와 모델 목록을 받는 것을 권장하며, 접속 방식 역시 OpenAI 호환 프로토콜입니다.

8. 현재의 제한과 가능한 진화 방향

프로젝트는 README에서 세 가지 현재 제한을 매우 솔직하게 나열하고 있습니다.

- 런타임 동적 모델 전환 미지원: 설정 파일을 수정하고 애플리케이션을 재시작해야 합니다.

- 단일 모델 모드: 동시에 하나의 모델만 사용할 수 있어 여러 모델을 동시에 호출할 수 없습니다.

- 모델 라우팅 기능 없음: 모든 요청이 동일하게 설정된 하나의 모델을 사용합니다.

한마디로 요약하면, 설정 기반의 단일 모델 백엔드입니다. 프로토타입 제작, 내부 도구 제작, 접속 방식 학습에는 충분합니다. 시나리오별 동적 라우팅과 다중 모델 협업이 필요하다면 직접 아키텍처를 업그레이드해야 합니다.

9. 가져다 개조하고 싶다면? 세 단계로 내 업무에 연결하기

이 프로젝트를 자신의 채팅 백엔드의 출발점으로 삼고 싶다면, 개조 경로는 사실 꽤 명확하며 기본적으로 세 가지를 중심으로 합니다.

첫째, 모델을 정합니다. 모델 관련 사항은 전부 application.yml에 있으므로 base-url, api-key, model-name 세 항목만 바꾸면 됩니다. 처음에는 로컬 Ollama로 시작하는 것을 권장합니다. Key를 신청할 필요도, 온라인 호출 비용도 없이 "요청 유입 → 모델 반환 → 프런트엔드 표시"라는 흐름을 먼저 통과시키고, 문제가 없다는 것을 확인한 뒤 온라인 모델로 교체하면 됩니다.

둘째, 인터페이스를 정합니다. 업무에 필요한 AI 능력은 모두 ChatAssistant 인터페이스에 추가합니다. 메서드를 하나 추가할 때마다 @SystemMessage로 이 시나리오의 역할과 제약을 설정하고, @UserMessage로 사용자 입력을 받고, @MemoryId로 어느 세션에 속하는지 표시합니다. String을 반환하면 일회성 응답, Flux<String>을 반환하면 스트리밍 응답입니다. 작성 방식은 앞서 나온 두 줄 예시와 완전히 동일하며, HTTP 계층을 직접 건드릴 필요가 없습니다.

셋째, 라우팅을 정합니다. ChatController에서 새 메서드를 REST 인터페이스로 노출하고, memoryId로 세션을 구분합니다. 프런트엔드에서 각 사용자(또는 각 대화 창)에게 독립적인 ID를 부여하면 기억이 자동으로 격리되어 서로 섞이지 않습니다.

놓치기 쉬운 마무리 작업이 두 가지 더 있습니다. 기억 창의 .maxMessages(20)은 업무 리듬에 맞게 조정해야 하며, 고객센터 Q&A 같은 짧은 대화는 너무 많이 남길 필요가 없고, 긴 기록형 대화는 적절히 늘릴 수 있습니다. api-key는 일괄적으로 환경 변수를 통해 관리하고 코드 저장소에 커밋하지 않습니다.

이 세 단계를 마치면 모델 전환이 가능하고, 스트리밍 출력을 지원하며, 다중 턴 기억을 갖춘 AI 채팅 백엔드를 얻게 됩니다.

10. 소스 코드

전체 코드가 오픈 소스로 공개되었습니다: langchain4j-chat(Gitee)

맺음말

이 프로젝트에서 정말 배울 만한 점은 "모델 교체 가능성"을 아키텍처의 핵심 설계로 삼은 것입니다. OpenAI 호환 프로토콜이 각 사의 차이를 평준화하고, 설정 기반 구동이 전환 비용을 최소로 줄이며, 선언형 API와 슬라이딩 윈도우 기억이 대화 백엔드의 흔히 쓰이는 능력을 채워 줍니다. Java 개발자가 이것을 채팅 백엔드의 출발점으로 삼거나 LangChain4j와 Spring Boot 3.5 통합의 참조 구현으로 삼기에 모두 적합합니다.

직접 한 번 실행해 보고 싶다면 로컬 Ollama 설정으로 시작하면 한 푼도 들이지 않고 전체 흐름을 통과할 수 있습니다.