🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

실전 Function Calling 패턴 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2026. 2. 1. · 7 Views

실전 Function Calling 패턴 완벽 가이드

Spring AI에서 Function Calling을 실전에 적용하는 핵심 패턴들을 다룹니다. 복잡한 파라미터 처리부터 멀티 함수 호출, 에러 핸들링까지 실무에서 바로 쓸 수 있는 패턴을 배웁니다.


목차

  1. 복잡한_파라미터_처리
  2. 멀티_함수_호출
  3. 함수_체인_패턴
  4. 에러_핸들링_전략
  5. 데이터베이스_연동
  6. 실전_주문_관리_시스템

1. 복잡한 파라미터 처리

김개발 씨는 AI 챗봇에 날씨 조회 기능을 추가했습니다. 단순히 도시 이름만 받으면 되는 줄 알았는데, 기획팀에서 요청이 들어왔습니다.

"날짜 범위도 지정하고, 섭씨/화씨 단위도 선택하고, 상세 정보 포함 여부도 결정할 수 있어야 해요."

복잡한 파라미터 처리란 단순한 문자열이나 숫자를 넘어서 여러 필드를 가진 객체, 열거형, 중첩 구조 등을 Function Calling에서 다루는 것입니다. 마치 온라인 쇼핑몰의 상세 검색 필터처럼, 다양한 조건을 조합해서 정확한 결과를 얻어내는 것과 같습니다.

이를 잘 활용하면 AI가 사용자의 복잡한 요청도 정확하게 이해하고 처리할 수 있습니다.

다음 코드를 살펴봅시다.

// 복잡한 요청 객체 정의
public record WeatherRequest(
    String city,                    // 필수: 도시명
    @Nullable LocalDate startDate,  // 선택: 시작 날짜
    @Nullable LocalDate endDate,    // 선택: 종료 날짜
    TemperatureUnit unit,           // 열거형: 온도 단위
    boolean includeDetails          // 상세 정보 포함 여부
) {
    public enum TemperatureUnit { CELSIUS, FAHRENHEIT }
}

@Bean
@Description("지정된 조건으로 날씨 정보를 조회합니다")
public Function<WeatherRequest, WeatherResponse> getWeather() {
    return request -> weatherService.fetch(request);
}

김개발 씨는 입사 6개월 차에 접어든 주니어 개발자입니다. 회사에서 새로 도입한 AI 챗봇 프로젝트에 투입되어 Function Calling 기능을 담당하게 되었습니다.

처음에는 간단했습니다. 도시 이름 하나만 받아서 날씨를 조회하면 됐으니까요.

그런데 기획팀의 요구사항이 점점 복잡해졌습니다. 날짜 범위를 지정하고 싶다, 온도 단위를 선택하고 싶다, 습도나 풍속 같은 상세 정보는 필요할 때만 보고 싶다.

김개발 씨는 고민에 빠졌습니다. 파라미터가 6개나 되는데, 이걸 어떻게 깔끔하게 처리하지?

선배 개발자 박시니어 씨가 조언을 건넸습니다. "Java의 Record를 써보세요.

복잡한 파라미터를 하나의 객체로 묶으면 훨씬 관리하기 쉬워집니다." Record는 마치 택배 상자와 같습니다. 여러 물건을 낱개로 보내면 분실 위험도 있고 받는 사람도 헷갈립니다.

하지만 하나의 상자에 담아 보내면 안전하고 명확하죠. WeatherRequest라는 상자 안에 city, startDate, endDate, unit, includeDetails라는 물건들을 담아 보내는 것입니다.

여기서 주목할 점은 @Nullable 어노테이션입니다. 시작 날짜와 종료 날짜는 선택 사항입니다.

사용자가 "서울 날씨 알려줘"라고만 말하면 날짜 없이도 동작해야 합니다. @Nullable을 붙이면 Spring AI가 이 필드는 필수가 아니라고 AI 모델에게 알려줍니다.

TemperatureUnit 열거형도 중요합니다. 문자열로 "celsius"나 "fahrenheit"를 받으면 오타가 생길 수 있습니다.

하지만 열거형을 사용하면 CELSIUS와 FAHRENHEIT 두 가지 값만 허용됩니다. AI 모델도 이 제약을 이해하고, 사용자가 "섭씨로 알려줘"라고 하면 정확하게 CELSIUS를 선택합니다.

