RAP

Interface View 직접 노출하면 큰일 #shorts #SAP #RAP

▶ YouTube에서 보기

개요 및 핵심 포인트

SAP RAP(RESTful ABAP Programming Model)에서 CDS 뷰는 단순히 "데이터를 보여주는 SQL 래퍼"가 아닙니다. RAP의 계층 아키텍처에서 Interface View와 Projection View는 명확히 분리된 역할을 가지며, 이 분리 자체가 RAP 설계 철학의 핵심입니다. 이 글에서는 두 뷰의 차이를 단순 비교에 그치지 않고, "왜 SAP가 이런 이중 구조를 선택했는가"라는 배경부터 실전 SalesOrder 시나리오까지 다루겠습니다.

  • Interface View(I_*)와 Projection View(C_*)의 계층적 역할 이해
  • alias 키워드를 통한 필드 재정의 패턴 습득
  • @Consumption 어노테이션 계열의 적용 위치 판단
  • OData 서비스 노출 시 Projection만 expose하는 이유 파악
  • Stable API와 Use-case별 커스터마이징 분리 원칙 체득

읽기 전 알아두면 좋은 것

이 글은 ABAP CDS의 기본 문법(define view, select from, association)을 한 번이라도 작성해 본 독자를 가정합니다. RAP의 BO(Business Object) 개념, behavior definition의 존재 정도는 알고 있어야 코드 흐름을 따라갈 수 있습니다. OData V4 서비스 노출 경험이 있다면 더 좋지만, 없어도 4번 섹션의 비유로 충분히 따라올 수 있도록 설명합니다.

실습 환경과 사전 준비물

이 글의 코드는 다음 환경을 기준으로 작성되었습니다.

  • SAP S/4HANA 2022 또는 SAP BTP ABAP Environment (Steampunk) 2308 이상
  • ABAP Development Tools(ADT) for Eclipse 2024-06 이상
  • CDS View Entity 문법(define view entity) 기반 — 구형 @AbapCatalog.sqlViewName 방식이 아닌 최신 문법 사용
  • S/4HANA On-Premise 환경에서는 일부 어노테이션 가용성이 릴리스마다 다를 수 있으므로 권장 릴리스 노트 확인 필요

실습용 테이블로는 사용자가 직접 정의한 zsales_order(주문 헤더)와 zsales_item(주문 항목) 두 개를 가정합니다. 실제 환경에서는 EPM 데모 데이터의 snwd_so를 사용해도 동일한 패턴이 적용됩니다.

왜 두 개의 뷰를 분리하는가 — 계층 구조의 배경

RAP가 두 종류의 CDS 뷰를 강하게 권장하는 이유를 이해하려면, "데이터 모델"과 "서비스 모델"이라는 두 관심사가 본질적으로 다른 수명 주기를 가진다는 점을 인식해야 합니다.

비유하자면 Interface View는 도서관 서고의 원본 도서이고, Projection View는 특정 강의용으로 발췌·재편집된 교재입니다. 강의가 바뀌어도 원본은 그대로이며, 원본이 바뀌면 모든 강의 교재가 영향을 받습니다.

Interface View(접두어 I_)는 데이터 모델 계층에 위치합니다. DB 테이블에 직접 select하거나, 다른 Interface View를 조합해 "재사용 가능한 비즈니스 의미 단위"를 만듭니다. 한 번 만들어지면 여러 컨슈머가 공유하므로, 필드명·연관·키 구조가 자주 바뀌면 안 됩니다. 일반적으로 stable API의 성격을 가집니다.

Projection View(접두어 C_, Consumption)는 서비스 모델 계층에 위치합니다. 특정 UI나 외부 API의 use-case에 맞춰 필드 일부만 노출하고, 이름을 변경하며, UI 어노테이션·Value Help·Search 설정을 부여합니다. 같은 Interface View를 기반으로 Fiori 앱용, 모바일용, 외부 통합용 Projection을 각각 만들 수 있습니다.

이 분리를 지키지 않고 하나의 거대한 뷰에 UI 어노테이션까지 몰아넣으면, UI 요구사항이 바뀔 때마다 데이터 모델이 흔들리고, 다른 컨슈머가 깨지는 회귀가 발생합니다. RAP의 두 뷰 분리는 이런 결합도를 끊기 위한 구조적 장치입니다.

계층 흐름은 일반적으로 다음과 같이 표현됩니다.

DB Table (zsales_order)
        |
        v
Interface View  (I_SalesOrder)        <-- 재사용·안정성 우선
        |
        v
Projection View (C_SalesOrderTP)      <-- 소비·UI 어노테이션
        |
        v
Service Definition / Binding (OData)

1단계 — 기본 Interface View 작성

