BTP

커스텀 액션 vs RPA — Joule이 다른 이유 #shorts #SAP #Joule

왜 Joule 커스텀 액션인가

SAP Joule은 단순한 사내 챗봇이 아니라, SAP 애플리케이션 전반에 걸쳐 동작하는 코파일럿 계층입니다. 이 글은 Joule Studio에서 커스텀 액션(스킬)을 정의하고 실제 SAP 백엔드 — RAP 액션과 CAP 서비스 — 를 호출하도록 연동하는 전체 절차를 다루는 실전 예제입니다. 특히 기존 챗봇(인텐트 기반)이나 RPA 방식으로 백엔드를 연동할 때와 무엇이 구조적으로 다른지를 비교 관점에서 짚습니다.

  • Joule Studio에서 스킬과 액션을 분리해 설계하는 구조 이해
  • OpenAPI 기반 액션 임포트와 자연어 파라미터 매핑 설정
  • RAP 액션 / CAP 액션을 Joule에서 호출하는 코드 작성
  • Principal Propagation 기반 사용자 컨텍스트 보안 처리
  • 인텐트 학습·UI 레코딩 방식 대비 통합 난이도 비교 판단

미리 갖춰두면 좋은 배경

이 글은 advanced 난이도를 전제로 합니다. SAP BTP 서브어카운트 구성과 Destination 개념, OData V4 서비스의 기본 구조를 알고 있어야 하고, RAP(ABAP RESTful Application Programming Model)의 Behavior Definition 또는 CAP(Cloud Application Programming Model)의 CDS 서비스 정의 중 하나는 직접 작성해본 경험이 있으면 좋습니다. OAuth 2.0 흐름(특히 SAML Bearer Assertion)에 대한 개략적 이해가 있으면 보안 섹션을 훨씬 빠르게 소화할 수 있습니다.

실습 환경과 준비물

이 예제는 다음 환경을 기준으로 작성했습니다. 버전에 따라 화면 구성이 다를 수 있으니 참고용으로 보시기 바랍니다.

  • Joule Studio: SAP Build 로비에 통합된 형태(2024년 말 이후 제공, 이후 기능이 지속 확장됨). Skill Builder와 Agent Builder 중 이 글은 Skill/Action 중심.
  • 백엔드 옵션 A: SAP BTP ABAP Environment 2408 이상 또는 S/4HANA Cloud Public Edition — RAP 기반 OData V4 서비스
  • 백엔드 옵션 B: CAP Node.js(@sap/cds 8.x), Cloud Foundry 런타임 배포
  • 공통: BTP 서브어카운트에 Joule 구독 활성화, SAP Cloud Identity Services(IAS) 연결, Destination 서비스 인스턴스

Joule 구독과 SAP Build 엔타이틀먼트는 글로벌 어카운트 관리자 권한이 필요하므로, 실습 전에 미리 확보해 두는 것이 일반적으로 권장됩니다.

핵심 개념 — 챗봇 통합과 무엇이 다른가

기존 챗봇으로 백엔드 액션을 연동하는 과정을 떠올려 보면 이렇습니다. 인텐트를 정의하고, 발화 예문을 수십 개 학습시키고, 엔티티 추출 규칙을 만들고, 웹훅 코드를 작성해 백엔드 API를 호출한 뒤, 응답을 다시 대화 포맷으로 변환합니다. RPA 방식이라면 화면 녹화로 UI 조작을 재현하는 봇을 만들고 서비스 계정으로 실행합니다. 두 방식 모두 "대화 이해"와 "백엔드 호출" 사이의 접착 코드를 개발자가 전부 책임집니다.

Joule의 접근은 반대 방향입니다. 비유하자면, 기존 챗봇이 "신입 상담원에게 예상 질문 목록과 답변 대본을 통째로 외우게 하는 것"이라면, Joule 커스텀 액션은 "경력 상담원에게 잘 정리된 업무 매뉴얼(API 스펙)을 건네주는 것"에 가깝습니다. LLM이 이미 언어를 이해하므로, 개발자가 할 일은 액션이 무엇을 하는지, 파라미터가 무슨 의미인지를 기계가 읽을 수 있게 서술하는 것뿐입니다.

구조적으로 세 가지 차이가 통합 난이도를 갈라놓습니다.

  • 파라미터 추출: 기존 챗봇은 엔티티 학습·정규식으로 슬롯을 채우고, 누락 슬롯의 되묻기 대화를 직접 설계합니다. Joule은 OpenAPI 스펙의 파라미터 description을 근거로 LLM이 자연어에서 값을 추출하고, 부족하면 스스로 되묻습니다. 즉 "슬롯 필링 대화 트리"라는 개발 산출물 자체가 사라집니다.
  • 컨텍스트 유지: 챗봇은 세션 변수를 코드로 관리해야 하지만, Joule은 대화 이력을 유지한 채 이전 턴에서 언급된 값("아까 그 티켓")을 후속 액션 호출에 재사용합니다.
  • 보안 모델: RPA는 대개 기술 사용자(서비스 계정)로 실행되어 권한이 과대해지기 쉽습니다. Joule은 대화 중인 실제 사용자의 아이덴티티를 Principal Propagation으로 백엔드까지 전파하므로, 백엔드의 기존 권한 체계(ABAP 권한 오브젝트, CAP @requires)가 그대로 최종 방어선이 됩니다.