코드의 @Description 어노테이션은 AI 모델에게 이 함수가 무엇을 하는지 설명합니다. 마치 도서관의 책 설명처럼, AI가 어떤 상황에서 이 함수를 호출해야 할지 판단하는 기준이 됩니다.

실제로 이 코드가 동작하면 어떻게 될까요? 사용자가 "내일부터 3일간 뉴욕 날씨를 화씨로 상세하게 알려줘"라고 말하면, AI 모델은 이를 분석해서 WeatherRequest 객체를 생성합니다.

city는 "New York", startDate는 내일 날짜, endDate는 3일 후, unit은 FAHRENHEIT, includeDetails는 true로 설정됩니다. 주의할 점이 있습니다.

Record의 필드 이름은 명확해야 합니다. AI 모델은 필드 이름을 보고 어떤 값을 넣어야 할지 판단하기 때문입니다.

startDate라고 쓰면 이해하지만, sd라고 줄여 쓰면 AI가 혼란스러워할 수 있습니다. 김개발 씨는 박시니어 씨의 조언대로 Record를 적용했습니다.

6개의 파라미터가 하나의 깔끔한 객체로 정리되니, 코드도 읽기 쉬워지고 테스트도 간편해졌습니다. 무엇보다 새로운 필드를 추가해야 할 때도 Record에 한 줄만 추가하면 됩니다.

실전 팁

💡 - 필드 이름은 AI가 이해하기 쉽게 명확하고 서술적으로 작성하세요

  • 선택적 필드에는 반드시 @Nullable을 붙여 필수 여부를 명시하세요
  • 제한된 선택지가 있다면 문자열 대신 열거형을 사용하세요

2. 멀티 함수 호출

김개발 씨가 만든 챗봇이 인기를 끌기 시작했습니다. 그런데 사용자들이 이상한 질문을 합니다.

"서울 날씨랑 내 일정 같이 알려줘." 날씨 함수와 일정 함수를 따로따로 호출해야 하나? 아니면 AI가 알아서 둘 다 호출할 수 있나?

멀티 함수 호출이란 AI 모델이 하나의 요청에 대해 여러 개의 함수를 동시에 또는 순차적으로 호출하는 것입니다. 마치 비서에게 "커피 사오면서 우편물도 확인해줘"라고 부탁하면 두 가지 일을 한 번에 처리하는 것과 같습니다.

이 기능을 활용하면 복잡한 사용자 요청도 한 번의 대화로 해결할 수 있습니다.

다음 코드를 살펴봅시다.

@Configuration
public class MultiFunctionConfig {

    @Bean
    @Description("도시의 현재 날씨를 조회합니다")
    public Function<CityRequest, WeatherInfo> getWeather(WeatherService service) {
        return request -> service.getCurrentWeather(request.city());
    }

    @Bean
    @Description("사용자의 오늘 일정을 조회합니다")
    public Function<UserRequest, List<Schedule>> getSchedule(ScheduleService service) {
        return request -> service.getTodaySchedule(request.userId());
    }

    @Bean
    @Description("특정 장소까지의 예상 이동 시간을 계산합니다")
    public Function<RouteRequest, Duration> getRouteTime(MapService service) {
        return request -> service.calculateTime(request.from(), request.to());
    }
}

김개발 씨의 챗봇은 이제 날씨 조회 기능만 있는 게 아닙니다. 일정 관리, 길찾기, 뉴스 검색 등 다양한 기능이 추가되었습니다.

그런데 문제가 생겼습니다. 사용자들이 여러 기능을 한 번에 요청하는 것입니다.

"내일 부산 출장인데, 부산 날씨랑 회의 일정이랑 공항까지 가는 시간 알려줘." 이 요청을 처리하려면 세 가지 함수를 호출해야 합니다. 부산 날씨 조회, 내일 일정 조회, 공항까지 이동 시간 계산.

예전 같았으면 사용자에게 "한 번에 하나씩만 물어봐 주세요"라고 답했겠지만, 그건 좋은 사용자 경험이 아닙니다. 다행히 Spring AI의 멀티 함수 호출 기능이 이 문제를 해결해줍니다.

멀티 함수 호출은 마치 능숙한 요리사가 여러 요리를 동시에 하는 것과 같습니다. 파스타 면을 삶으면서 소스도 만들고, 샐러드도 준비합니다.

각각 따로 하면 시간이 오래 걸리지만, 동시에 진행하면 효율적이죠. AI 모델도 마찬가지입니다.