먼저 DB 테이블 위에 stable한 의미 단위를 만드는 Interface View입니다. 여기서는 SalesOrder 헤더를 모델링합니다.

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Sales Order - Interface View'
@VDM.viewType: #BASIC
define root view entity ZI_SalesOrder
  as select from zsales_order as Order
  composition [0..*] of ZI_SalesOrderItem as _Item
{
  key Order.so_id              as SalesOrderId,
      Order.buyer_partner      as BuyerPartnerId,
      Order.currency_code      as CurrencyCode,
      Order.gross_amount       as GrossAmount,
      Order.lifecycle_status   as LifecycleStatus,
      Order.overall_status     as OverallStatus,
      Order.created_by         as CreatedBy,
      Order.created_at         as CreatedAt,
      Order.last_changed_by    as LastChangedBy,
      Order.last_changed_at    as LastChangedAt,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      Order.local_last_changed_at as LocalLastChangedAt,

      _Item
}

이 뷰의 특징을 보면 UI나 Value Help 관련 어노테이션이 전혀 없습니다. 필드명은 비즈니스 의미를 그대로 반영한 SalesOrderId, BuyerPartnerId처럼 명사형입니다. @VDM.viewType: #BASIC은 VDM(Virtual Data Model) 분류에서 기본 계층임을 표시합니다.

2단계 — Projection View로 소비 모델 만들기

같은 데이터를 Fiori 앱이 요구하는 형태로 노출합니다. 여기서 provider contract transactional_query가 핵심이며, projection은 반드시 기반 Interface View를 as projection on으로 지정합니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Order - Projection (Fiori)'
@Metadata.allowExtensions: true
@Search.searchable: true
define root view entity ZC_SalesOrderTP
  provider contract transactional_query
  as projection on ZI_SalesOrder
{
  key SalesOrderId      as OrderNo,

      @Consumption.valueHelpDefinition: [
        { entity: { name: 'ZI_BusinessPartnerVH', element: 'PartnerId' } }
      ]
      @Search.defaultSearchElement: true
      BuyerPartnerId    as Customer,

      @Consumption.valueHelpDefinition: [
        { entity: { name: 'I_Currency', element: 'Currency' } }
      ]
      CurrencyCode      as Currency,

      @Semantics.amount.currencyCode: 'Currency'
      GrossAmount       as TotalAmount,

      LifecycleStatus   as Status,
      OverallStatus     as ProcessStatus,

      @Semantics.user.createdBy: true
      CreatedBy         as CreatedByUser,
      @Semantics.systemDateTime.createdAt: true
      CreatedAt         as CreationTime,

      /* association도 그대로 projection */
      _Item : redirected to composition child ZC_SalesOrderItemTP
}

여기서 주목할 점이 세 가지입니다. 첫째, as 키워드(alias)로 SalesOrderId → OrderNo, BuyerPartnerId → Customer처럼 UI에서 부르기 쉬운 이름으로 재정의했습니다. Interface View의 명칭은 그대로 보존되므로, 다른 use-case에서는 다른 alias를 줄 수 있습니다. 둘째, @Consumption.valueHelpDefinition은 Projection에서만 사용하는 어노테이션입니다. Interface View에 두면 모든 컨슈머가 해당 Value Help에 묶이는 강한 결합이 발생합니다. 셋째, provider contract transactional_query는 이 Projection이 RAP BO의 트랜잭션 처리에 사용된다는 계약을 선언합니다.

3단계 — 서비스 노출과 권한·확장성 고려

실제 운영 환경에서는 Projection View를 Service Definition에 등록하고, Service Binding으로 OData V4 엔드포인트를 생성합니다. 이때 Interface View는 절대 직접 expose하지 않는 것이 권장됩니다.

@EndUserText.label: 'Sales Order OData Service'
define service ZUI_SalesOrder {
  expose ZC_SalesOrderTP     as SalesOrder;
  expose ZC_SalesOrderItemTP as SalesOrderItem;
  expose ZI_BusinessPartnerVH as BusinessPartnerVH;
}

왜 Projection만 노출할까요. 운영상 세 가지 효과가 있습니다.

  1. API 호환성 보장 — Interface View가 내부적으로 리팩토링되어도 외부 OData 계약(필드명·구조)은 alias가 흡수해 줍니다.
  2. 권한 분리@AccessControl.authorizationCheck: #CHECK를 Projection 단에 두어, 같은 데이터라도 use-case별로 다른 DCL(Data Control Language) 규칙을 적용할 수 있습니다.
  3. 확장성@Metadata.allowExtensions: true 덕분에 고객·파트너가 metadata extension으로 UI 어노테이션을 덧붙일 수 있습니다. Interface View가 흔들리지 않으므로 안전합니다.

