Spring AI에서 커스텀 Advisor로 인터셉터 구현하기

커스텀 Advisor의 개념과 활용

Servlet 및 Spring AOP를 경험한 개발자라면 요청/응답을 가로채는 기능에 익숙할 것이다. 예를 들어, 로그 기록, 인증 처리 등이 대표적이다.

Spring AI의 Advisor는 이러한 인터셉터 역할을 수행하며, AI 호출 전후에 추가적인 처리를 가능하게 한다. 주요 용도는 다음과 같다:

  • 호출 전: 사용자 입력 검증, 권한 확인, Prompt 변환 등
  • 호출 후: 응답 로깅, 결과 분석, 데이터 정제 등

공식 제공되는 내장 Advisor 외에도 실제 비즈니스 요구사항에 맞춰 커스텀 Advisor를 구현할 수 있다. 이는 단계별 설정으로 쉽게 적용 가능하다.

1. 커스텀 Advisor 구현 방법

  1. 인터페이스 구현: CallAroundAdvisor 또는 StreamAroundAdvisor
  2. 메서드 오버라이드: aroundCall() 또는 aroundStream() 재정의
  3. 실행 순서 설정: getOrder() 메서드로 우선순위 지정 (값이 작을수록 먼저 실행)
  4. 고유 이름 부여: getName()을 통해 식별 가능한 이름 제공

비스트리밍 처리 (CallAroundAdvisor)

@Override
public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
    // 1. 요청 전처리
    AdvisedRequest modified = preProcess(request);

    // 2. 다음 Advisor로 위임
    AdvisedResponse response = chain.nextAroundCall(modified);

    // 3. 응답 후처리
    return postProcess(response);
}

스트리밍 처리 (StreamAroundAdvisor)

@Override
public Flux<AdvisedResponse> aroundStream(AdvisedRequest request, StreamAroundAdvisorChain chain) {
    // 1. 요청 전처리
    AdvisedRequest modified = preProcess(request);

    // 2. 스트리밍 응답 처리
    return chain.nextAroundStream(modified)
        .map(response -> postProcess(response));
}

2. 로깅용 커스텀 Advisor 예시

package com.example.ai.advisor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClientMessageAggregator;
import org.springframework.ai.chat.client.ChatClientRequest;
import org.springframework.ai.chat.client.ChatClientResponse;
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
import reactor.core.publisher.Flux;

@Slf4j
public class RequestLoggingAdvisor implements CallAdvisor, StreamAdvisor {

    @Override
    public String getName() {
        return this.getClass().getSimpleName();
    }

    @Override
    public int getOrder() {
        return 10;
    }

    private ChatClientRequest before(ChatClientRequest req) {
        log.info("AI 요청: {}", req.prompt());
        return req;
    }

    private void after(ChatClientResponse resp) {
        log.info("AI 응답: {}", resp.chatResponse().getResult().getOutput().getText());
    }

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
        request = before(request);
        ChatClientResponse response = chain.nextCall(request);
        after(response);
        return response;
    }

    @Override
    public Flux<ChatClientResponse> adviseStream(ChatClientRequest request, StreamAdvisorChain chain) {
        request = before(request);
        Flux<ChatClientResponse> stream = chain.nextStream(request);
        return new ChatClientMessageAggregator().aggregateChatClientResponse(stream, this::after);
    }
}

3. Re-Reading (Re2) Advisor 설계

Re-Reading은 모델이 질문을 다시 읽도록 유도해 복잡한 추론 능력을 향상시키는 기법이다. 연구에 따르면 일부 케이스에서 성능 개선이 가능하다.

주의 사항: 호출 비용이 두 배로 증가하므로, 고객 중심 서비스에서는 사용하지 않는 것이 좋다.

기본 원리

사용자 입력을 아래 형식으로 재구성한다:

{원본 질문}
다시 문제를 읽어보세요: {원본 질문}

이 방식은 모델이 핵심 정보를 더 깊이 이해하도록 유도한다.

적합한 사용 사례

  • 금융권의 복잡한 리스크 판단
  • 산업 장비의 고장 진단 (장문, 다조건 분석)
  • 연구 분야의 데이터 해석 (정밀한 질문 이해 필요)

반면, 일반 사용자의 질문들은 대부분 간단하거나 일상적이다:

  • 날씨 정보
  • 비밀번호 변경 방법
  • 기초 문법 질문

이러한 경우엔 오히려 응답 속도 저하와 비용 증가만 발생하며, 실질적인 성능 향상은 느껴지지 않는다.

4. Advisor 최적화 팁

  • 단일 책임 원칙 준수: 하나의 Advisor는 하나의 기능만 담당해야 함
  • 실행 순서 조절: getOrder()로 체인 내 실행 순서 제어
  • 스트리밍/비스트리밍 동시 지원: 가능한 한 두 인터페이스 모두 구현하여 호환성 확보
  • 성능 최적화: 블로킹 작업, 느린 데이터베이스 쿼리 등은 피해야 함
  • 예외 처리 강화: null 요청, 빈 응답, 네트워크 오류 등 다양한 경계 조건 테스트
  • Flux 연산자 활용: doOnNext, filter, map 등을 활용해 스트리밍 처리 정교화
  • Context 공유: adviseContext를 이용해 체인 내 상태 공유 가능

태그: Spring AI Advisor AOP Interceptor streaming

7월 10일 06:00에 게시됨