여러 함수가 서로 의존하지 않는다면 동시에 호출해서 결과를 모아줍니다. 위 코드를 보면 세 개의 함수가 각각 @Bean으로 등록되어 있습니다.

getWeather, getSchedule, getRouteTime. 이 함수들은 서로 독립적입니다.

날씨를 알아야 일정을 조회할 수 있는 게 아니니까요. 핵심은 @Description 어노테이션입니다.

AI 모델은 이 설명을 읽고 어떤 함수를 호출해야 할지 결정합니다. "도시의 현재 날씨를 조회합니다"라는 설명을 보고, 사용자가 날씨를 물어보면 이 함수를 선택합니다.

설명이 명확할수록 AI의 판단도 정확해집니다. 실제 동작 과정을 살펴봅시다.

사용자가 복합 질문을 하면, AI 모델은 먼저 어떤 함수들이 필요한지 분석합니다. 그리고 필요한 함수들의 파라미터를 추출합니다.

부산을 날씨 함수에, 사용자 ID를 일정 함수에, 현재 위치와 공항을 이동 시간 함수에 전달합니다. Spring AI는 이 함수 호출 요청들을 받아서 실행하고, 결과를 다시 AI 모델에게 전달합니다.

AI 모델은 세 가지 결과를 종합해서 사용자에게 자연스러운 답변을 만들어냅니다. "내일 부산 날씨는 맑고 기온은 15도입니다.

오전 10시에 거래처 미팅이 있고, 공항까지는 약 40분 걸립니다." 여기서 중요한 점은 함수 간의 독립성입니다. 만약 함수들이 서로 의존한다면, 예를 들어 일정을 조회해야 목적지를 알 수 있다면, AI 모델은 이를 순차적으로 처리합니다.

먼저 일정을 조회하고, 그 결과에서 목적지를 추출한 다음, 이동 시간을 계산합니다. 주의할 점도 있습니다.

너무 많은 함수를 등록하면 AI 모델이 혼란스러워할 수 있습니다. 비슷한 기능의 함수가 여러 개 있으면 어떤 것을 호출해야 할지 판단하기 어렵습니다.

함수의 역할을 명확하게 구분하고, @Description을 정확하게 작성하는 것이 중요합니다. 김개발 씨는 멀티 함수 호출을 적용한 후, 사용자 만족도가 크게 올랐습니다.

복잡한 질문에도 한 번에 답변할 수 있게 되었으니까요. 사용자는 마치 진짜 비서와 대화하는 것 같다고 느끼게 되었습니다.

실전 팁

💡 - 함수의 @Description은 AI가 선택의 기준으로 삼으므로 명확하고 구체적으로 작성하세요

  • 서로 독립적인 함수들은 병렬로 호출되어 응답 시간이 단축됩니다
  • 비슷한 역할의 함수가 있다면 통합하거나 이름과 설명으로 차이를 분명히 하세요

3. 함수 체인 패턴

박시니어 씨가 김개발 씨에게 새로운 과제를 던졌습니다. "사용자가 상품을 검색하면, 재고를 확인하고, 재고가 있으면 가격을 조회해서 알려주는 기능을 만들어봐." 김개발 씨는 고민에 빠졌습니다.

세 단계가 순서대로 실행되어야 하는데, 이걸 어떻게 연결하지?

함수 체인 패턴은 하나의 함수 실행 결과가 다음 함수의 입력이 되는 연쇄적인 호출 구조입니다. 마치 공장의 조립 라인처럼, 각 단계가 순서대로 진행되며 이전 단계의 결과물이 다음 단계의 재료가 됩니다.

AI 모델이 이 흐름을 자동으로 파악하고 순차적으로 함수들을 호출합니다.

다음 코드를 살펴봅시다.

// 1단계: 상품 검색
@Bean
@Description("상품명으로 상품 정보를 검색합니다")
public Function<ProductSearchRequest, ProductInfo> searchProduct(ProductService service) {
    return request -> service.findByName(request.productName());
}

// 2단계: 재고 확인 (상품 ID 필요)
@Bean
@Description("상품 ID로 현재 재고 수량을 확인합니다")
public Function<StockCheckRequest, StockInfo> checkStock(InventoryService service) {
    return request -> service.getStock(request.productId());
}