정리하면, 통합의 무게중심이 "대화 로직 코딩"에서 "API 계약을 얼마나 의미 있게 기술하느냐"로 이동합니다. 코드량은 줄지만, 스펙 서술 품질과 보안 구성이라는 새로운 난이도 축이 생깁니다.

실전 예제 3단계 — RAP·CAP 백엔드와 연동하기

시나리오는 설비 수리 티켓(RepairTicket)입니다. 현장 관리자가 Joule에게 "3번 라인 프레스 수리 티켓 승인해줘"라고 말하면 백엔드 액션이 실행되는 흐름을 만듭니다.

1단계: RAP 액션을 노출하고 Joule 액션으로 임포트하기

먼저 ABAP 환경에서 승인 액션을 가진 Behavior Definition을 정의합니다.

managed implementation in class zbp_repairticket unique;
strict ( 2 );

define behavior for ZR_RepairTicket alias RepairTicket
persistent table ztrepair_ticket
lock master
authorization master ( instance )
{
  update;
  action ( features : instance ) approveTicket result [1] $self;
  field ( readonly ) TicketId, CreatedBy;
}

액션 구현에서는 상태 검증 후 승인 처리를 합니다.

METHOD approveTicket.
  READ ENTITIES OF ZR_RepairTicket IN LOCAL MODE
    ENTITY RepairTicket
    ALL FIELDS WITH CORRESPONDING #( keys )
    RESULT DATA(lt_tickets).

  LOOP AT lt_tickets INTO DATA(ls_ticket).
    IF ls_ticket-Status <> 'OPEN'.
      APPEND VALUE #( %tky = ls_ticket-%tky ) TO failed-repairticket.
      APPEND VALUE #( %tky = ls_ticket-%tky
                      %msg = new_message_with_text(
                        severity = if_abap_behv_message=>severity-error
                        text = '이미 처리된 티켓입니다' ) )
        TO reported-repairticket.
      CONTINUE.
    ENDIF.
    MODIFY ENTITIES OF ZR_RepairTicket IN LOCAL MODE
      ENTITY RepairTicket
      UPDATE FIELDS ( Status ApprovedAt )
      WITH VALUE #( ( %tky = ls_ticket-%tky
                      Status = 'APPROVED'
                      ApprovedAt = utclong_current( ) ) ).
  ENDLOOP.
ENDMETHOD.

이 서비스를 OData V4로 바인딩·게시한 뒤, BTP Destination에 등록합니다. Joule Studio에서는 Actions 프로젝트를 새로 만들고 해당 Destination(또는 OpenAPI 문서)을 선택해 approveTicket 오퍼레이션을 임포트하면 액션 골격이 생성됩니다. 여기까지가 기존 챗봇의 "웹훅 코드 작성 + 인텐트 학습" 전체를 대체하는 구간입니다.

2단계: 파라미터 서술과 에러 응답 다듬기 (CAP 예시)

Joule 액션의 품질은 스펙 서술이 좌우합니다. LLM은 description을 읽고 어떤 발화에서 이 액션을 쓸지, 어떤 단어를 어떤 파라미터에 넣을지 판단합니다. CAP으로 기술자 배정 액션을 만든다면 이렇게 주석을 의도적으로 풍부하게 답니다.

service RepairService @(requires: 'MaintenanceManager') {
  entity RepairTickets as projection on db.RepairTicket;

  @title: '수리 기술자 배정'
  @Core.Description: '오픈 상태의 수리 티켓에 가용 기술자를 배정한다. 사용자가 배정, 할당, 기사 보내기 등을 요청할 때 사용.'
  action dispatchTechnician(
    @Core.Description: '수리 티켓 번호. 예: RT-2026-0831'
    ticketId : String not null,
    @Core.Description: '기술자 사번 또는 이름. 생략 시 자동 배정'
    technician : String
  ) returns { ticketId: String; assignedTo: String; eta: String };
}

핸들러에서는 에러를 "사람이 읽을 문장"으로 던지는 것이 중요합니다. Joule은 백엔드 에러 메시지를 대화 응답 생성의 근거로 활용하기 때문입니다.

const cds = require('@sap/cds');
const LOG = cds.log('repair-service');

