BTP

커스텀 액션 vs 챗봇 — Joule 연동 차이 #shorts #SAP #Joule

개요와 목표 체크리스트

SAP Joule은 SAP 애플리케이션 전반에 내장되는 생성형 AI 코파일럿입니다. 이 글은 기존 레거시 챗봇(intent/webhook 방식)과 비교하면서, Joule Studio에서 커스텀 액션(Custom Action)을 등록하고 SAP BTP CAP 서비스를 OpenAPI 3.0 스펙으로 노출한 뒤 XSUAA·Destination 인증까지 연동하는 전체 흐름을 다루는 실전 예제입니다. 완료하면 다음을 할 수 있습니다.

  • 레거시 챗봇과 Joule 커스텀 액션의 구조적 차이를 설명할 수 있다
  • 재고 조회 CAP 서비스를 OpenAPI 스펙으로 노출할 수 있다
  • Joule Studio에 커스텀 액션을 등록하고 Destination/XSUAA 인증을 구성할 수 있다
  • operationId·description 부실로 인한 액션 오매칭 문제를 예방할 수 있다

미리 알아두면 좋은 배경

고급(advanced) 난이도 기준으로 다음 경험이 있으면 수월합니다. SAP BTP Cloud Foundry 환경 기본 조작, CAP(Cloud Application Programming Model) Node.js 서비스 개발 경험, OpenAPI 3.0 스펙 문법, OAuth 2.0 클라이언트 크리덴셜 플로우에 대한 이해입니다. 레거시 챗봇(SAP Conversational AI 등) 운영 경험이 있다면 비교 관점이 더 명확해집니다.

환경 구성과 준비물

이 예제는 다음 환경을 기준으로 합니다. 버전에 따라 화면 구성이 다를 수 있으니 일반적으로 최신 문서를 함께 확인하는 것을 권장합니다.

  • SAP BTP Cloud Foundry 런타임 (엔터프라이즈 또는 트라이얼 계정)
  • Joule Studio — SAP Build 구독에 포함된 Joule 확장 도구 (2025년 이후 제공 에디션 기준)
  • SAP CAP Node.js — @sap/cds 8.x 이상 권장
  • XSUAA, Destination Service 인스턴스 (BTP 서브어카운트 내)
  • Node.js 20 LTS, cf CLI, cds CLI

Joule Studio 접근을 위해 서브어카운트에 Joule 및 SAP Build 엔타이틀먼트가 할당되어 있어야 하며, 역할 컬렉션(예: Joule Studio 관련 역할)이 사용자에게 부여되어 있어야 합니다.

핵심 개념 — 챗봇의 intent 매칭 vs Joule의 스펙 기반 매칭

레거시 챗봇 통합을 전화 교환원에 비유할 수 있습니다. 교환원(챗봇 엔진)에게 "재고 확인"이라는 표현의 변형 수십 개(utterance)를 미리 학습시키고, intent가 확정되면 하드코딩된 webhook으로 연결하는 구조입니다. 즉 사람이 언어 → 의도 → API의 매핑 규칙을 전부 수작업으로 정의해야 했습니다.

레거시 챗봇: 발화 학습 → intent 분류기 → webhook 코드 → API 호출 (매핑 4단계 전부 수동)
Joule 커스텀 액션: OpenAPI 스펙 등록 → LLM이 스펙의 description을 읽고 자동 매칭 (매핑 1단계)

Joule 커스텀 액션은 접근이 다릅니다. 개발자는 OpenAPI 3.0 스펙으로 "이 API가 무엇을 하는지"를 서술하고, Joule의 LLM이 사용자 발화와 스펙의 summary/description/파라미터 설명을 대조해 호출할 액션과 파라미터를 스스로 결정합니다. 잘 쓴 메뉴판을 주면 점원이 알아서 주문을 접수하는 셈입니다.