// 3단계: 가격 조회 (상품 ID와 수량 필요)
@Bean
@Description("상품 ID와 수량으로 총 가격을 계산합니다")
public Function<PriceRequest, PriceInfo> calculatePrice(PricingService service) {
    return request -> service.calculate(request.productId(), request.quantity());
}

김개발 씨는 처음에 세 가지 함수를 따로따로 만들어서 등록했습니다. 그런데 테스트를 해보니 AI가 이상하게 동작했습니다.

재고를 확인하려면 상품 ID가 필요한데, 사용자는 상품 이름만 알고 있습니다. AI가 어떻게 상품 ID를 알아낼 수 있을까요?

박시니어 씨가 설명했습니다. "AI 모델은 생각보다 똑똑해요.

함수들의 입력과 출력을 분석해서 어떤 순서로 호출해야 하는지 스스로 파악합니다." 함수 체인은 마치 릴레이 경주와 같습니다. 첫 번째 주자가 바통을 들고 달리고, 다음 주자에게 바통을 넘겨줍니다.

바통 없이는 다음 주자가 달릴 수 없죠. 여기서 바통은 데이터입니다.

searchProduct 함수가 ProductInfo를 반환하고, 이 안에 담긴 productId가 다음 함수의 입력이 됩니다. 코드를 자세히 보겠습니다.

searchProduct 함수는 상품 이름을 받아서 ProductInfo를 반환합니다. 이 ProductInfo 안에는 productId, productName, category 등의 정보가 들어있습니다.

checkStock 함수는 productId를 입력으로 받습니다. AI 모델은 이 연결고리를 파악합니다.

사용자가 "에어팟 프로 5개 사려는데 가격이 얼마야?"라고 물으면, AI 모델은 다음과 같이 생각합니다. "가격을 계산하려면 calculatePrice를 호출해야 해.

그런데 productId가 필요하네. productId를 얻으려면 searchProduct를 먼저 호출해야겠어." 이것이 암묵적 체인입니다.

개발자가 명시적으로 순서를 지정하지 않아도, AI가 데이터 흐름을 분석해서 올바른 순서로 함수를 호출합니다. 하지만 모든 상황에서 AI가 완벽하게 판단하지는 못합니다.

특히 복잡한 비즈니스 로직이 있을 때는 명시적 체인이 필요할 수 있습니다. 예를 들어, 재고가 0이면 가격 조회를 하지 말아야 하는 경우가 있습니다.

이런 조건부 로직은 함수 내부에서 처리하거나, 별도의 오케스트레이션 레이어를 두는 것이 좋습니다. 실무에서 자주 보는 체인 패턴을 살펴봅시다.

주문 처리 시스템에서는 이런 흐름이 일반적입니다. 회원 정보 조회 → 배송지 확인 → 결제 수단 검증 → 주문 생성.

각 단계가 이전 단계의 결과에 의존합니다. 회원이 존재해야 배송지를 확인할 수 있고, 배송지가 유효해야 결제를 진행할 수 있습니다.

주의해야 할 점이 있습니다. 체인이 너무 길어지면 에러 발생 시 어디서 문제가 생겼는지 파악하기 어렵습니다.

또한 중간 단계에서 실패하면 전체 체인이 멈춥니다. 따라서 각 함수에서 적절한 에러 처리를 하고, 실패 시 의미 있는 메시지를 반환하는 것이 중요합니다.

김개발 씨는 함수 체인 패턴을 적용한 후, 복잡한 비즈니스 로직도 깔끔하게 처리할 수 있게 되었습니다. 각 함수는 자신의 역할만 충실히 하고, AI가 전체 흐름을 조율하니 코드도 단순해지고 유지보수도 쉬워졌습니다.

실전 팁

💡 - 함수의 반환 타입에 다음 함수가 필요로 하는 정보를 포함하세요

  • 체인이 3단계를 넘어가면 중간 결과를 로깅하여 디버깅에 대비하세요
  • 조건부 분기가 필요하면 함수 내부에서 처리하거나 별도 오케스트레이터를 두세요

4. 에러 핸들링 전략

금요일 오후, 김개발 씨의 챗봇이 갑자기 이상하게 동작하기 시작했습니다. 외부 API가 응답하지 않는 것입니다.

사용자에게 "에러가 발생했습니다"라고만 답하니 항의가 빗발쳤습니다. 박시니어 씨가 말했습니다.

"에러 처리는 사용자 경험의 핵심이야. 뭐가 잘못됐는지, 어떻게 해야 하는지 안내해줘야 해."

