이 글의 목적과 도달점
SAP RAP(ABAP RESTful Application Programming Model)로 Fiori Elements 앱을 개발하다 보면, 개발 초기에는 @UI 어노테이션을 CDS 뷰 안에 직접 넣는 경우가 많습니다. 처음에는 편리해 보이지만, 필드가 30개를 넘어가고 List Report/Object Page 레이아웃이 복잡해질수록 CDS 뷰는 "데이터 정의"와 "UI 정의"가 뒤엉킨 스파게티가 됩니다. 이 글에서는 Metadata Extension(MDE)을 별도 아티팩트로 분리해야 하는 이유를 유지보수 관점에서 파헤치고, 실무 시나리오(구매주문·판매송장)로 리팩터링 과정을 단계별로 보여줍니다.
- CDS 뷰에 직접
@UI를 박아 넣었을 때 발생하는 세 가지 대표 문제 이해 - Metadata Extension의 역할과 활성화 메커니즘 파악
- PurchaseOrder 시나리오로 MDE 분리 리팩터링 실습
- 레이어 오버라이드·환경별 어노테이션 관리·성능 이슈 대응 방법 학습
사전에 알고 있어야 할 것들
이 글은 RAP의 기본 구조(Behavior Definition, Service Definition, Service Binding)를 한 번이라도 다뤄본 개발자를 대상으로 합니다. ADT(ABAP Development Tools) in Eclipse 사용 경험, CDS DDL(Data Definition Language)의 define view entity 구문 이해, Fiori Elements List Report/Object Page의 최소한의 개념(LineItem, HeaderInfo, FieldGroup)이 있으면 무리 없이 따라올 수 있습니다.
실습 환경과 준비물
본문 코드는 다음 환경을 기준으로 검증되는 것이 일반적입니다.
- SAP S/4HANA Cloud, ABAP Environment (Steampunk) 또는 SAP BTP, ABAP Environment 2308 이상
- 온프레미스: SAP S/4HANA 2022 FPS02 이상 (RAP managed/unmanaged 모두 지원)
- ABAP Development Tools (ADT) 3.36 이상 설치된 Eclipse 2023-12 또는 2024-03
- Fiori Elements Preview 사용을 위한 Service Binding Type:
OData V4 - UI - 테스트용 사용자 롤:
SAP_BR_DEVELOPER또는 이에 준하는 개발 권한
MDE 파일은 ADT에서 New → Other ABAP Repository Object → Core Data Services → Metadata Extension 경로로 생성합니다. 확장자는 별도로 없지만 내부적으로 DDLX(DDL Extension) 소스로 저장된다는 점을 기억해두면 문서를 찾을 때 편합니다.
왜 분리해야 하는가 — 핵심 개념 파고들기
CDS 뷰는 본질적으로 데이터 모델을 정의하는 아티팩트입니다. 필드가 어디서 오는지, 어떤 조인·집계·필터가 걸리는지, 어떤 associations이 있는지를 서술합니다. 반면 @UI 어노테이션은 그 데이터를 어떻게 화면에 표시할지를 지시하는 프레젠테이션 계층 정보입니다. 두 관심사가 한 파일 안에 섞이면 다음과 같은 문제가 발생합니다.
비유하자면, 데이터베이스 스키마 SQL 스크립트에 CSS를 함께 적어 놓는 것과 같습니다. 당장은 파일 하나로 관리되어 편해 보이지만, 디자이너가 색상만 바꾸려 해도 DBA의 리뷰가 필요해집니다.
문제 1: 활성화 파급 효과 (Activation Blast Radius) — CDS 뷰에 어노테이션 한 줄만 수정해도 뷰 자체가 재활성화됩니다. 대형 뷰의 경우 dependent view 수십 개가 함께 재검증되며, 잘못하면 dictionary lock까지 걸립니다. MDE는 CDS 뷰의 활성 상태에 영향을 주지 않고 어노테이션만 다시 반영합니다.
문제 2: 재사용 뷰 오염 — 하나의 CDS 뷰가 여러 서비스에서 재사용되는 경우, List Report용 어노테이션과 Analytical Query용 어노테이션이 충돌합니다. MDE는 Layer 개념을 통해 서비스별로 다른 UI 어노테이션을 얹을 수 있습니다.
문제 3: 코드 리뷰 지옥 — Git diff에서 데이터 로직 변경과 UI 순서 변경이 뒤섞여, 리뷰어가 "이 커밋에서 실제 SQL이 바뀐 게 뭔지" 판별하기 어려워집니다.
MDE의 동작 원리는 다음과 같습니다. 런타임에 CDS 프레임워크는 원본 뷰의 어노테이션과 MDE의 어노테이션을 merge합니다. Layer 우선순위는 #CORE < #LOCALIZATION < #INDUSTRY < #PARTNER < #CUSTOMER 순으로 뒤에 오는 레이어가 앞의 값을 덮어씁니다. 이 구조 덕분에 SAP 표준 어노테이션을 유지하면서 고객사 커스터마이징 어노테이션을 얹는 다층 아키텍처가 가능합니다.
실전 예제 1단계 — 안티패턴부터 이해하기
먼저 흔히 만나는 "잘못된 예"를 살펴봅니다. 구매주문 헤더를 표시하는 CDS 뷰에 UI 어노테이션이 직접 박혀 있는 형태입니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Purchase Order Consumption View'
@Metadata.allowExtensions: true
@Search.searchable: true
@UI.headerInfo: { typeName: 'Purchase Order',
typeNamePlural: 'Purchase Orders',
title: { value: 'PurchaseOrderId' } }
define view entity ZC_PURCHASEORDER_UI
as select from zpo_header as Header
association [0..*] to ZC_POITEM_UI as _Items
on $projection.PurchaseOrderId = _Items.PurchaseOrderId
{
@UI.facet: [ { id: 'GeneralInfo',
type: #COLLECTION,
label: 'General Information',
position: 10 } ]
@UI.lineItem: [ { position: 10, label: 'PO Number' } ]
@UI.identification: [ { position: 10, label: 'PO Number' } ]
key Header.po_id as PurchaseOrderId,
@UI.lineItem: [ { position: 20, label: 'Supplier' } ]
@UI.selectionField: [ { position: 10 } ]
@Search.defaultSearchElement: true
Header.supplier_id as SupplierId,
@UI.lineItem: [ { position: 30, label: 'Order Date' } ]
Header.order_date as OrderDate,
@UI.lineItem: [ { position: 40, label: 'Total Amount' } ]
@Semantics.amount.currencyCode: 'CurrencyCode'
Header.total_amount as TotalAmount,
Header.currency_code as CurrencyCode,
_Items
}
이 코드는 동작은 하지만, 필드가 40~50개 되는 실제 구매주문에서 어노테이션이 필드 사이사이에 끼어들며 뷰 전체가 400줄을 넘어갑니다. 특히 @UI.headerInfo가 select 절 위에 있고 @UI.lineItem은 필드 옆에 붙어 있어 UI 정의가 파일 전체에 흩어져 있다는 점이 유지보수 시 큰 부담이 됩니다.
실전 예제 2단계 — Metadata Extension으로 분리하기
먼저 CDS 뷰에서 @UI 어노테이션을 모두 제거하고, extension을 허용하도록 @Metadata.allowExtensions: true만 유지합니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Purchase Order Consumption View'
@Metadata.allowExtensions: true
@Search.searchable: true
define view entity ZC_PURCHASEORDER_UI
as select from zpo_header as Header
association [0..*] to ZC_POITEM_UI as _Items
on $projection.PurchaseOrderId = _Items.PurchaseOrderId
{
key Header.po_id as PurchaseOrderId,
@Search.defaultSearchElement: true
Header.supplier_id as SupplierId,
Header.order_date as OrderDate,
@Semantics.amount.currencyCode: 'CurrencyCode'
Header.total_amount as TotalAmount,
Header.currency_code as CurrencyCode,
_Items
}
다음으로 별도의 MDE 파일 ZC_PURCHASEORDER_UI(같은 이름)를 생성합니다.
@Metadata.layer: #CUSTOMER
@UI: { headerInfo: { typeName: 'Purchase Order',
typeNamePlural: 'Purchase Orders',
title: { type: #STANDARD, value: 'PurchaseOrderId' },
description: { value: 'SupplierId' } } }
annotate view ZC_PURCHASEORDER_UI with
{
@UI.facet: [ { id: 'POHeader',
purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE,
label: 'Purchase Order Header',
position: 10 },
{ id: 'POItems',
purpose: #STANDARD,
type: #LINEITEM_REFERENCE,
label: 'Items',
position: 20,
targetElement: '_Items' } ]
@UI: { lineItem: [ { position: 10, importance: #HIGH, label: 'PO Number' } ],
identification: [ { position: 10, label: 'PO Number' } ],
selectionField: [ { position: 10 } ] }
PurchaseOrderId;
@UI: { lineItem: [ { position: 20, importance: #HIGH, label: 'Supplier' } ],
identification: [ { position: 20, label: 'Supplier' } ],
selectionField: [ { position: 20 } ] }
SupplierId;
@UI: { lineItem: [ { position: 30, label: 'Order Date' } ],
identification: [ { position: 30, label: 'Order Date' } ] }
OrderDate;
@UI: { lineItem: [ { position: 40, label: 'Total Amount' } ],
identification: [ { position: 40, label: 'Total Amount' } ] }
TotalAmount;
}
이제 데이터 모델과 UI 정의가 완전히 분리되었습니다. CDS 뷰 파일을 열면 데이터 흐름만 눈에 들어오고, MDE 파일을 열면 화면 배치만 집중해서 볼 수 있습니다. @Metadata.layer: #CUSTOMER 선언은 이 어노테이션이 고객 레이어에 위치함을 의미하며, 만약 파트너가 별도 MDE로 어노테이션을 제공했다면 그 위에 우선 적용됩니다.
실전 예제 3단계 — 프로덕션 관점의 다층 관리와 검증
실무에서는 하나의 CDS 뷰에 여러 MDE를 붙여 시나리오별로 UI를 분기시키는 것이 일반적입니다. 예를 들어 승인자용 화면과 조달 담당자용 화면에서 표시 필드와 순서가 다르다면, 서비스 정의를 분리하고 MDE도 분리합니다.
@Metadata.layer: #PARTNER
@UI.presentationVariant: [{
sortOrder: [{ by: 'OrderDate', direction: #DESC }],
visualizations: [{ type: #AS_LINEITEM }]
}]
annotate view ZC_PURCHASEORDER_UI with
{
@UI.lineItem: [ { position: 15,
label: 'Approval Status',
criticality: 'ApprovalCriticality' } ]
@UI.hidden: false
ApprovalStatus;
}
배포 전 검증 체크리스트로 다음 항목을 확인하는 것이 권장됩니다. 첫째, ADT의 F2 Element Info로 최종 병합된 어노테이션을 확인하여 의도한 레이어가 우선 적용되었는지 봅니다. 둘째, Service Binding의 Preview에서 Fiori Elements List Report를 열어 라벨·순서·필터가 예상대로 나오는지 확인합니다. 셋째, 성능 관점에서 @UI.selectionField가 붙은 필드에는 반드시 DB 인덱스가 존재하는지 검증합니다. 넷째, ABAP Test Cockpit(ATC)에서 CDS_ANNO 관련 체크를 실행해 라벨 누락이나 중복 position 값을 잡아냅니다.
보안 측면에서는 @UI.hidden이 절대 접근 제어 수단이 아니라는 점을 반드시 기억해야 합니다. UI에서만 숨겨질 뿐 OData 응답에는 여전히 포함되므로, 민감 필드는 CDS DCL(Data Control Language)이나 프로젝션 뷰 자체에서 제외해야 합니다.
흔히 마주치는 함정과 해결법
Q1. MDE를 만들었는데 Fiori Preview에 반영되지 않습니다. 원본 CDS 뷰에 @Metadata.allowExtensions: true가 있는지 먼저 확인하세요. 이 어노테이션이 없으면 MDE 자체는 활성화되어도 무시됩니다. 또한 MDE 이름은 원본 뷰 이름과 정확히 같아야 하며, 대소문자까지 일치해야 합니다.
Q2. 같은 필드에 대해 두 MDE가 서로 다른 값을 지정하면 어떻게 되나요? Layer 우선순위에 따라 뒤 레이어(#CUSTOMER가 가장 강함)가 이깁니다. 같은 레이어 내에서 충돌이 발생하면 활성화 시 오류가 나거나 정의되지 않은 순서로 병합되므로, 같은 레이어에서 같은 필드를 중복 정의하지 않도록 주의합니다.
Q3. CDS 뷰에 @UI를 두는 것과 MDE로 빼는 것의 런타임 성능 차이가 있나요? 런타임 요청 처리 자체의 성능 차이는 실질적으로 무시할 수준입니다. 그러나 활성화 시간과 메타데이터 캐시 무효화 범위에서 유의미한 차이가 있습니다. MDE 수정은 뷰 자체를 재컴파일하지 않으므로 개발 사이클이 훨씬 짧아집니다.
Q4. 이미 @UI가 CDS에 박혀 있는 legacy 뷰를 마이그레이션할 때 팁이 있나요? ADT의 Quick Fix — Extract Annotations to Metadata Extension 기능을 활용하면 어노테이션을 자동으로 추출해 새 MDE 파일로 옮겨줍니다. 다만 @Semantics, @Search, @AccessControl 같은 데이터 성격의 어노테이션은 CDS에 그대로 두어야 한다는 원칙을 지켜야 합니다. MDE는 어디까지나 프레젠테이션 어노테이션(@UI, @EndUserText.label 일부, @ObjectModel UI 관련 하위 속성)을 위한 공간입니다.
Q5. Transport 관점에서 MDE는 어떻게 관리되나요? MDE는 원본 CDS 뷰와 독립된 개발 오브젝트이므로 별도 transport request에 담길 수 있습니다. 그러나 원본 뷰의 필드가 삭제되었는데 MDE가 그 필드를 참조하고 있으면 활성화가 실패합니다. 이런 종속성 때문에 실무에서는 뷰와 MDE를 같은 TR에 묶는 것이 일반적으로 권장됩니다.
여기서 확장해볼 만한 주제들
MDE 구조에 익숙해졌다면 다음 주제로 나아가 볼 수 있습니다. Value Help를 위한 @Consumption.valueHelpDefinition과 MDE에서의 활용, Draft 활성화된 RAP 시나리오에서의 @UI.createHidden·@UI.updateHidden 동적 제어, Behavior Definition의 Determinations와 UI 어노테이션 간 상호작용, 그리고 SAP Build Work Zone에 배포할 때 필요한 @UI.headerInfo.imageUrl과 커스텀 아이콘 관리 등이 다음 학습 후보입니다. 특히 Adaptation Project를 통해 SAP 표준 앱의 어노테이션을 확장하는 방법을 익혀두면, 표준-파트너-고객 3계층 어노테이션 전략을 실전에서 활용할 수 있습니다.
댓글 0
아직 댓글이 없습니다.