12/5

졸용·2025년 12월 5일

TIL

목록 보기
129/144

🔹 Async 실전 적용

📌 시나리오:

  • DeliveryCommandService 가 허브/업체 배송을 생성하고
  • 동시에 AsyncAiService 를 통해
  • OpenAiClient(AiClient 구현체) 에게 “이 주문, 언제까지 보내야 하는지 + Discord 메시지” 생성을 시키고
  • 그 결과를 AiDeadlineResponseV1 로 받아서 (지금은 로그만 찍고, 나중에 Discord 서비스로 보낼 예정)
  • 이 전체 흐름에서 프롬프트 템플릿은 OpenAiConstants.DISCORD_FORMATTED_DEADLINE_PROMPT 가 담당.

🔸 HubRouteAfterCommandV1 메시지 수신

Kafka 리스너로 HubRouteAfterCommandV1 라는 DTO를 들고 DeliveryCommandService.createDelivery(...) 를 호출.


🔸 DeliveryCommandService

DeliveryCommandService.createDelivery(...) 호출

@Transactional
public void createDelivery(
    HubRouteAfterCommandV1 message,
    UUID hubDeliveryPersonId,
    UUID firmDeliveryPersonId) {

    log.info("[배송 생성 시작] orderId={}", message.orderId());

    createHubDelivery(message, hubDeliveryPersonId);  // 허브 배송 생성
    createFirmDelivery(message, firmDeliveryPersonId); // 업체 배송 생성

    log.info("[배송 생성 완료] orderId={}", message.orderId());

    asyncAiService.sendDeadlineRequest(message); // ★ AI 비동기 호출
}
  • DB 트랜잭션 안에서 허브 배송 + 업체 배송 엔티티를 각각 생성 & 저장.
  • 그 후 AsyncAiService.sendDeadlineRequest(message) 로 AI 호출을 비동기로 날림.

🔸 AsyncAiService

AsyncAiService.sendDeadlineRequest(...)

@Async
public void sendDeadlineRequest(HubRouteAfterCommandV1 message) {
    log.info("[AI 비동기 호출 시작] orderId={}", message.orderId());

    AiDeadlineRequestV1 request = new AiDeadlineRequestV1(
        message.orderId(),
        message.startHubId(),
        message.startHubName(),
        message.startHubFullAddress(),
        message.endHubId(),
        message.endHubName(),
        message.endHubFullAddress(),
        message.receiverFirmId(),
        message.receiverFirmFullAddress(),
        message.receiverFirmOwnerName(),
        message.requestNote(),
        message.productName(),
        message.productQuantity(),
        message.orderCreatedAt(),
        message.expectedDeliveryDuration()
    );

    AiDeadlineResponseV1 response = aiClient.generateDeadlineMessage(request);

    log.info("[AI 결과] orderId={}, finalDeadline={}", message.orderId(),response.finalDeadline());

    // TODO: Discord 서비스로 response.discordMessage() 전달
}
  • @Async 덕분에 별도의 스레드에서 실행 → 배송 생성 트랜잭션과 분리.
  • HubRouteAfterCommandV1AiDeadlineRequestV1 로 변환 (AI에게 줄 데이터 정제).
  • AiClient.generateDeadlineMessage(request) 호출 → 실제 OpenAI 호출은 구현체(OpenAiClient)가 담당.
  • AI 응답으로 받은 AiDeadlineResponseV1 에서 finalDeadline 을 로그에 남김.
  • 나중에는 response.discordMessage() 를 Discord 서비스에 보내서 실제 DM/알림으로 사용할 예정.

🔸 OpenAiClient

OpenAiClient.generateDeadlineMessage(...)

@Override
public AiDeadlineResponseV1 generateDeadlineMessage(AiDeadlineRequestV1 request) {

    String prompt = buildPrompt(request);

    log.info("[AI 요청] 배송 최종 발송 시한 계산, orderId={}", request.orderId());

    String outputText = chatClient
        .prompt()
        .user(prompt)
        .call()
        .content();

    log.info("[AI 응답 수신] 배송 최종 발송 시한 계산 완료, orderId={}", request.orderId());

    String finalDeadline = extractDeadline(outputText);

    return new AiDeadlineResponseV1(outputText, finalDeadline);
}
  • buildPrompt(request)거대한 텍스트 프롬프트 생성.

  • chatClient.prompt().user(prompt).call().content()Spring AI의 ChatClient 를 통해 OpenAI 호출.

  • 전체 응답(outputText)에서 extractDeadline(...) 으로 "yyyy-MM-dd HH:mm" 형식의 최종 발송 시한만 파싱.

  • AiDeadlineResponseV1

    • discordMessage: Discord 코드블록 그대로
    • finalDeadline: 파싱된 마지막 줄 시간만
      담아서 리턴.