에러 핸들링 전략은 Function Calling 중 발생하는 다양한 오류 상황을 우아하게 처리하는 방법입니다. 마치 숙련된 안내원이 길을 잃은 손님에게 다른 경로를 제안하는 것처럼, 에러가 발생해도 사용자에게 유용한 정보를 제공하고 대안을 안내해야 합니다.

잘 설계된 에러 핸들링은 서비스의 신뢰도를 높입니다.

다음 코드를 살펴봅시다.

@Bean
@Description("상품 재고를 조회합니다")
public Function<StockRequest, StockResult> checkStock(InventoryService service) {
    return request -> {
        try {
            int stock = service.getStock(request.productId());
            return StockResult.success(stock);
        } catch (ProductNotFoundException e) {
            // 명확한 에러 메시지를 AI에게 전달
            return StockResult.error("PRODUCT_NOT_FOUND",
                "상품 ID " + request.productId() + "를 찾을 수 없습니다");
        } catch (ServiceUnavailableException e) {
            return StockResult.error("SERVICE_UNAVAILABLE",
                "재고 시스템이 일시적으로 점검 중입니다. 잠시 후 다시 시도해주세요");
        } catch (Exception e) {
            log.error("재고 조회 실패: {}", request.productId(), e);
            return StockResult.error("UNKNOWN_ERROR",
                "재고 조회 중 문제가 발생했습니다");
        }
    };
}

김개발 씨는 처음에 에러가 발생하면 그냥 예외를 던졌습니다. 간단하니까요.

그런데 문제가 생겼습니다. 예외가 발생하면 AI 모델이 받는 응답이 단절됩니다.

AI는 무슨 일이 일어났는지 모르고, 사용자에게 "죄송합니다, 요청을 처리할 수 없습니다"라는 무미건조한 답변만 합니다. 박시니어 씨가 핵심을 짚었습니다.

"에러도 정보야. AI에게 에러 상황을 제대로 전달하면, AI가 사용자에게 훨씬 나은 안내를 해줄 수 있어." 에러 핸들링은 마치 병원의 접수 창구와 같습니다.

환자가 전문의 진료를 원하는데 해당 의사가 휴가 중이라면, "안 됩니다"라고만 하는 게 아니라 "해당 전문의는 다음 주 월요일에 복귀합니다. 대신 다른 전문의 진료를 받으시거나, 월요일에 예약을 잡으시겠어요?"라고 안내합니다.

에러 상황에서도 대안을 제시하는 것입니다. 코드를 보면 StockResult라는 통합 결과 객체를 사용합니다.

이 객체는 성공 시에는 재고 수량을, 실패 시에는 에러 코드와 메시지를 담습니다. 핵심은 예외를 던지지 않고 결과 객체로 반환한다는 점입니다.

ProductNotFoundException이 발생하면 "PRODUCT_NOT_FOUND" 코드와 함께 구체적인 메시지를 반환합니다. AI 모델은 이 메시지를 보고 사용자에게 "해당 상품을 찾을 수 없습니다.

상품명을 다시 확인해 주시겠어요?"라고 자연스럽게 안내할 수 있습니다. ServiceUnavailableException은 외부 서비스 장애 상황입니다.

이때는 사용자 탓이 아니므로 "시스템 점검 중"이라는 친절한 안내와 함께 "잠시 후 다시 시도"를 권합니다. AI는 이를 바탕으로 "현재 재고 시스템이 점검 중이에요.

5분 후에 다시 물어봐 주시겠어요?"라고 답할 수 있습니다. 마지막 catch 블록은 예상치 못한 에러를 처리합니다.

여기서 중요한 것은 로깅입니다. 개발자는 로그를 통해 무슨 문제인지 파악할 수 있어야 합니다.

하지만 사용자에게는 기술적인 세부 사항을 노출하지 않고 일반적인 메시지만 보여줍니다. 에러 코드를 사용하는 이유가 있습니다.

AI 모델이 에러의 종류를 구분할 수 있게 해줍니다. "PRODUCT_NOT_FOUND"면 상품명을 다시 물어보고, "SERVICE_UNAVAILABLE"이면 재시도를 권하고, "INSUFFICIENT_STOCK"이면 다른 상품을 추천할 수 있습니다.

에러의 성격에 따라 다른 대응이 가능해지는 것입니다. 실무에서는 폴백 전략도 함께 사용합니다.

