이 글에서 다루는 내용과 도달 지점
SAP RAP(RESTful ABAP Programming Model)로 실무 서비스를 구축하다 보면 CDS View를 왜 여러 층으로 나누는지 궁금해집니다. 특히 Interface View(I_) 와 Consumption View(C_) 는 겉보기에는 비슷한 SELECT 문처럼 보이지만, 역할과 수명주기가 완전히 다릅니다. 이 글은 4-Layer 구조에서 두 View의 분리 이유와 실전 적용 패턴을 다룹니다.
- RAP 4-Layer(Basic / Composite / Interface / Consumption) 의 층별 책임 이해
- Interface View(재사용) 와 Projection View(소비) 의 명확한 차이 정리
- PurchaseOrder 예제로 3단계 확장 실습
- 실무에서 자주 발생하는 결합도 문제와 회피 패턴 학습
- Behavior Definition Projection(BDEF Projection) 과의 연결 방식 이해
미리 알고 오면 좋은 것들
ABAP CDS View 문법(DEFINE VIEW ENTITY, association, annotation)에 대한 기본 이해가 필요합니다. RAP 관리 시나리오(Managed / Unmanaged)의 큰 그림, ABAP Development Tools(ADT) for Eclipse 사용 경험, 그리고 Fiori Elements가 OData v4 메타데이터를 어떻게 소비하는지에 대한 감각이 있으면 개념이 훨씬 잘 잡힙니다.
실습 환경 및 준비물
이 글의 코드는 SAP S/4HANA 2022 온프레미스 또는 SAP BTP ABAP Environment(Steampunk) 2308 릴리스 이상을 기준으로 검증했습니다. ABAP Cloud 개발 모델을 따르므로, ADT 3.36 이상과 ABAP Language Version은 ABAP for Cloud Development 로 설정합니다. Behavior 구현은 예시 코드에서 개념 위주로만 다루며, 실제 실행을 위해서는 트랜잭션 데이터 테이블(예: ZTPO_ORDER, ZTPO_ITEM) 을 미리 만들어 두어야 합니다.
본 문서의 뷰 이름 접두어(P_, R_, I_, C_) 는 SAP Virtual Data Model(VDM) 가이드라인에서 권장되는 명명 규칙을 기준으로 재구성한 것입니다. 실제 프로젝트에서는 사내 네이밍 규칙에 맞게 조정하세요.
4-Layer 구조가 필요한 이유와 각 층의 성격
RAP의 CDS View 계층은 크게 4개로 구분됩니다. 이는 마치 건물의 배관을 설계할 때 원수(原水) → 정수 → 급수관 → 수도꼭지 로 나누는 것과 같습니다. 각 층이 명확한 책임을 가져야 유지보수 시 한쪽을 뜯어도 다른 쪽이 무너지지 않습니다.
- Basic View (R_ 또는 P_): DB 테이블에 1:1로 대응하는 최하위 층. 필드 alias, 화폐/단위 참조, 기본 association만 정의합니다. 비즈니스 로직은 넣지 않습니다.
- Composite View: 여러 Basic View를 조인·집계하여 재사용 가능한 조합을 만듭니다. 예: 주문 헤더 + 아이템 + 공급업체 정보.
- Interface View (I_): 외부(다른 앱/서비스) 에서 재사용할 수 있도록 "안정적인 계약(contract)" 을 제공하는 층. RAP의 Behavior Definition이 이 층에 붙습니다.
- Consumption View (C_) = Projection View: 특정 UI 시나리오(예: List Report, Object Page) 에 맞춰 필드를 선택하고 UI annotation을 부여하는 층. 서비스 정의(Service Definition) 가 이 층을 노출합니다.
여기서 핵심 질문. "Interface View 하나로 UI까지 그대로 노출하면 안 되나요?" 실제로 초보자가 가장 많이 저지르는 실수입니다. Interface View는 모든 소비자를 위한 최대 공약수이기 때문에, 특정 화면의 UI annotation(@UI.lineItem, @UI.facet)이나 화면 전용 필드 순서를 여기 붙이면 다른 소비자에게 오염이 전파됩니다. 반대로 Projection View는 "해당 서비스에서만 유효한 view" 이므로 마음껏 UI 옷을 입혀도 됩니다.
또한 RAP Behavior도 두 층으로 나뉩니다. Interface Layer의 define behavior for I_PurchaseOrder 는 실제 CRUD·validation·determination을 정의하고, Projection Layer의 define behavior for C_PurchaseOrder 는 "이 서비스에서 어떤 액션을 노출할지" 만 선언합니다. 이렇게 나누면 하나의 Interface Behavior에 UI별로 여러 Projection Behavior가 연결될 수 있어, 동일한 비즈니스 규칙을 여러 UX 시나리오에서 재사용할 수 있습니다.
1단계: Basic View와 Interface View 만들기
구매 주문(PurchaseOrder) 시나리오로 시작합니다. 먼저 DB 테이블 ztpo_order 를 감싸는 Basic View를 정의합니다.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Basic View - Purchase Order'
define view entity R_PurchaseOrderTP
as select from ztpo_order
{
key order_uuid as OrderUUID,
order_id as OrderID,
supplier_id as SupplierID,
order_date as OrderDate,
@Semantics.amount.currencyCode: 'CurrencyCode'
total_amount as TotalAmount,
currency_code as CurrencyCode,
overall_status as OverallStatus,
created_by as CreatedBy,
created_at as CreatedAt,
last_changed_by as LastChangedBy,
last_changed_at as LastChangedAt
}
그 위에 재사용 가능한 Interface View를 얹습니다. 여기서 중요한 것은 UI annotation을 절대 넣지 않는다는 점입니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Interface View - Purchase Order'
@Metadata.allowExtensions: true
@ObjectModel.semanticKey: [ 'OrderID' ]
define root view entity I_PurchaseOrderTP
as select from R_PurchaseOrderTP
association [0..*] to I_PurchaseOrderItemTP as _Items
on $projection.OrderUUID = _Items.OrderUUID
{
key OrderUUID,
OrderID,
SupplierID,
OrderDate,
@Semantics.amount.currencyCode: 'CurrencyCode'
TotalAmount,
CurrencyCode,
OverallStatus,
CreatedBy,
CreatedAt,
LastChangedBy,
LastChangedAt,
_Items
}
@Metadata.allowExtensions: true 는 다른 소비자가 확장 필드를 붙일 수 있도록 문을 열어두는 옵션입니다. Interface View의 핵심 목적인 "재사용성" 을 뒷받침합니다.
2단계: Projection View와 UI 시나리오 분리
이제 List Report에서 사용할 Projection(Consumption) View를 만듭니다. Interface View의 필드 중 화면에 필요한 것만 골라내고, UI annotation을 여기에서만 부여합니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Projection - Purchase Order for Buyer UI'
@Metadata.allowExtensions: true
@Search.searchable: true
define root view entity C_PurchaseOrderBuyerTP
provider contract transactional_query
as projection on I_PurchaseOrderTP
{
key OrderUUID,
@UI.lineItem: [{ position: 10, label: 'Order No.' }]
@UI.identification: [{ position: 10 }]
@Search.defaultSearchElement: true
OrderID,
@UI.lineItem: [{ position: 20, label: 'Supplier' }]
@UI.selectionField: [{ position: 10 }]
SupplierID,
@UI.lineItem: [{ position: 30, label: 'Order Date' }]
OrderDate,
@UI.lineItem: [{ position: 40, label: 'Amount' }]
@Semantics.amount.currencyCode: 'CurrencyCode'
TotalAmount,
CurrencyCode,
@UI.lineItem: [{ position: 50, criticality: 'StatusCriticality' }]
OverallStatus,
case OverallStatus
when 'A' then 3
when 'P' then 2
else 1
end as StatusCriticality,
_Items : redirected to composition child C_PurchaseOrderItemBuyerTP
}
핵심 포인트를 정리하면.
provider contract transactional_query: 이 view가 RAP 트랜잭션 시나리오의 쿼리 진입점임을 선언.- UI annotation은 오직 이 층에만 존재 → 다른 서비스가
I_PurchaseOrderTP를 재사용해도 UI 잡음이 없음. StatusCriticality처럼 화면 전용 계산 필드는 Projection에서만 생성 → Interface 오염 방지.redirected to로 자식 컴포지션도 Projection 계층에서 재연결.
3단계: 프로덕션 관점 — Behavior Projection, 서비스 정의, 권한
실무 배포 단계에서는 Behavior와 Service Definition까지 두 층으로 정리해야 안전합니다.
managed implementation in class zbp_i_purchaseordertp unique;
strict ( 2 );
with draft;
define behavior for I_PurchaseOrderTP alias PurchaseOrder
persistent table ztpo_order
draft table ztpo_order_d
etag master LastChangedAt
lock master total etag LastChangedAt
authorization master ( instance )
{
field ( readonly ) OrderUUID, CreatedBy, CreatedAt;
field ( mandatory ) SupplierID, CurrencyCode;
create;
update;
delete;
validation validateSupplier on save { create; field SupplierID; }
determination calcTotalAmount on modify { field _Items; }
association _Items { create; with draft; }
draft action Edit;
draft action Activate;
draft action Discard;
draft determine action Prepare;
}
projection;
strict ( 2 );
use draft;
define behavior for C_PurchaseOrderBuyerTP alias PurchaseOrder
{
use create;
use update;
use delete;
use action Edit;
use action Activate;
use action Discard;
use action Prepare;
use association _Items { create; with draft; }
}
@EndUserText.label: 'Purchase Order Buyer Service'
define service ZUI_PURCHASE_ORDER_BUYER {
expose C_PurchaseOrderBuyerTP as PurchaseOrder;
expose C_PurchaseOrderItemBuyerTP as PurchaseOrderItem;
expose I_Supplier as Supplier;
expose I_Currency as Currency;
}
이렇게 하면 향후 "관리자용 UI", "모바일 조회 전용 UI" 를 새로 만들 때 Interface Layer는 그대로 두고 C_PurchaseOrderAdminTP, C_PurchaseOrderMobileTP 만 추가하면 됩니다. 각 서비스는 자체 권한, 자체 UI annotation, 자체 필드 세트를 가지지만 비즈니스 규칙(validation, determination) 은 한 곳에서 관리됩니다.
실무에서 자주 밟는 지뢰와 해결 방법
Q1. Interface View에 @UI.lineItem 을 붙였더니 다른 앱에서 이상한 컬럼이 튀어나옵니다.
Interface View는 모든 서비스의 공통 계약입니다. 여기에 UI annotation을 넣으면 이를 참조하는 모든 서비스의 OData 메타데이터에 어노테이션이 전파되어, 의도치 않은 컬럼이 List Report에 나타나거나 Fiori Elements 렌더링이 꼬입니다. UI annotation은 반드시 Projection Layer로 이관하세요.
Q2. Projection View에서 새 필드를 계산해서 넣었더니 CDS 활성화 시 "field not found in projection source" 오류가 발생합니다.
Projection View는 소스 View(Interface View) 에 존재하는 필드만 노출할 수 있는 것이 원칙입니다. 단, case·cast 같은 표현식 결과는 새 이름으로 alias 할 수 있습니다. 만약 여러 소비자가 공통으로 쓸 계산이라면 Interface View나 그 하위 Composite View로 옮기는 것이 옳습니다.
Q3. Behavior를 Interface에 정의했는데 Projection Behavior에 use 를 빠뜨렸더니 "action not available" 오류가 납니다.
Projection Behavior는 "노출 화이트리스트" 역할을 합니다. Interface에 있어도 Projection에 use 로 선언하지 않으면 서비스에서 호출할 수 없습니다. 특히 draft action(Edit, Activate, Discard, Prepare) 은 하나라도 빠지면 편집 화면이 동작하지 않으니 세트로 관리하세요.
Q4. 한 View에서 다 하면 성능이 더 좋지 않나요?
런타임 관점에서는 CDS 컴파일러가 View를 flatten 하여 최종 SQL로 만들기 때문에, 층을 나눴다고 성능이 떨어지지 않습니다. 오히려 각 층이 재사용되면서 HANA 캐시 히트율이 올라갈 가능성도 있습니다. 걱정해야 할 것은 층수가 아니라 불필요한 조인과 * selection입니다.
더 깊이 파고들 자료
- SAP Help — Getting Started with the ABAP RESTful Application Programming Model
- SAP Help — CDS Projection Views 개념 및 문법
- SAP Help — Business Object Projection (Behavior Projection)
- SAP Help — Virtual Data Model(VDM) 및 View 계층 명명 규칙
댓글 0
아직 댓글이 없습니다.