🔸 OpenAiConstants

OpenAiConstants.DISCORD_FORMATTED_DEADLINE_PROMPT

  • AI에게 전달하는 전체 템플릿 문자열.
  • 예시 1, 2, 3 을 포함해서 “이런 형식으로 Discord 코드블록을 만들어라” 를 강하게 가이드.
  • 마지막에 실제 주문 정보 위치에 "%s" / "%d" 자리로 템플릿 변수를 만들어 두고,
  • OpenAiClient.buildPrompt(...) 에서 formatted(...) 호출로 실제 값 채워 넣음.


🔹 각 클래스의 역할

🔸 OpenAiConstants

  • 순수 상수 모음 클래스.

  • 생성자를 private 으로 막아서 인스턴스 생성 불가.

  • 현재 역할:

    • Discord용 메시지 + 발송 시한 계산 규칙 전체를 담은 프롬프트 템플릿 제공.
    • 예시 1, 2, 3 포함.
    • 마지막 부분에 실제 주문 정보 %s, %d 자리 (formatted()로 채움)를 제공.

🔸 AiClient (인터페이스)

public interface AiClient {
    AiDeadlineResponseV1 generateDeadlineMessage(AiDeadlineRequestV1 request);
}
  • AI 호출을 추상화한 인터페이스.

  • 장점:

    • 나중에 OpenAI가 아니라 다른 모델/다른 구현체 로 교체해도

      • AsyncAiService 입장에서는 AiClient 만 보면 됨.
    • 테스트 시에는 FakeAiClient / StubAiClient 를 주입하기 쉬움.


🔸 OpenAiClient (AiClient 구현체)

  • AiClient실제 구현체, Spring Bean (@Component).

  • ChatClient (Spring AI)를 주입받아서 OpenAI와 통신.

  • buildPrompt(...):

    • request 를 보고

      • requestNote 가 비어 있으면 "없음" 으로 치환.
      • 경유지(route)는 아직 "없음" 으로 고정 (TODO 주석 있음).
    • OpenAiConstants.DISCORD_FORMATTED_DEADLINE_PROMPT.formatted(...)실제 주문 값 바인딩.

  • extractDeadline(...):

    • 줄 단위로 쪼갠 뒤
    • "위 내용을 기반으로 도출된 최종 발송 시한은" 으로 시작하는 줄을 찾아서
    • 앞/뒤 고정 문구를 제거 → "yyyy-MM-dd HH:mm" 부분만 남김.
    • 찾지 못하면 null.

🔸 AsyncAiService

  • 애플리케이션 레벨에서 AI 호출을 담당하는 비동기 서비스.

  • 역할:

    1. HubRouteAfterCommandV1AiDeadlineRequestV1 로 매핑.
    2. AiClient 를 통해 실제 AI 호출.
    3. 결과 로그 남기고, 나중에 Discord 서비스로 메시지 전달 예정.
  • @Async 덕분에:

    • DeliveryCommandService.createDelivery(...) 처리가 AI 응답을 기다리지 않고 바로 끝날 수 있음.
    • 배송 생성 로직과 AI 호출을 느슨하게 연결.

🔸 DeliveryCommandService

  • 배송과 관련된 “명령(Command)” 책임을 한 데 모은 서비스.

  • 주요 기능:

    • 배송 생성 + AI 호출 (createDelivery)
    • 배송 상태 변경 (changeDeliveryStatus)
    • 배송 취소 (cancelDelivery)
  • createDelivery()도메인 관점의 “전체 배송 생성” 유즈케이스:

    • HubDelivery + FirmDelivery 를 모두 생성.
    • 그다음에 AI에게 이 주문의 “최종 발송 시한 + Discord 메시지”를 비동기 요청.
profile
꾸준한 공부만이 답이다

0개의 댓글