메인 API가 실패하면 캐시된 데이터를 반환하거나, 백업 서비스를 호출하는 것입니다. "정확한 실시간 재고는 확인이 어렵지만, 마지막으로 확인된 재고는 15개입니다"처럼 부분적인 정보라도 제공하는 것이 아무것도 없는 것보다 낫습니다.

김개발 씨는 에러 핸들링을 개선한 후, 장애 상황에서도 사용자 불만이 크게 줄었습니다. 에러는 피할 수 없지만, 어떻게 대응하느냐가 서비스 품질을 결정한다는 것을 배웠습니다.

실전 팁

💡 - 예외를 던지지 말고 에러 정보를 담은 결과 객체를 반환하세요

  • 에러 코드를 정의하여 AI가 상황별로 다른 대응을 할 수 있게 하세요
  • 내부 기술 정보는 로그에만 남기고 사용자에게는 친절한 메시지를 보여주세요

5. 데이터베이스 연동

프로젝트가 확장되면서 김개발 씨는 새로운 요구사항을 받았습니다. "챗봇이 데이터베이스를 직접 조회할 수 있었으면 좋겠어요.

사용자가 자기 주문 내역이나 포인트 잔액을 물어볼 수 있게요." 김개발 씨는 걱정이 됐습니다. AI가 데이터베이스에 접근한다니, 보안은 괜찮을까?

데이터베이스 연동은 Function Calling을 통해 AI가 실제 데이터를 조회하고 조작할 수 있게 하는 것입니다. 마치 창구 직원이 고객의 요청에 따라 내부 시스템을 조회해서 정보를 알려주는 것과 같습니다.

단, AI에게 직접 SQL을 작성하게 하는 것이 아니라, 미리 정의된 안전한 함수를 통해서만 접근하도록 합니다.

다음 코드를 살펴봅시다.

@Bean
@Description("사용자의 최근 주문 내역을 조회합니다")
public Function<OrderHistoryRequest, List<OrderSummary>> getOrderHistory(
        OrderRepository orderRepository) {
    return request -> {
        // 현재 인증된 사용자 ID 검증
        Long userId = SecurityContextHolder.getContext().getUserId();
        if (!userId.equals(request.userId())) {
            throw new UnauthorizedException("다른 사용자의 주문 내역은 조회할 수 없습니다");
        }

        // 안전한 Repository 메서드 호출
        return orderRepository.findRecentOrders(userId, request.limit())
            .stream()
            .map(OrderSummary::from)  // 필요한 정보만 노출
            .toList();
    };
}

public record OrderSummary(String orderId, LocalDate orderDate,
                           String productName, int quantity, String status) {
    public static OrderSummary from(Order order) {
        return new OrderSummary(order.getId(), order.getOrderDate(),
            order.getProduct().getName(), order.getQuantity(), order.getStatus().name());
    }
}

김개발 씨의 걱정은 타당했습니다. AI에게 데이터베이스 접근 권한을 준다는 것은 양날의 검입니다.

잘 사용하면 강력한 기능이 되지만, 잘못 설계하면 보안 사고로 이어질 수 있습니다. 박시니어 씨가 핵심 원칙을 알려줬습니다.

"AI에게 SQL을 작성하게 하면 안 돼. AI는 정해진 함수만 호출하고, 그 함수 안에서 우리가 통제하는 거야." 데이터베이스 연동은 마치 은행 창구와 같습니다.

고객이 "제 잔액 좀 알려주세요"라고 하면, 창구 직원이 내부 시스템에서 조회해서 알려줍니다. 하지만 고객이 "다른 사람 계좌에서 돈 좀 빼주세요"라고 하면 당연히 거절합니다.

직원은 정해진 규칙 안에서만 움직입니다. Function Calling도 마찬가지입니다.

코드에서 가장 중요한 부분은 권한 검증입니다. request.userId()가 현재 로그인한 사용자의 ID와 같은지 확인합니다.

AI가 아무리 "다른 사용자의 주문을 조회해줘"라고 요청해도, 이 검증에서 막힙니다. 이것이 방어적 프로그래밍입니다.

OrderSummary 레코드도 주목해야 합니다. Order 엔티티에는 결제 정보, 배송 주소 상세, 내부 메모 등 민감한 정보가 있을 수 있습니다.

하지만 OrderSummary는 사용자에게 보여줘도 되는 정보만 담습니다. 이것이 데이터 필터링입니다.

