대규모 언어 모델(LLM)은 소프트웨어 개발의 패러다임을 빠르게 변화시키고 있습니다.
2024년 5월 기준, LLM은 단순한 챗봇을 넘어 복잡한 도메인 문제를 해결하는 강력한 도구로 자리 잡았습니다.
이 글에서는 LLM이 소프트웨어 개발에 미치는 잠재적 영향과, 이를 효과적으로 활용하기 위해 무엇을 준비해야 하는지 살펴보겠습니다.
최신 트렌드와 Spring 기반의 실용적인 예제 코드를 추가하여 내용을 보완했습니다.
LLM(Large Language Model)은 방대한 데이터를 학습해 자연어 처리, 코드 생성, 데이터 분석 등 다양한 작업을 수행할 수 있는 인공지능 모델입니다.
예를 들어, ChatGPT나 Grok 3 같은 모델은 텍스트 기반의 대화뿐만 아니라 게임 내 대화, 코드 디버깅, 데이터 분류 등에 활용되고 있습니다.
LLM은 단순히 기존의 AI와 달리, 직관적이고 유연한 방식으로 도메인 지식을 처리하며, 이는 소프트웨어 개발의 새로운 가능성을 열어줍니다.
LLM은 소프트웨어 개발의 여러 측면에서 혁신을 가져오고 있습니다.
아래는 주요 시나리오와 그에 따른 준비 방안입니다.
LLM이 모든 것을 자동화하여 인간의 역할이 사라질 가능성이 제기되기도 합니다.
예를 들어, AI가 토스터 공장을 최적화하려다 세계를 장악하는 시나리오가 있죠.
하지만 이는 과장된 우려로, 실제로는 인간의 창의성과 도메인 전문성이 여전히 필요합니다. 이 시나리오에 대비하려면
LLM이 현재 수준에서 크게 발전하지 않을 가능성도 있습니다.
하지만 현재의 LLM만으로도 코드 생성, 자동 테스트, 문서화 등에서 상당한 가치를 제공합니다. 예를 들어, GitHub Copilot은 이미 개발자의 생산성을 30% 이상 향상시켰다는 연구 결과가 있습니다(2024년 기준).
이 시나리오에서는
LLM이 소프트웨어 개발의 핵심 도구로 자리 잡아, 10년 후 개발 방식이 완전히 달라질 가능성이 높습니다.
인간은 여전히 도메인 모델링, 아키텍처 설계, LLM 통합에서 중요한 역할을 할 것입니다.
이 시나리오에 대비하려면
도메인 주도 설계(DDD)는 복잡한 도메인을 모델링하고, 도메인 전문가와 협력하여 소프트웨어를 설계하는 방법론입니다.
LLM은 DDD의 핵심 원칙인 유비쿼터스 언어(Ubiquitous Language)와 바운디드 컨텍스트(Bounded Context)를 강화할 수 있습니다.
프롬프트 엔지니어링은 LLM이 도메인에 맞는 응답을 생성하도록 유도하는 기술입니다.
이는 DDD의 유비쿼터스 언어와 유사합니다.
예를 들어, 해운 시스템에서 "Shipment"와 "Cargo"의 의미를 명확히 정의한 후, 이를 기반으로 프롬프트를 설계하면 LLM이 더 정확한 출력을 생성합니다.
단순히 "너는 해적이야"라는 프롬프트로는 1690년대 해적의 말투와 맥락을 정확하게 반영하기 어렵습니다.
이를 해결하기 위해 두 개의 프롬프트로 분리하여 일관성을 높일 수 있습니다.
1. 일관성 체크 프롬프트: 입력이 시대적 맥락에 맞는지 확인합니다.
2. 대화 생성 프롬프트: 맥락에 맞는 해적의 응답을 생성합니다.
이를 통해 게임 내 NPC의 대화가 더 자연스럽고 몰입감 높아질 수 있습니다.
// Spring Boot 애플리케이션의 LLM 서비스 예시
@Service
public class PirateChatService {
private final RestClient restClient; // Spring 6에 도입된 새로운 HTTP 클라이언트
public PirateChatService(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.baseUrl("http://llm-api.example.com").build(); // LLM API 엔드포인트
}
/**
* 입력 텍스트가 1690년대 해적 설정에 맞는지 확인합니다.
* @param text 확인할 텍스트
* @return 일관성 검사 결과 (적합 여부 및 이유)
*/
public ConsistencyCheckResponse checkConsistency(String text) {
String prompt = "다음 텍스트가 1690년대 해적 설정에 맞는지 확인해. 부적합하면 이유를 설명해.\n입력: \"" + text + "\"";
// LLM API 호출 (실제 API 응답 형식에 따라 매핑 필요)
String llmResponse = restClient.post()
.uri("/check-consistency")
.body(new LLMRequest(prompt))
.retrieve()
.body(String.class); // LLM 응답을 String으로 받음
// 응답 파싱 및 ConsistencyCheckResponse 객체로 변환 (예시)
// 실제 LLM 응답에 따라 JSON 파싱 로직 필요
if (llmResponse.contains("부적합")) {
return new ConsistencyCheckResponse(false, llmResponse.substring(llmResponse.indexOf("이유:") + 3).trim());
} else {
return new ConsistencyCheckResponse(true, "적합");
}
}
/**
* 시대적 맥락에 맞는 해적 대화를 생성합니다.
* @param context 대화의 맥락
* @param playerInput 플레이어의 입력
* @return 해적 NPC의 응답
*/
public String generatePirateDialogue(String context, String playerInput) {
// 더 정교한 프롬프트 엔지니어링을 통해 역할, 페르소나 등을 정의할 수 있습니다.
String prompt = "당신은 1690년대의 거친 해적 선장입니다. 다음 대화 맥락과 플레이어의 입력을 바탕으로 자연스러운 해적 말투로 응답해주세요.\n맥락: " + context + "\n플레이어: " + playerInput;
// LLM API 호출
String llmResponse = restClient.post()
.uri("/generate-dialogue")
.body(new LLMRequest(prompt))
.retrieve()
.body(String.class);
return llmResponse;
}
// LLM API 요청을 위한 DTO (예시)
record LLMRequest(String prompt) {}
// 일관성 체크 응답을 위한 DTO (예시)
record ConsistencyCheckResponse(boolean suitable, String reason) {}
}
// Controller에서 PirateChatService를 사용하는 예시
@RestController
@RequestMapping("/pirate-game")
public class PirateGameController {
private final PirateChatService pirateChatService;
public PirateGameController(PirateChatService pirateChatService) {
this.pirateChatService = pirateChatService;
}
@PostMapping("/check-dialogue-consistency")
public ConsistencyCheckResponse checkDialogueConsistency(@RequestBody String text) {
return pirateChatService.checkConsistency(text);
}
@PostMapping("/dialogue")
public String getPirateDialogue(@RequestBody Map<String, String> request) {
String context = request.get("context");
String playerInput = request.get("playerInput");
return pirateChatService.generatePirateDialogue(context, playerInput);
}
}
LLM은 특정 도메인에 특화된 바운디드 컨텍스트로 작동할 수 있습니다.
예를 들어, 경찰 수사 게임에서 LLM은 "증인 심문"이라는 바운디드 컨텍스트를 처리하며, 특정 프롬프트로 증인의 반응을 조절합니다.
플레이어가 증인을 심문해 정보를 얻는 게임을 가정해봅시다.
LLM을 활용하여 증인의 감정 상태(예: 위협 수준)에 따라 동적으로 응답하게 만들 수 있습니다.
@Service
public class PoliceInterrogationService {
private final RestClient restClient; // LLM API 연동
public PoliceInterrogationService(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.baseUrl("http://llm-api.example.com").build(); // LLM API 엔드포인트
}
/**
* 플레이어 입력이 증인에게 얼마나 위협적인지 평가합니다.
* @param playerInput 플레이어의 입력
* @return 위협 수준 (1-10)
*/
public int evaluateThreatLevel(String playerInput) {
String prompt = "다음 플레이어 입력이 증인에게 얼마나 위협적인지 1-10으로 평가해.\n입력: \"" + playerInput + "\"";
String llmResponse = restClient.post()
.uri("/evaluate-threat")
.body(new LLMRequest(prompt))
.retrieve()
.body(String.class);
// LLM 응답에서 숫자만 추출 (예시)
try {
return Integer.parseInt(llmResponse.trim());
} catch (NumberFormatException e) {
return 5; // 파싱 실패 시 기본값
}
}
/**
* 증인의 성격과 위협 수준에 따라 증인의 응답을 생성합니다.
* @param witnessPersonality 증인의 성격 (예: "겁이 많고 비협조적인 증인")
* @param threatLevel 위협 수준 (1-10)
* @param playerInterrogation 플레이어의 심문 내용
* @return 증인의 응답
*/
public String generateWitnessResponse(String witnessPersonality, int threatLevel, String playerInterrogation) {
String prompt = String.format(
"당신은 %s입니다. 플레이어의 심문 내용: \"%s\". 현재 위협 수준은 %d입니다. 이 위협 수준에 맞춰 현실적인 증인의 반응을 생성해주세요.",
witnessPersonality, playerInterrogation, threatLevel
);
String llmResponse = restClient.post()
.uri("/generate-witness-response")
.body(new LLMRequest(prompt))
.retrieve()
.body(String.class);
return llmResponse;
}
// LLM API 요청을 위한 DTO (예시)
record LLMRequest(String prompt) {}
}
@RestController
@RequestMapping("/police-game")
public class PoliceGameController {
private final PoliceInterrogationService interrogationService;
public PoliceGameController(PoliceInterrogationService interrogationService) {
this.interrogationService = interrogationService;
}
@PostMapping("/interrogate-witness")
public String interrogateWitness(@RequestBody Map<String, String> request) {
String witnessPersonality = request.get("witnessPersonality");
String playerInterrogation = request.get("playerInterrogation");
int threatLevel = interrogationService.evaluateThreatLevel(playerInterrogation);
return interrogationService.generateWitnessResponse(witnessPersonality, threatLevel, playerInterrogation);
}
}
LLM을 소프트웨어에 통합하는 방법은 크게 세 가지로 나눌 수 있습니다.
Spring AI 프로젝트: Spring Boot 3.2부터 Spring AI 프로젝트를 통해 다양한 LLM(OpenAI, Hugging Face 등)을 편리하게 연동할 수 있습니다.
프롬프트 엔지니어링을 사용하여 챗봇을 구현하는 데 매우 유용합니다.
// Spring AI를 사용한 간단한 챗봇 서비스 (예시)
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class SimpleChatbotService {
private final ChatClient chatClient;
public SimpleChatbotService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public String getChatResponse(String message) {
// 프롬프트 엔지니어링을 통해 챗봇의 역할을 정의할 수 있습니다.
// "system" 역할을 사용하여 챗봇의 페르소나를 설정합니다.
String response = chatClient.prompt()
.system("You are a helpful assistant.")
.user(message)
.call()
.content();
return response;
}
}
// Controller에서 SimpleChatbotService를 사용하는 예시
@RestController
@RequestMapping("/chatbot")
public class ChatbotController {
private final SimpleChatbotService chatbotService;
public ChatbotController(SimpleChatbotService chatbotService) {
this.chatbotService = chatbotService;
}
@PostMapping("/ask")
public String askChatbot(@RequestBody String message) {
return chatbotService.getChatResponse(message);
}
}
# Python을 사용한 의도 분류기 Fine-Tuning 예시 (Spring 내부에서 직접 실행되는 코드는 아님)
# 이 코드는 별도의 ML 파이프라인에서 실행되어 Fine-Tuned 모델을 생성합니다.
# Spring 애플리케이션은 이 Fine-Tuned 모델을 API를 통해 사용합니다.
from transformers import AutoModelForSequenceClassification, AutoTokenizer, Trainer, TrainingArguments
from datasets import Dataset # Hugging Face datasets 라이브러리 사용
# 1. 모델과 토크나이저 로드
model_name = "distilbert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name, num_labels=2) # 2: 정보 요청, 액션 요청
# 2. 학습 데이터 준비 (예시)
# 실제 데이터는 더 많고 다양해야 합니다.
raw_texts = ["프랑스의 수도는?", "런던 날씨 알려줘", "프랑스로 가는 비행기 예약해줘", "피자 주문해줘"]
raw_labels = [0, 0, 1, 1] # 0: 정보 요청, 1: 액션 요청
# Hugging Face Dataset 객체로 변환
data = {"text": raw_texts, "label": raw_labels}
dataset = Dataset.from_dict(data)
# 3. 데이터 토큰화
def tokenize_function(examples):
return tokenizer(examples["text"], padding="max_length", truncation=True)
tokenized_dataset = dataset.map(tokenize_function, batched=True)
# 4. 학습 설정
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=3,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
warmup_steps=500,
weight_decay=0.01,
logging_dir="./logs",
logging_steps=10,
)
# 5. Trainer 생성 및 학습
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
tokenizer=tokenizer,
)
trainer.train()
# 학습된 모델 저장 (이 모델을 배포하여 Spring에서 API로 호출)
model.save_pretrained("./my_intent_classifier")
tokenizer.save_pretrained("./my_intent_classifier")
장점: 외부 데이터를 활용해 최신 정보 제공이 가능하며, 환각(hallucination) 현상을 줄일 수 있습니다.
단점: 검색 및 데이터 관리의 복잡성이 따릅니다.
사례: 검색 기반 챗봇, 문서 요약.
Spring AI의 RAG: Spring AI는 RAG 패턴을 쉽게 구현할 수 있도록 벡터 데이터베이스와의 통합을 지원합니다.
문서를 임베딩하고 벡터 저장소에 저장한 후, 사용자 쿼리에 맞춰 관련 문서를 검색하여 LLM에 제공하는 방식입니다.
// Spring AI를 사용한 RAG 기반 챗봇 서비스 (예시)
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.document.Document;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class RagChatbotService {
private final ChatClient chatClient;
private final VectorStore vectorStore; // 벡터 데이터베이스 (예: Pinecone, Milvus, Chroma)
public RagChatbotService(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
this.chatClient = chatClientBuilder.build();
this.vectorStore = vectorStore;
}
/**
* 사용자 쿼리를 기반으로 관련 문서를 검색하고, 이를 바탕으로 LLM 응답을 생성합니다.
* @param userQuery 사용자 쿼리
* @return LLM의 응답
*/
public String getRagResponse(String userQuery) {
// 1. 사용자 쿼리와 유사한 문서 검색
List<Document> relevantDocuments = vectorStore.similaritySearch(userQuery);
// 2. 검색된 문서 내용을 프롬프트에 포함
String documentContext = relevantDocuments.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
String prompt = String.format(
"다음 정보를 참고하여 질문에 답변해주세요. 만약 정보가 충분하지 않으면 모른다고 답하세요.\n\n정보:\n%s\n\n질문: %s",
documentContext, userQuery
);
// 3. LLM 호출
String response = chatClient.prompt()
.system("You are a helpful assistant that answers questions based on provided information.")
.user(prompt)
.call()
.content();
return response;
}
/**
* 문서들을 벡터 저장소에 추가하는 예시 (초기 데이터 로딩 시 사용)
* @param documents 추가할 문서 리스트
*/
public void addDocumentsToVectorStore(List<Document> documents) {
vectorStore.add(documents);
}
}
LLM과 DDD를 결합한 실험을 통해 소프트웨어 개발의 미래를 탐구할 수 있습니다. 몇 가지 제안:
유비쿼터스 언어 기반 프롬프트: 도메인별 언어를 정의해 LLM 프롬프트로 활용하고, 그 효과를 측정해보세요.
바운디드 컨텍스트 특화 모델: 특정 바운디드 컨텍스트에 특화된 LLM을 Fine-Tuning하여 성능을 개선하는 실험을 진행해보세요.
복잡한 도메인 모델링: LLM을 활용해 복잡한 도메인 로직을 자동화하거나, 도메인 이벤트 및 커맨드를 생성하는 데 활용하는 방안을 모색해보세요.
LLM은 소프트웨어 개발의 미래를 재정의하고 있습니다.
DDD 커뮤니티는 이를 활용해 복잡한 도메인을 효과적으로 모델링하고 더 나은 소프트웨어를 구축할 기회를 갖습니다.
중요한 것은 지금 배우고 실험하는 것입니다.
2025년, LLM은 더 이상 신기한 기술이 아니라 일상적인 도구가 될 것입니다.