Spring AI와 Azure OpenAI를 활용한 AI 기반 여행 플래너 구축하기
요약
Spring AI와 Azure OpenAI를 사용하여 개인화된 여행 일정을 생성하는 REST API 구축 과정을 다룬 튜토리얼입니다. 프로젝트 설정부터 프롬프트 엔지니어링, 서비스 및 컨트롤러 레이어 구현까지 단계별 가이드를 제공합니다.
핵심 포인트
- Spring AI를 활용한 Azure OpenAI 연동 방법
- 사용자 선호도 기반의 구조화된 JSON 응답 생성
- Spring Boot 기반의 프로덕션 준비 완료된 REST API 설계
- 프롬프트 엔지니어링을 통한 여행 계획 로직 구현
AI를 사용하여 개인화된 여행 일정(itinerary)을 생성하는 프로덕션 준비 완료된(production-ready) REST API를 구축하기 위한 완전한 단계별 튜토리얼입니다.
목차
- 우리가 만드는 것
- 프로젝트 설정 및 의존성 (Dependencies)
- 애플리케이션 진입점 (Entry Point)
- 요청 및 응답 모델 (DTOs)
- Spring AI를 이용한 프롬프트 엔지니어링 (Prompt Engineering)
- 서비스 레이어 (Service Layer) — 핵심 비즈니스 로직
- 컨트롤러 레이어 (Controller Layer) — REST API
- 예외 처리 (Exception Handling) 및 에러 응답
- OpenAPI / Swagger 설정
- 애플리케이션 설정
- 애플리케이션 실행
- API 테스트
- 실제 사용 사례 (Real-World Use Cases)
- 일반적인 문제 해결 (Troubleshooting)
1. 우리가 만드는 것
당신이 Goa 여행을 계획하고 있다고 상상해 보세요. 예산은 ₹25,000이고, 기간은 5일이며, 당신은 해변, 음식, 그리고 밤문화를 좋아합니다. 만약 AI가 활동, 식사 제안, 예산 내역, 준비물 체크리스트, 그리고 안전 팁을 포함한 완벽한 일별 일정을 즉석에서 생성해 준다면 정말 멋지지 않을까요?
이 프로젝트가 바로 그 역할을 수행합니다.
**AI 여행 플래너 (The AI Travel Planner)**는 다음과 같은 기능을 수행하는 Spring Boot REST API입니다:
- 사용자의 여행 선호도(목적지, 기간, 예산, 스타일, 관심사)를 수락합니다.
- 이를 Azure OpenAI (GPT 모델)로 전송합니다.
- 구조화되고 현실적인 여행 계획을 JSON 형식으로 반환합니다.
실제 사례:
친구들과 졸업 여행을 계획 중인 사용자가 "Goa, 5일, ₹25,000, 예산 중심 스타일, 해변/음식/밤문화에 관심 있음"이라는 요청을 보내면, 스쿠터 대여 조언, 비치 쉑(beach shack) 추천, 안전 팁 등을 포함한 완벽한 일정을 단 몇 초 만에 돌려받을 수 있습니다.
2. 프로젝트 설정 및 의존성 (Dependencies)
기초부터 시작해 보겠습니다 — pom.xml 파일입니다. 이것을 프로젝트를 위한 레시피 카드라고 생각하세요. Maven에게 어떤 재료(라이브러리)가 필요한지 알려줍니다.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
...
주요 의존성 설명:
| 의존성 (Dependency) | 용도 (Purpose) | 실생활 비유 (Real-World Analogy) |
|---|---|---|
spring-boot-starter-web | REST API 구축 | 레스토랑 주방을 갖추는 것과 같습니다 — HTTP 요청을 처리하기 위한 모든 도구를 제공합니다 |
| ... |
실제 시나리오 (Real-world scenario):
실제 스타트업에서는 Azure OpenAI를 OpenAI로 직접 교체하거나, 인증을 위해 Spring Security를 추가할 수도 있습니다. 모듈형 의존성 설계 덕분에 코드를 다시 작성하지 않고도 컴포넌트를 교체할 수 있습니다.
3. 애플리케이션 진입점 (The Application Entry Point)
@SpringBootApplication
public class SpringAiTravelPlannerApplication {
public static void main(String[] args) {
...
이것은 애플리케이션의 **점화 스위치 (ignition switch)**입니다. @SpringBootApplication은 다음 세 가지를 결합한 편의 어노테이션 (convenience annotation)입니다:
- @Configuration — 이 클래스를 빈 (bean) 정의의 소스로 표시합니다.
- @EnableAutoConfiguration — 의존성에 따라 컴포넌트를 자동으로 구성하도록 Spring에 지시합니다.
- @ComponentScan — Spring이 이 패키지와 하위 패키지에서 컴포넌트를 스캔하도록 지시합니다.
실생활 비유 (Real-world analogy):
@SpringBootApplication을 자동차 점화 장치에 열쇠를 돌리는 것에 비유해 보세요. 한 번의 동작이 연쇄 반응을 일으킵니다: 연료 펌프가 활성화되고, 스타터 모터가 회전하며, 엔진 실린더가 점화되어 자동차가 살아 움직이게 됩니다. 이와 유사하게, 이 하나의 어노테이션이 Spring이 전체 애플리케이션을 스캔, 구성 및 부트스트랩 (bootstrap) 하도록 트리거합니다.
4. 요청 및 응답 모델 (Request & Response Models, DTOs)
DTO (Data Transfer Objects, 데이터 전송 객체)는 클라이언트와 서버 간에 데이터를 전달하는 일반적인 Java 클래스입니다. 레스토랑의 주문서나 송장와 같습니다.
4.1 TravelPlanRequest — 주문서 (The Order Form)
@Data
@Builder
@NoArgsConstructor
...
실생활 예시를 통한 어노테이션 설명:
-
@NotBlank— "목적지를 입력할 수 없습니다 (Destination cannot be blank)"목적지를 지정하지 않고 항공권을 예약한다고 상상해 보세요. 항공사는 당신을 어디로 보내야 할지 모를 것입니다!
-
@Min/@Max— 여행 기간 1일에서 30일 사이0일간의 여행이나 100일간의 여행은 실질적으로 의미가 없을 수 있습니다. 이러한 제약 조건은 데이터를 현실적으로 유지해 줍니다.
-
@Positive— 예산은 0보다 커야 함(안타깝게도!) 마이너스 금액으로는 여행할 수 없습니다.
-
**Lombok
@Data**는 getter, setter,toString,equals,hashCode를 자동으로 생성합니다.Lombok이 없다면 약 50줄의 상용구 코드 (Boilerplate code)를 작성해야 했을 것입니다. Lombok을 사용하면 어노테이션 하나면 충분합니다.
실제 검증 시나리오 (Real-world validation scenarios):
MakeMyTrip과 같은 여행 예약 웹사이트도 유사한 검증을 사용합니다. "0일 여행"이나 "마이너스 예산"을 검색할 수 없도록 하는 식입니다. 이러한 검증은 데이터베이스나 AI 서비스에 도달하기 전에 오류를 잡아냅니다.
4.2 TravelPlanResponse — 인보이스 (The Invoice)
응답 모델 (Response model)은 AI가 풍부하고 구조화된 여행 계획을 반환하기 때문에 더 복잡합니다. 여기에는 중첩 클래스 (Nested classes)가 포함됩니다:
public class TravelPlanResponse {
private String destination;
private String tripOverview;
...
중첩 클래스 (Nested classes):
DayItinerary— 각 날짜의 일차(day number), 제목, 활동, 식사 및 숙박 정보를 포함합니다.Meals— 아침, 점심, 저녁 식사 제안을 포함합니다.EstimatedBudget— 카테고리별 상세 내역(숙박, 음식, 교통 등)이 포함된 총 예산입니다.
응답의 실제 예시:
AI 여행 에이전트에게 "5일간의 고아(Goa) 여행"을 요청하면, 응답에는 특정 해변 이름(Baga, Anjuna, Vagator), 정확한 식당, 스쿠터 대여 가격(₹300-500/일), 그리고 "방수 휴대폰 파우치"와 같은 준비물까지 포함됩니다. 이 모든 것은 AI에 의해 동적으로 생성됩니다.
5. Spring AI를 활용한 프롬프트 엔지니어링 (Prompt Engineering)
프롬프트 엔지니어링 (Prompt engineering)은 AI 모델을 위한 지침을 정교하게 만드는 기술입니다. 바로 여기서 마법이 일어납니다.
5.1 프롬프트 템플릿 파일 (The Prompt Template File)
src/main/resources/prompts/travel-plan-prompt.st 파일에는 다음과 같이 작성되어 있습니다:
{destination} 목적지로 {days}일 동안 ₹{budget} 예산에 맞춘 상세한 여행 계획을 생성해 주세요.
여행 스타일: {travelStyle}
...
{variable} 구문은 Spring AI의 PromptTemplate에서 사용하는 방식입니다. 변수들은 런타임(Runtime) 시점에 실제 값으로 교체됩니다.
이 방식이 강력한 이유:
프롬프트를 Java 코드 내에 하드코딩(Hardcoding)하는 대신, 별도의
.st파일로 관리합니다. 이는 개발자가 아닌 사람(콘텐츠 작성자나 도메인 전문가 등)이 코드 수정 없이 프롬프트를 미세 조정할 수 있음을 의미하며, 실제 운영 팀에서 생산성을 크게 높여주는 요소입니다.
5.2 PromptTemplateConfig — 템플릿 연결하기
@Configuration
public class PromptTemplateConfig {
@Bean
...
이 코드는 클래스패스(Classpath)로부터 프롬프트 템플릿 파일을 로드하는 Spring 빈(Bean)을 생성합니다. @Bean 어노테이션을 통해 이 Resource를 다른 곳에서 의존성 주입(Dependency Injection)이 가능하도록 만듭니다.
실제 사례 비유:
이를 회사의 문서 관리 시스템에 템플릿 문서를 등록하는 것에 비유할 수 있습니다. 템플릿이 필요한 사람은 폴더를 뒤지는 대신 시스템에서 바로 요청하여 사용할 수 있습니다.
5.3 TravelPromptBuilder — 프롬프트 구축하기
@Component
public class TravelPromptBuilder {
...
프롬프트 구조 분석:
| 구성 요소 | 목적 | 실제 사례 비유 |
|---|---|---|
| 시스템 메시지 (System Message) | AI의 역할과 행동 양식을 설정 | 여행 상담원이 고객과 대화하기 전에 브리핑을 받는 것과 같습니다: "당신은 저가 여행 전문가이며, 항상 현지 음식을 추천하고, 존재하지 않는 명소를 지어내지 마세요" |
| ... |
실제 시나리오:
여행 기술 스타트업은 어떤 시스템 프롬프트가 더 많은 예약을 생성하는지 확인하기 위해 서로 다른 프롬프트로 A/B 테스트를 진행할 수 있습니다. 프롬프트를 파일로 유지함으로써, 엔지니어가 아닌 사람들도 배포 주기(Deployment Cycle) 없이 실험을 수행할 수 있습니다.
6. 서비스 레이어 (The Service Layer) — 핵심 비즈니스 로직
TravelPlanService는 애플리케이션의 두뇌 역할을 합니다. 전체 흐름을 조율(Orchestrate)합니다.
@Service
public class TravelPlanService {
...
생성자 주입 (Constructor Injection)
Spring은 application.yml을 통해 설정된 ChatClient.Builder, 우리의 TravelPromptBuilder, 그리고 Jackson의 ObjectMapper를 자동으로 제공합니다. 이것이 바로 의존성 주입 (Dependency Injection) 입니다. Spring이 모든 것을 하나로 연결해 줍니다.
실생활 비유:
요리사가 직접 채소를 재배하거나 접시를 만들지 않는 레스토랑을 상상해 보세요. 재료는 공급업체(Spring)로부터 이미 준비된 상태로 도착합니다. 요리사는 오직 요리에만 집중하면 됩니다.
generate 메서드 — 단계별 설명
public TravelPlanResponse generateTravelPlan(TravelPlanRequest request) {
// 1단계: 사용자 요청으로부터 프롬프트(Prompt) 생성
Prompt prompt = promptBuilder.buildTravelPlanPrompt(request);
...
실제 사례를 통한 단계별 과정:
- 사용자 전송:
{destination: "Goa", days: 5, budget: 25000, travelStyle: "Budget", interests: ["Beaches", "Food", "Nightlife"]} - 프롬프트 생성: 시스템 메시지 + "Goa를 위한 5일간의 상세 여행 계획을 ₹25000 예산에 맞춰 생성해 주세요..."
- AI 응답: 5일간의 일정, 예산 내역, 안전 팁 등이 포함된 2,000단어 분량의 JSON 문서
- JSON 파싱 (Parsing): 가공되지 않은 문자열(Raw string)이
TravelPlanResponseJava 객체로 변환됨 - 응답 전송: 포맷팅된 여행 계획이 HTTP JSON 응답으로 반환됨
extractJson 메서드 — AI 출력 처리
private String extractJson(String text) {
int start = text.indexOf('{');
int end = text.lastIndexOf('}');
...
이 메서드가 필요한 이유:
AI 모델은 때때로 JSON을 마크다운 코드 블록(
json ...)으로 감싸거나, 앞뒤에 설명 텍스트를 추가하곤 합니다. 이 메서드는 금을 채취할 때 금괴만 남기고 나머지는 걸러내는 것처럼, JSON 구조를 제외한 모든 것을 제거합니다.
실제 사례:
이 메서드가 없다면, AI는 다음과 같이 응답할 수 있습니다:
여기 당신의 여행 계획입니다!{"destination": "Goa", ...}즐거운 여행 되세요!
extractJson메서드는 이를 유효한 JSON으로 정제합니다.
extractErrorMessage 메서드 — 사용자 친화적인 에러 메시지
private String extractErrorMessage(Exception ex) {
String message = ex.getMessage();
if (message != null) {
...
이 메서드는 난해한 Azure 에러 메시지를 사람이 읽을 수 있는 메시지로 변환합니다. 사용자에게 스택 트레이스 (Stack Trace)를 노출하는 대신, 명확하고 실행 가능한 피드백을 제공합니다.
7. 컨트롤러 레이어 (Controller Layer) — REST API
컨트롤러는 애플리케이션의 정문 (front door) 역할을 합니다. 즉, HTTP 요청을 받고 응답을 보냅니다.
@RestController
@RequestMapping("/api/v1")
@Tag(name = "Travel Plan", description = "AI-powered travel plan generation endpoints")
...
@RestController—@Controller와@ResponseBody를 결합한 것으로, 모든 메서드가 자동으로 JSON을 반환합니다.@RequestMapping("/api/v1")— 모든 엔드포인트 (Endpoint)가/api/v1으로 시작합니다.@Tag— Swagger 문서 그룹화를 위해 사용됩니다.
엔드포인트 (Endpoint)
@PostMapping("/travel-plan")
public ResponseEntity<TravelPlanResponse> generateTravelPlan(
@Valid @RequestBody TravelPlanRequest request) {
...
주요 어노테이션 (Annotations):
| 어노테이션 | 목적 | 실생활 비유 |
|---|---|---|
@PostMapping | HTTP POST 요청을 이 메서드에 매핑합니다 | "새 여행 계획 요청"이라고 적힌 특정 우체통과 같습니다 |
| ... |
실제 API 호출:
POST http://localhost:8080/api/v1/travel-plan
Content-Type: application/json
...
컨트롤러에는 또한 상세한 OpenAPI 어노테이션(@Operation, @ApiResponses, @ExampleObject)이 포함되어 있어 아름다운 Swagger 문서를 자동으로 생성합니다. 이를 통해 프론트엔드 개발자가 API를 쉽게 이해하고 테스트할 수 있습니다.
8. 예외 처리 (Exception Handling) 및 에러 응답
이 프로젝트는 중앙 집중식 예외 처리를 위해 Spring의 @RestControllerAdvice를 사용합니다. 즉, 한 곳에서 모든 에러를 처리합니다.
@RestControllerAdvice
public class GlobalExceptionHandler {
ProblemDetail — 현대적인 에러 응답
Spring Boot 3에서는 표준화된 에러 응답을 위해 ProblemDetail (RFC 9457)을 도입했습니다.
유효성 검사 에러 (400 Bad Request):
{
"type": "urn:problem-type:validation",
"title": "Validation Failed",
...
AI 서비스 에러 (503 Service Unavailable):
{
"type": "urn:problem-type:ai-service",
"title": "AI Service Error",
...
중앙 집중식 에러 핸들링 (Centralized error handling)이 중요한 이유:
10개의 서로 다른 컨트롤러(Controller)가 각각 다르게 에러를 처리한다고 상상해 보세요. 어떤 것은 400을 반환하고, 어떤 것은 "bad request"를 반환하며, 어떤 것은 난해한 스택 트레이스 (Stack trace)를 반환합니다.
@RestControllerAdvice는 모든 에러 응답이 동일한 형식을 따르도록 보장하며, 이는 에러 처리 로직을 구축하는 프론트엔드 개발자에게 매우 중요합니다.
실제 사례 비유:
이것을 쇼핑몰의 고객 서비스 센터라고 생각하세요. 각 매장이 불만 사항을 제각각 처리하는 대신, 표준 절차를 갖춘 하나의 중앙 데스크가 있는 것과 같습니다. 고객은 정확히 어디로 가야 할지, 어떤 형식의 응답을 기대할 수 있는지 알 수 있습니다.
9. OpenAPI / Swagger 설정
OpenApiConfig 클래스는 자동 생성된 API 문서 (API documentation)를 커스텀합니다:
@Configuration
public class OpenApiConfig {
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기