이 차이가 통합 난이도를 바꿉니다. 챗봇에서는 intent 하나당 발화 20~50개 작성, 파라미터 추출용 entity 정의, webhook 핸들러 코드가 필요했지만, Joule에서는 스펙 문서의 품질이 곧 매칭 품질입니다. 코드량은 줄어드는 대신, description을 모호하게 쓰면 LLM이 엉뚱한 액션을 고르는 새로운 유형의 장애가 생깁니다. 즉 난이도가 "코딩량"에서 "API 설계·서술 품질 + 인증 아키텍처"로 이동한 것입니다. 인증 역시 챗봇 시대의 단순 API 키 방식 대신, BTP의 Destination Service와 XSUAA를 통한 OAuth 기반 위임이 일반적입니다.

실전 구현 3단계 — 재고 조회 액션 만들기

1단계 — CAP 서비스로 재고 조회 API 만들기

가전 유통사의 재고 조회 시나리오입니다. 먼저 CDS 모델과 서비스를 정의합니다.

// srv/stock-service.cds
using { cuid } from '@sap/cds/common';

entity WarehouseStock : cuid {
  materialCode : String(18);
  materialName : String(60);
  plant        : String(4);
  quantity     : Integer;
  unit         : String(3);
}

service StockService @(path: '/stock') {
  @readonly entity Stocks as projection on WarehouseStock;

  // Joule이 호출할 커스텀 액션 함수
  function checkStock(materialCode : String, plant : String)
    returns { materialName: String; quantity: Integer; unit: String };
}

핸들러를 구현하고 cds compile srv --to openapi 명령으로 OpenAPI 스펙을 생성할 수 있습니다.

// srv/stock-service.js
const cds = require('@sap/cds');

module.exports = class StockService extends cds.ApplicationService {
  init() {
    this.on('checkStock', async (req) => {
      const { materialCode, plant } = req.data;
      const row = await SELECT.one.from('WarehouseStock')
        .where({ materialCode, plant });
      if (!row) return req.error(404, `자재 ${materialCode} 재고 없음`);
      return { materialName: row.materialName,
               quantity: row.quantity, unit: row.unit };
    });
    return super.init();
  }
};

2단계 — Joule이 이해하는 스펙 다듬기 + 에러/로깅

자동 생성된 스펙을 그대로 쓰면 안 됩니다. Joule의 매칭 정확도는 operationIddescription에 좌우되므로 사람이 읽어도 명확하게 보강합니다.

{
  "openapi": "3.0.3",
  "info": { "title": "Warehouse Stock API", "version": "1.0.0" },
  "paths": {
    "/stock/checkStock": {
      "get": {
        "operationId": "checkWarehouseStock",
        "summary": "특정 플랜트의 자재 재고 수량 조회",
        "description": "자재 코드와 플랜트 코드를 받아 현재 가용 재고 수량과 단위를 반환합니다. 재고 확인, 수량 조회, 품절 여부 질문에 사용합니다.",
        "parameters": [
          { "name": "materialCode", "in": "query", "required": true,
            "schema": { "type": "string" },
            "description": "SAP 자재 번호 (예: TV-55-OLED)" },
          { "name": "plant", "in": "query", "required": true,
            "schema": { "type": "string" },
            "description": "4자리 플랜트 코드 (예: KR01)" }
        ],
        "responses": { "200": { "description": "재고 정보 반환" },
                       "404": { "description": "해당 자재 재고 데이터 없음" } }
      }
    }
  }
}

운영 관점에서는 핸들러에 구조화 로깅을 넣어 Joule이 어떤 파라미터로 호출했는지 추적 가능하게 합니다.

const LOG = cds.log('joule-action');
this.before('checkStock', (req) => {
  LOG.info('Joule action invoked', {
    action: 'checkWarehouseStock',
    materialCode: req.data.materialCode,
    plant: req.data.plant,
    correlationId: req.headers['x-correlation-id']
  });
});

404 같은 오류 응답에도 사람이 읽을 수 있는 메시지를 담아야 Joule이 사용자에게 자연어로 상황을 설명할 수 있습니다.

3단계 — XSUAA·Destination 인증과 프로덕션 배포