AI에게 전달되는 데이터는 노출 가능한 것만이어야 합니다. Repository 메서드 이름도 봅시다.

findRecentOrders라는 명확한 이름의 메서드를 호출합니다. 이 메서드는 내부적으로 JPQL이나 QueryDSL로 안전하게 쿼리를 실행합니다.

AI가 직접 "SELECT * FROM orders WHERE..."를 작성하는 게 아닙니다. 파라미터 바인딩을 통해 SQL 인젝션 공격도 방지됩니다.

limit 파라미터도 중요합니다. 사용자가 "내 주문 전체 보여줘"라고 해도 무제한으로 반환하면 안 됩니다.

성능 문제도 있고, 너무 많은 데이터가 AI 컨텍스트에 들어가면 비용도 늘어납니다. 적절한 페이지네이션이 필요합니다.

실무에서는 읽기 전용쓰기 가능 함수를 구분합니다. 주문 조회는 읽기 전용이라 위험이 적지만, 주문 취소나 수정은 쓰기 작업입니다.

쓰기 작업에는 추가적인 검증이 필요합니다. "정말 취소하시겠습니까?"라는 확인 단계를 넣거나, 특정 조건(주문 후 1시간 이내 등)에서만 가능하게 합니다.

또 하나 고려할 것은 감사 로그입니다. AI를 통해 어떤 데이터가 조회되었는지 기록해두면, 문제가 생겼을 때 추적이 가능합니다.

"누가 언제 어떤 데이터를 조회했는가"를 알 수 있어야 합니다. 김개발 씨는 이런 보안 원칙들을 적용하여 데이터베이스 연동 기능을 안전하게 구현했습니다.

AI 챗봇이 개인화된 정보를 제공할 수 있게 되면서 사용자 만족도가 크게 올랐고, 보안 사고도 발생하지 않았습니다.

실전 팁

💡 - AI에게 raw SQL 접근을 주지 말고, 미리 정의된 Repository 메서드만 사용하세요

  • 민감한 필드는 DTO로 필터링하여 필요한 정보만 AI에게 전달하세요
  • 모든 데이터 접근에 권한 검증과 감사 로그를 추가하세요

6. 실전 주문 관리 시스템

김개발 씨는 지금까지 배운 모든 것을 종합할 기회를 얻었습니다. 회사에서 AI 기반 주문 관리 시스템을 새로 만들기로 한 것입니다.

상품 검색, 장바구니 관리, 주문 생성, 주문 조회까지 모든 기능을 AI 챗봇으로 처리해야 합니다. 박시니어 씨가 말했습니다.

"지금까지 배운 패턴들을 전부 활용해봐."

실전 주문 관리 시스템은 복잡한 파라미터, 멀티 함수 호출, 함수 체인, 에러 핸들링, 데이터베이스 연동을 모두 활용하는 종합 예제입니다. 마치 백화점의 원스톱 서비스처럼, 고객이 원하는 모든 쇼핑 관련 요청을 하나의 챗봇에서 처리할 수 있습니다.

이 시스템을 통해 실무에서 Function Calling을 어떻게 설계하고 구현하는지 배울 수 있습니다.

다음 코드를 살펴봅시다.

@Configuration
public class OrderManagementFunctions {

    @Bean
    @Description("상품을 검색합니다. 카테고리, 가격 범위, 정렬 기준을 지정할 수 있습니다")
    public Function<ProductSearchRequest, SearchResult> searchProducts(
            ProductService productService) {
        return request -> productService.search(request);
    }

    @Bean
    @Description("장바구니에 상품을 추가합니다")
    public Function<AddToCartRequest, CartResult> addToCart(CartService cartService) {
        return request -> cartService.addItem(
            getCurrentUserId(), request.productId(), request.quantity());
    }

    @Bean
    @Description("현재 장바구니 내용과 총액을 조회합니다")
    public Function<EmptyRequest, CartSummary> getCart(CartService cartService) {
        return request -> cartService.getCartSummary(getCurrentUserId());
    }

    @Bean
    @Description("장바구니의 상품으로 주문을 생성합니다")
    public Function<CreateOrderRequest, OrderResult> createOrder(OrderService orderService) {
        return request -> orderService.createFromCart(
            getCurrentUserId(), request.shippingAddressId(), request.paymentMethodId());
    }
}

김개발 씨는 드디어 지금까지 배운 모든 것을 실전에 적용할 때가 왔습니다. 주문 관리 시스템이라는 큰 프로젝트, 어디서부터 시작해야 할까요?