module.exports = class RepairService extends cds.ApplicationService {
  init() {
    this.on('dispatchTechnician', async (req) => {
      const { ticketId, technician } = req.data;
      LOG.info('dispatch requested', { ticketId, user: req.user.id });

      const ticket = await SELECT.one.from('db.RepairTicket')
        .where({ ticketNo: ticketId });
      if (!ticket)
        return req.error(404, `티켓 ${ticketId}를 찾을 수 없습니다. 번호를 확인해 주세요.`);
      if (ticket.status !== 'APPROVED')
        return req.error(409, `티켓 ${ticketId}는 승인 전 상태라 배정할 수 없습니다.`);

      const assignee = technician ?? await this._autoAssign(ticket.plant);
      await UPDATE('db.RepairTicket', ticket.ID)
        .with({ assignedTo: assignee, status: 'DISPATCHED' });
      return { ticketId, assignedTo: assignee, eta: '4시간 이내' };
    });
    return super.init();
  }
};

기존 챗봇이라면 404·409 각각에 대한 대화 분기를 수동 설계해야 했지만, Joule은 이 메시지를 받아 "해당 티켓은 아직 승인 전이라 배정이 불가하다"는 식의 자연어 응답을 스스로 구성합니다.

3단계: 프로덕션 — 사용자 아이덴티티 전파와 테스트

운영 전환의 핵심은 Destination 인증을 기술 사용자에서 Principal Propagation으로 바꾸는 것입니다. ABAP 백엔드라면 일반적으로 다음과 같은 구성이 권장됩니다.

{
  "Name": "REPAIR_BACKEND",
  "Type": "HTTP",
  "URL": "https://my-abap-env.abap.region.hana.ondemand.com",
  "Authentication": "OAuth2SAMLBearerAssertion",
  "audience": "https://my-abap-env...",
  "tokenServiceURL": "https://.../oauth2/token",
  "nameIdFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
}

이렇게 하면 Joule 대화 사용자의 이메일이 SAML Assertion으로 교환되어 백엔드에 도달하고, RAP의 authorization master 검사와 권한 오브젝트가 그 사용자 기준으로 평가됩니다. 승인 권한이 없는 사용자가 "티켓 승인해줘"라고 해도 백엔드가 거부합니다 — 챗봇 서비스 계정에 슈퍼 권한을 몰아주던 안티패턴이 원천적으로 사라지는 지점입니다. 배포 전에는 Joule Studio의 미리보기 대화창에서 파라미터 오추출 케이스("프레스 말고 컨베이어 티켓")를 최소 열 개 이상 시도해 보고, 액션 설명을 반복 수정하는 프롬프트 회귀 테스트를 릴리스 절차에 포함하는 것이 좋습니다. 성능 측면에서는 액션 응답 페이로드를 필요한 필드만 반환하도록 줄여야 LLM 응답 생성 지연을 낮출 수 있습니다.

자주 만나는 문제와 해결 FAQ

Q1. 액션을 배포했는데 Joule이 계속 "도와드릴 수 없다"고만 답합니다.
대부분 스킬의 capability 서술이나 액션 description이 사용자 발화 어휘와 동떨어진 경우입니다. "approve repair ticket"이라고만 적고 사용자는 "수리 건 결재"라고 말하면 매칭 신뢰도가 떨어집니다. 실제 사용자들이 쓰는 동의어(승인, 결재, 처리)를 서술에 반영하고 미리보기에서 재검증하세요.

Q2. 파라미터가 엉뚱한 값으로 채워집니다.
파라미터 description에 형식 예시(예: RT-2026-0831)를 넣고, 백엔드에서 형식 검증 후 명확한 에러 문장을 반환하게 하면 Joule이 되묻기로 회복합니다. 정규식 검증을 LLM에 기대지 말고 백엔드에 두는 것이 안전합니다.

Q3. 미리보기에서는 되는데 운영에서 401/403이 발생합니다.
Principal Propagation 체인 문제일 가능성이 큽니다. IAS와 백엔드 간 신뢰 구성, Destination의 nameIdFormat과 백엔드 사용자 매핑(이메일 vs 로그인 ID) 불일치를 순서대로 점검하세요. 개발 단계에서 BasicAuthentication으로 임시 테스트하다가 이 차이를 놓치는 경우가 흔합니다.

Q4. 여러 액션 중 잘못된 액션이 선택됩니다.
기능이 겹치는 액션들의 서술을 상호 배타적으로 다시 쓰고, 하나의 스킬에 액션을 과도하게 몰아넣지 않는 것이 일반적으로 권장됩니다.

이어서 확장할 주제

커스텀 액션이 안정화되면, Joule Studio의 Agent Builder로 여러 액션을 조합해 다단계 업무(티켓 승인 → 기술자 배정 → 부품 재고 확인)를 자율 수행하는 에이전트를 구성해볼 수 있습니다. 또한 문서 그라운딩을 결합해 수리 매뉴얼 기반 Q&A와 액션 실행을 한 대화에서 오가게 만드는 패턴, SAP Build Process Automation 프로세스를 액션으로 호출해 승인 워크플로와 연결하는 패턴도 자연스러운 다음 확장입니다. 기존 챗봇 자산이 있다면 인텐트 정의를 액션 서술로 이관하는 마이그레이션 전략을 검토해 보세요.

함께 읽을 자료

댓글 0

아직 댓글이 없습니다.