Joule은 백엔드를 직접 호출하지 않고 Destination을 경유하는 구성이 일반적입니다. 먼저 CAP 서비스를 XSUAA로 보호합니다.

// xs-security.json
{
  "xsappname": "warehouse-stock-srv",
  "tenant-mode": "dedicated",
  "scopes": [{ "name": "$XSAPPNAME.StockViewer",
               "description": "재고 조회 권한" }],
  "role-templates": [{ "name": "StockViewer",
                       "scope-references": ["$XSAPPNAME.StockViewer"] }]
}
# mta.yaml 핵심 부분
modules:
  - name: stock-srv
    type: nodejs
    path: gen/srv
    requires: [{ name: stock-xsuaa }]
resources:
  - name: stock-xsuaa
    type: org.cloudfoundry.managed-service
    parameters:
      service: xsuaa
      service-plan: application
      path: ./xs-security.json

배포 후 BTP 콕핏에서 Destination을 생성합니다. OAuth2ClientCredentials 타입으로 XSUAA의 clientid/clientsecret/tokenServiceURL(서비스 키에서 확인)을 입력하고, URL에는 CAP 서비스 엔드포인트를 지정합니다. 마지막으로 Joule Studio에서 Actions → Create Action으로 이동해 위 OpenAPI 스펙을 임포트하고, 방금 만든 Destination을 액션의 연결 대상으로 선택한 뒤, Skill에 액션을 연결하고 배포하면 "KR01 창고에 TV-55-OLED 재고 얼마나 있어?" 같은 질문이 자동으로 checkWarehouseStock 호출로 이어집니다. 배포 전 Joule Studio의 테스트 콘솔에서 다양한 발화 변형으로 매칭을 검증하는 것을 권장합니다.

흔한 실수와 트러블슈팅 FAQ

  • Q1. Joule이 내 액션을 아예 호출하지 않아요. — 대부분 operationId 누락 또는 generated_op_1 같은 무의미한 자동 생성 값이 원인입니다. 동사+명사 형태(checkWarehouseStock)로 명명하고 description에 "언제 이 액션을 쓰는지"를 사용 상황 중심으로 서술하세요.
  • Q2. 비슷한 액션 두 개 중 엉뚱한 쪽이 호출됩니다. — 재고 조회와 주문 조회의 description이 서로 비슷하면 LLM이 혼동합니다. 각 액션의 설명에 상호 배타적인 키워드(재고/가용 수량 vs 주문/납기)를 명시해 경계를 분리하세요. 레거시 챗봇의 intent 충돌과 같은 문제이지만, 해결 수단이 발화 재학습이 아니라 문서 수정이라는 점이 다릅니다.
  • Q3. 401/403 오류가 반환됩니다. — Destination의 tokenServiceURL 끝에 /oauth/token이 붙었는지, XSUAA 서비스 키의 clientsecret이 회전되지 않았는지 확인하세요. 403이면 스코프 문제로, CAP 서비스의 @requires 어노테이션과 클라이언트 크리덴셜로 발급된 토큰의 스코프가 일치하는지 점검합니다.
  • Q4. 파라미터가 빈 값으로 들어옵니다. — 파라미터 description에 형식과 예시가 없으면 Joule이 추출에 실패할 수 있습니다. "4자리 플랜트 코드 (예: KR01)"처럼 예시를 포함하세요.

이어서 살펴볼 주제

이 예제의 조회형 액션을 넘어, 다음 주제로 확장할 수 있습니다. 쓰기 작업(POST 액션)과 사용자 확인 플로우 설계, Joule Skill에서 여러 액션을 체이닝하는 오케스트레이션, Principal Propagation으로 최종 사용자 신원을 백엔드까지 전달하는 인증 고도화, 그리고 SAP Build Process Automation과 결합해 승인 워크플로를 Joule 대화로 트리거하는 패턴입니다. S/4HANA 표준 Joule 시나리오와 커스텀 액션의 공존 전략도 실무에서 중요한 주제입니다.

더 읽어볼 자료

댓글 0

아직 댓글이 없습니다.