박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다. "먼저 사용자 시나리오를 생각해봐.

사용자가 뭘 하고 싶어할까?" 시나리오를 정리해봤습니다. 첫째, 상품을 검색하고 싶다.

둘째, 마음에 드는 상품을 장바구니에 담고 싶다. 셋째, 장바구니에 뭐가 담겼는지 확인하고 싶다.

넷째, 주문을 완료하고 싶다. 다섯째, 주문이 잘 됐는지 확인하고 싶다.

이 다섯 가지 시나리오를 지원하면 기본적인 쇼핑 경험이 완성됩니다. 각 시나리오가 하나의 함수가 됩니다.

이것이 단일 책임 원칙입니다. searchProducts 함수는 검색만, addToCart 함수는 장바구니 추가만, 각자 맡은 일만 합니다.

상품 검색 함수를 보면 복잡한 파라미터 처리가 적용되어 있습니다. ProductSearchRequest에는 키워드, 카테고리, 최소 가격, 최대 가격, 정렬 기준 등 다양한 필드가 있습니다.

사용자가 "3만원 이하 무선 이어폰 인기순으로 보여줘"라고 하면, AI가 이 조건들을 추출해서 요청 객체를 만듭니다. 장바구니 추가 함수에는 함수 체인이 숨어 있습니다.

사용자가 "그 에어팟 장바구니에 담아줘"라고 하면, AI는 이전 검색 결과에서 상품 ID를 알아냅니다. 대화의 맥락을 이해하는 것입니다.

getCart 함수는 흥미롭습니다. 파라미터가 EmptyRequest로 비어 있습니다.

사용자 ID는 SecurityContext에서 가져오기 때문입니다. AI에게 사용자 ID를 노출하지 않는 것이 보안상 중요합니다.

createOrder 함수는 가장 복잡합니다. 배송지 ID와 결제 수단 ID가 필요합니다.

이 정보들은 어디서 올까요? 사용자가 "기본 배송지로 카드 결제해줘"라고 하면, AI는 다른 함수를 먼저 호출해서 기본 배송지 ID와 기본 결제 수단 ID를 얻어와야 합니다.

이것이 멀티 함수 호출함수 체인의 조합입니다. 에러 상황도 고려해야 합니다.

장바구니에 담으려는 상품이 품절이면? 결제 실패가 발생하면?

각 함수는 에러 핸들링 전략에 따라 의미 있는 에러 메시지를 반환합니다. "죄송합니다, 해당 상품은 현재 품절이에요.

비슷한 다른 상품을 보여드릴까요?" 데이터베이스 연동도 곳곳에 적용됩니다. 장바구니 정보, 주문 내역, 배송지 목록 등 모든 데이터가 데이터베이스에서 옵니다.

각 함수는 Repository를 통해 안전하게 데이터에 접근합니다. 실제 대화 흐름을 상상해봅시다.

사용자가 "무선 이어폰 추천해줘"라고 합니다. AI가 searchProducts를 호출해서 인기 상품 5개를 보여줍니다.

"첫 번째 거 2개 장바구니에 담아줘." AI가 addToCart를 호출합니다. "지금 장바구니에 뭐 있어?" AI가 getCart를 호출해서 목록과 총액을 알려줍니다.

"집 주소로 배송하고 카드로 결제해줘." AI가 배송지 조회, 결제 수단 조회, 주문 생성 함수를 순차적으로 호출합니다. 이 모든 과정이 자연스러운 대화 속에서 일어납니다.

사용자는 복잡한 UI를 조작할 필요 없이, 마치 점원과 대화하듯 쇼핑을 완료할 수 있습니다. 김개발 씨는 이 프로젝트를 성공적으로 완료했습니다.

처음에는 막막했던 Function Calling이 이제는 강력한 무기가 되었습니다. 박시니어 씨가 말했습니다.

"이제 넌 AI 시대의 백엔드 개발자야."

실전 팁

💡 - 사용자 시나리오를 먼저 정의하고, 각 시나리오를 지원하는 함수를 설계하세요

  • 함수 간의 의존성을 파악하여 체인과 병렬 호출을 적절히 활용하세요
  • 모든 함수에 일관된 에러 처리와 보안 검증을 적용하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Spring#FunctionCalling#AI#ChatModel#ErrorHandling#Spring,AI,FunctionCalling

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스