여기에 UI 어노테이션을 별도 Metadata Extension 파일로 분리하면, 같은 Projection을 데스크톱 Fiori와 모바일에서 서로 다른 레이아웃으로 보여줄 수도 있습니다.

@Metadata.layer: #CORE
annotate entity ZC_SalesOrderTP with
{
  @UI.headerInfo: { typeName: 'Sales Order',
                    typeNamePlural: 'Sales Orders',
                    title: { value: 'OrderNo' } }

  @UI.lineItem: [{ position: 10, label: 'Order' }]
  @UI.identification: [{ position: 10 }]
  OrderNo;

  @UI.lineItem: [{ position: 20, label: 'Customer' }]
  @UI.selectionField: [{ position: 10 }]
  Customer;

  @UI.lineItem: [{ position: 30, label: 'Amount' }]
  TotalAmount;
}

이 확장 파일이 Projection만 참조한다는 점이 중요합니다. UI 변경이 발생해도 데이터 모델 계층은 일절 건드리지 않습니다.

자주 부딪히는 함정과 해결

Q1. Interface View에 @UI 어노테이션을 직접 달면 어떻게 되나요.
문법 오류는 나지 않지만, 같은 Interface View를 사용하는 다른 Projection 모두가 해당 UI 설정을 상속하게 되어 분리 원칙이 깨집니다. 단기적으로 편해 보여도, 두 번째 컨슈머가 등장하는 순간 어노테이션 충돌이 일어납니다. UI/Search/Consumption 계열은 Projection 또는 Metadata Extension으로 옮기는 것이 권장됩니다.

Q2. alias로 이름을 바꿨더니 behavior implementation 클래스에서 필드를 못 찾습니다.
behavior definition은 일반적으로 Projection이 아니라 Interface View 기준의 원본 필드명을 사용합니다. projection behavior가 별도로 있긴 하지만, EML(Entity Manipulation Language) 호출 시 어떤 alias를 따르는지 혼동되기 쉽습니다. 헤더에 define behavior for ZI_SalesOrder로 선언했다면 원본명, projection behavior라면 alias명을 사용합니다.

Q3. Projection에서 새로운 계산 필드를 추가할 수 있나요.
원칙적으로 Projection은 "선택과 alias"가 중심이며 복잡한 계산식이나 join 추가는 권장되지 않습니다. 새 계산 필드가 필요하면 그것을 Interface View 또는 별도 Composite View(I_* 또는 R_*)에 두고, Projection은 그 결과를 단순히 노출하는 것이 일반적으로 권장됩니다. 이 규칙을 어기면 Projection이 점점 무거워져 계층 분리의 의미가 사라집니다.

Q4. C_와 I_ 접두어를 안 지키면 동작이 달라지나요.
기술적으로는 어떤 이름을 써도 동작합니다. 다만 SAP 표준 VDM 네이밍 컨벤션을 따르지 않으면, 팀 내 협업이나 외부 컨설턴트의 인수인계 시 의도를 즉시 알아채기 어려워집니다. I_=Interface(기본/재사용), C_=Consumption(소비), R_=Restricted/Composite 등 표기 규약을 지키는 것이 권장됩니다.

실전 패턴 정리 — 두 뷰를 한눈에 비교

지금까지 살펴본 내용을 정리하면, Interface View와 Projection View는 각각 다음과 같은 책임을 가집니다.

  • Interface View (I_*): DB 접근, 비즈니스 의미 단위 정의, Stable API, 재사용 대상, @UI/@Consumption/@Search 없음
  • Projection View (C_*): Interface View 소비, alias로 필드명 재정의, use-case별 어노테이션 부여, OData 서비스에만 노출, @Metadata.allowExtensions 활성화

실무에서 이 두 뷰의 경계가 흐려지는 가장 흔한 상황은 "편의상" Interface View에 UI 어노테이션을 추가하거나, Projection에 복잡한 JOIN 로직을 심는 경우입니다. 처음에는 빠르게 동작하지만, 컨슈머가 늘어나거나 UI 요구사항이 변경될 때 의존성이 뒤엉켜 수정 비용이 기하급수적으로 증가합니다.

RAP의 계층 원칙을 지키는 가장 실용적인 기준은 "이 변경이 데이터 모델의 변경인가, UI 표현의 변경인가"를 자문하는 것입니다. 데이터 모델 변경이라면 Interface View, UI 표현 변경이라면 Projection View(또는 Metadata Extension)를 수정합니다. 이 원칙만 지켜도 RAP 아키텍처의 유지보수성이 크게 향상됩니다.

댓글 0

아직 댓글이 없습니다.