개요와 이 글에서 다룰 것
RAP에서 하나의 Business Object를 만들었는데, 부서마다 요구하는 OData 서비스가 다르다면 어떻게 해야 할까요? 같은 데이터를 노출해야 하는데 서비스를 새로 만들 때마다 BO도 복제해야 한다면, 유지보수 지옥이 시작됩니다. Additional Binding은 이 문제를 우아하게 해결하는 RAP의 핵심 메커니즘입니다. 이 글에서는 단일 BO를 여러 서비스에 동시에 노출하는 패턴을 실무 예제로 풀어봅니다.
- Service Definition과 Service Binding의 분리 원칙 이해
- 하나의 BO를 V2/V4/UI/API용으로 분기 노출하는 패턴
- 내부 관리용과 외부 파트너용 서비스를 동일 BO로 운영하기
- Behavior 권한 분리(Authorization Context)와 Projection 활용
- 버전 관리(Versioning) 및 마이그레이션 전략
알고 있으면 좋은 배경 지식
RAP의 기본 4계층 구조(Data Model & Behavior, Business Object Projection, Service Definition, Service Binding)에 익숙해야 합니다. CDS View Entity, Behavior Definition(BDEF), Projection View 작성 경험이 있다면 충분합니다. Managed/Unmanaged BO 차이와 ADT(ABAP Development Tools) 사용 경험이 전제됩니다.
실습 환경과 준비물
이 글의 예제는 다음 환경 기준으로 작성되었습니다. SAP S/4HANA Cloud ABAP Environment(Steampunk) 또는 SAP BTP ABAP Environment 2402 이상, 그리고 On-Premise는 S/4HANA 2022 FPS01 이상에서 동일하게 동작합니다. ADT(Eclipse 기반) 2024-03 릴리스 이상, ABAP 언어 버전은 ABAP for Cloud Development를 권장합니다.
- SAP BTP ABAP Environment (Steampunk) 또는 S/4HANA 2022+
- ADT 3.40 이상 (Service Binding Editor가 V4 UI/API를 모두 지원하는 버전)
- Fiori Elements Preview 사용 시 Chromium 기반 브라우저
- 패키지 분리 권장: ZPKG_SALES_BO(BO), ZPKG_SALES_API(External), ZPKG_SALES_UI(Internal)
핵심 개념: 왜 Additional Binding이 필요한가
RAP의 서비스 계층은 의도적으로 분리되어 있습니다. 데이터/행동(BO Layer)은 한 번만 만들고, 외부 노출은 여러 채널로 분기시키는 구조입니다. 비유하자면 BO는 방송국 스튜디오이고, Service Binding은 송출 채널입니다. 같은 방송을 케이블, 위성, 인터넷 스트리밍으로 동시에 송출하지만 콘텐츠 제작은 한 곳에서 합니다.
핵심 규칙: 1 BO Projection → N Service Definitions → M Service Bindings. Projection 하나에 Service Definition을 여러 개 만들 수 있고, 각 Service Definition은 다시 여러 Binding을 가질 수 있습니다.
Additional Binding이 해결하는 대표 시나리오를 보겠습니다.
- 이중 프로토콜 지원: 레거시 Fiori 앱은 OData V2를, 신규 SAP Build Apps는 V4를 요구하는 경우
- UI vs API 분리: 내부 Fiori UI(UI 어노테이션 포함)와 외부 시스템 연계용 순수 API(어노테이션 미노출)를 분리
- 권한 컨텍스트 분리: 내부 직원은 모든 필드를, 파트너는 제한된 필드/액션만 사용
- 점진적 마이그레이션: 신구 서비스를 병행 운영하며 단계적으로 클라이언트 이전
실전 예제 1단계 — 기본 BO와 첫 번째 Binding
판매주문(SalesOrder) BO를 만들고, 우선 내부 관리자용 Fiori UI 서비스를 노출합니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order Root'
define root view entity ZR_SalesOrderTP
as select from zsales_ord_hdr as Header
composition [0..*] of ZR_SalesOrderItemTP as _Items
{
key Header.order_uuid as OrderUUID,
Header.order_no as OrderNo,
Header.customer_id as CustomerId,
Header.order_date as OrderDate,
@Semantics.amount.currencyCode: 'CurrencyCode'
Header.gross_amount as GrossAmount,
Header.currency_code as CurrencyCode,
Header.overall_status as OverallStatus,
Header.created_by as CreatedBy,
Header.created_at as CreatedAt,
Header.last_changed_by as LastChangedBy,
Header.last_changed_at as LastChangedAt,
_Items
}
BO Projection을 생성합니다. 이 Projection이 이후 모든 Service Definition의 입력이 됩니다.
define root view entity ZC_SalesOrder
provider contract transactional_query
as projection on ZR_SalesOrderTP
{
key OrderUUID, OrderNo, CustomerId, OrderDate,
GrossAmount, CurrencyCode, OverallStatus,
CreatedBy, CreatedAt,
_Items : redirected to composition child ZC_SalesOrderItem
}
첫 번째 Service Definition과 Binding을 만듭니다.
@EndUserText.label: 'Internal Sales Order Service'
define service ZUI_SALESORDER_O4 {
expose ZC_SalesOrder as SalesOrder;
expose ZC_SalesOrderItem as SalesOrderItem;
expose I_Currency as Currency;
}
실전 예제 2단계 — 동일 BO를 외부 파트너용 API로 추가 노출
파트너사가 시스템 연계로 호출할 OData V4 Web API를 같은 BO에서 분기시킵니다.
@EndUserText.label: 'Partner Sales Order API'
define service ZAPI_SALESORDER_EXT {
expose ZC_SalesOrder as SalesOrder
excluding { CreatedBy; CreatedAt; LastChangedBy; LastChangedAt; };
expose ZC_SalesOrderItem as SalesOrderItem
excluding { CreatedBy; LastChangedBy; };
}
이어서 외부 API용 Service Binding을 생성합니다. Binding Type을 OData V4 - Web API로 지정합니다.
ZUI_SALESORDER_O4: 내부 Fiori UI (V4 UI)ZAPI_SALESORDER_EXT: 외부 파트너 Web API (V4 Web API)ZUI_SALESORDER_O2: 레거시 V2 호환 채널 (V2 UI, 필요 시)
Behavior Projection 분리 패턴입니다.
projection;
strict ( 2 );
use draft;
define behavior for ZC_SalesOrder alias SalesOrder
{
use create;
use update;
use delete;
use action SubmitForApproval;
use action Cancel;
use association _Items { create; with draft; }
}
실전 예제 3단계 — 프로덕션 패턴: 멀티 채널, 권한 컨텍스트, 버전 공존
@EndUserText.label: 'Sales Order - Internal UI'
define service ZUI_SO_INTERNAL {
expose ZC_SalesOrder as SalesOrder;
expose ZC_SalesOrderItem as SalesOrderItem;
expose I_Currency as Currency;
}
@EndUserText.label: 'Sales Order - Partner API v1 (stable)'
define service ZAPI_SO_PARTNER_V1 {
expose ZC_SalesOrder as SalesOrder
excluding { CreatedBy; CreatedAt; LastChangedBy; LastChangedAt; OverallStatus; };
expose ZC_SalesOrderItem as SalesOrderItem
excluding { CreatedBy; LastChangedBy; };
}
@EndUserText.label: 'Sales Order - Partner API v2 (next)'
define service ZAPI_SO_PARTNER_V2 {
expose ZC_SalesOrder as SalesOrder;
expose ZC_SalesOrderItem as SalesOrderItem;
expose I_Currency as Currency;
}
권한 컨텍스트 분리 패턴입니다.
@MappingRole: true
define role ZR_SALESORDER_ACCESS {
grant select on ZR_SalesOrderTP
where ( CustomerId ) =
aspect pfcg_auth ( ZSALES_AUTH, CUSTOMERID, ACTVT = '03' );
grant select on ZR_SalesOrderTP
where OverallStatus = 'R'
for grant select on entity ZC_SalesOrder
where ( CustomerId ) =
aspect user_communication_partner;
}
RAP BO 단위 테스트는 채널과 무관하게 작동합니다.
CLASS ltc_salesorder_create DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS create_valid_order FOR TESTING.
ENDCLASS.
CLASS ltc_salesorder_create IMPLEMENTATION.
METHOD create_valid_order.
MODIFY ENTITIES OF zc_salesorder
ENTITY SalesOrder
CREATE FIELDS ( OrderNo CustomerId GrossAmount CurrencyCode )
WITH VALUE #( ( %cid = 'C1'
OrderNo = '4900001234'
CustomerId = 'CUST_KR_001'
GrossAmount = '15000.00'
CurrencyCode = 'KRW' ) )
MAPPED DATA(mapped) FAILED DATA(failed) REPORTED DATA(reported).
cl_abap_unit_assert=>assert_initial( failed-salesorder ).
cl_abap_unit_assert=>assert_not_initial( mapped-salesorder ).
ENDMETHOD.
ENDCLASS.
현장에서 자주 마주치는 함정과 해결법
- Binding 활성화 후 Service URL이 안 보임: Service Binding 활성화 후 "Publish Local" 또는 "Publish to System"을 눌렀는지 확인합니다. BTP ABAP Environment에서는 Communication Scenario까지 연결되어야 외부에서 호출됩니다.
- 한쪽 채널에서 UI 어노테이션이 깨짐: Metadata Extension이 Service Definition 단위로 적용되므로, V2/V4가 동시에 활성화되어 있으면 충돌이 발생할 수 있습니다. 채널별로 분리하거나 어노테이션을 Projection에 직접 넣는 방식을 검토합니다.
- 외부 API에서 의도치 않은 액션이 노출됨: Behavior Projection의
use action *와일드카드 노출은 외부 API에 위험합니다. 명시적 화이트리스트를 사용하세요.
자주 묻는 질문
Q: Service Definition에서 같은 엔티티를 다른 이름으로 두 번 노출할 수 있나요?
A: 하나의 Service Definition 안에서는 동일 엔티티를 한 번만 expose할 수 있습니다. 다른 alias가 필요하다면 Service Definition을 분리하거나 Projection View를 하나 더 만들어 노출하는 방식을 권장합니다.
Q: V2와 V4 Binding을 동시에 활성화하면 성능 문제가 있나요?
A: 런타임 실행 경로는 Binding마다 독립적으로 동작하므로 직접적인 충돌은 없습니다. 다만 Metadata 생성 비용이 증가하므로, V2는 점진적 폐지 일정과 함께 운영하는 것을 권장합니다.
Q: 같은 BO를 노출하는 두 서비스 중 한쪽에서만 Draft를 쓰고 싶습니다.
A: Draft는 Behavior Projection 단위로 활성화됩니다. 각 서비스용 Behavior Projection을 분리하면 한쪽만 use draft를 선언할 수 있습니다. 동일 BO를 Draft/Non-Draft 채널로 동시 운영할 때는 락(Locking)과 동시성 시나리오를 충분히 테스트해야 합니다.
더 넓은 RAP 아키텍처로 확장하기
Additional Binding에 익숙해졌다면 다음 주제를 이어서 살펴보시기 바랍니다. RAP Service Consumption Model을 통해 외부 서비스를 다시 끌어와 BO로 가공하는 패턴, Custom Entity와 Unmanaged Query를 통해 비-CDS 데이터를 같은 채널 구조로 노출하는 방법, 그리고 Event Binding을 추가하여 같은 BO의 변경 사항을 RAP Events(SAP Event Mesh)로 발행하는 시나리오까지 확장하면 멀티 채널 BO의 풀스택 그림이 완성됩니다.
댓글 0
아직 댓글이 없습니다.