ABAP

VDM 설계할 때 @ObjectModel 실수 3가지 #shorts #SAP #ABAP

1. 개요 및 이 글에서 얻어갈 것

ABAP CDS(Core Data Services)를 처음 마주하면 보통 DEFINE VIEW 한 줄로 시작하지만, 실제 S/4HANA 표준 코드를 들여다보면 Virtual Data Model(이하 VDM)이라 부르는 거대한 뷰 계층 구조가 존재합니다. 같은 데이터를 다루는 CDS 뷰가 왜 4~5개나 겹쳐 있는지, 왜 어떤 뷰에는 @ObjectModel.category가 붙고 어떤 뷰에는 @ObjectModel.representativeKey가 필수인지 이해하지 못하면, 표준과 충돌하는 커스텀 뷰를 만들어 활성화 에러에 시달리게 됩니다.

이 글에서는 다음을 정리합니다.

  • VDM의 4단 레이어(Raw/Basic/Composite/Consumption)와 책임 분리 원칙
  • @ObjectModel.category 세 가지 분류와 강제 규칙
  • BASIC_INTERFACE_VIEWrepresentativeKey 선언 방법
  • PROJECTION_VIEWexposedAssociations 패턴
  • 구매 발주(EKKO/EKPO) VDM 4단 구성 실전 예제
  • 활성화 에러 빈출 3종 및 CDS View Entity 전환 시 어노테이션 변화

2. 핵심 개념 — VDM 레이어와 @ObjectModel.category

VDM은 DB 테이블을 직접 노출하지 않는다는 단일 원칙에서 출발합니다. 모든 소비자(UI5/Fiori, OData, 분석 도구)는 의미가 부여된 CDS 뷰만 바라봐야 하고, 그 뷰들은 책임에 따라 다음과 같이 분리됩니다.

  • Raw 뷰 — DBMS 테이블을 1:1로 감싸는 가장 얕은 레이어. 컬럼명/타입만 정돈하고 의미는 부여하지 않음
  • Basic Interface View — 단일 도메인 객체(예: 발주 헤더)의 권위 있는 표현. 다른 뷰가 안전하게 재사용하는 출발점
  • Composite Interface View — 두 개 이상의 Basic 뷰를 조인/집계해 비즈니스 의미를 합성
  • Consumption/Projection View — Fiori, OData Service, Analytical Query처럼 외부에 노출되는 최종 레이어

이 레이어 구분을 코드 수준에서 강제하는 장치가 @VDM.viewType@ObjectModel.category입니다. 특히 @ObjectModel.category는 다음 세 값을 갖습니다.

  • #BASIC_INTERFACE_VIEW — 도메인의 기초 표현. representativeKey 선언이 사실상 강제됨
  • #COMPOSITE_INTERFACE_VIEW — 여러 Basic을 조합. 자체 키가 명확하지 않을 수 있어 representativeKey가 선택적
  • #PROJECTION_VIEW — 외부 소비용 슬림 뷰. exposedAssociations로 외부에 보일 관계를 명시적으로 노출
비유하자면 Basic은 원자재 카탈로그, Composite는 완성품 카탈로그, Projection은 매대에 진열된 상품입니다. 매대(Projection)는 손님(OData 소비자)에게 보일 정보만 골라 보여주고, 창고 구조(Basic 키, 내부 관계)를 그대로 노출하지 않습니다.

3. 1단계: BASIC_INTERFACE_VIEW 선언과 representativeKey

Basic Interface View는 한 비즈니스 객체의 표준 표현이 되어야 하므로, 어떤 컬럼이 그 객체의 정체성을 결정하는지 어노테이션으로 못 박아 둡니다. 이때 사용하는 것이 @ObjectModel.representativeKey이고, 값으로는 키 컬럼명을 문자열로 줍니다.

@AbapCatalog.sqlViewName: 'ZIPURCHASEORDH'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Purchase Order Header - Basic'
@VDM.viewType: #BASIC
@ObjectModel.dataCategory: #MASTER_DATA
@ObjectModel.representativeKey: 'PurchaseOrder'
@ObjectModel.usageType: {
  serviceQuality: #D,
  sizeCategory: #L,
  dataClass: #TRANSACTIONAL
}
define view Z_I_PurchaseOrder
  as select from ekko
{
  key ebeln as PurchaseOrder,
      bukrs as CompanyCode,
      lifnr as Supplier,
      bsart as PurchaseOrderType,
      aedat as CreationDate,
      ernam as CreatedByUser
}

핵심은 두 가지입니다. 첫째, key 키워드와 @ObjectModel.representativeKey 값이 일치해야 합니다. 둘째, Basic 레이어에서는 가공/계산보다는 컬럼 별칭(alias)으로 의미 있는 이름을 만들어 주는 데 집중합니다. 비즈니스 로직(상태 계산, 합계 등)은 다음 레이어로 넘깁니다.

4. 2단계: COMPOSITE_INTERFACE_VIEW 구성

Composite는 두 개 이상의 Basic을 조합해 비즈니스 의미를 만듭니다. 예를 들어 발주 헤더와 발주 아이템을 함께 보고 싶을 때, 헤더의 Basic(Z_I_PurchaseOrder)과 아이템의 Basic(Z_I_PurchaseOrderItem)을 association으로 연결한 Composite를 만듭니다.

@AbapCatalog.sqlViewName: 'ZCPURCHASEORD'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Purchase Order - Composite'
@VDM.viewType: #COMPOSITE
@ObjectModel.representativeKey: 'PurchaseOrder'
define view Z_C_PurchaseOrder
  as select from Z_I_PurchaseOrder as Header
  association [0..*] to Z_I_PurchaseOrderItem as _Item
    on $projection.PurchaseOrder = _Item.PurchaseOrder
{
  key Header.PurchaseOrder,
      Header.CompanyCode,
      Header.Supplier,
      Header.PurchaseOrderType,
      Header.CreationDate,
      // 합계 컬럼: Composite 레이어에서 의미 부여
      cast( 0 as abap.curr( 23, 2 ) ) as NetAmount,
      _Item
}

Composite는 단순 조인뿐 아니라 금액 합계, 상태 파생, 다국어 텍스트 결합처럼 도메인 지식이 필요한 가공을 담당합니다. 다만 외부 소비자가 직접 호출하기보다는, 다음 단계의 Projection 뷰가 이 Composite를 호출하도록 만드는 것이 권장 패턴입니다.

5. 3단계: PROJECTION_VIEW와 exposedAssociations

Projection 뷰는 외부에 노출되는 표면입니다. 키 알리아싱, 라벨, 필드 의미(Semantics) 외에 어떤 연관(association)을 OData/UI로 따라갈 수 있게 할지를 명시적으로 선언해야 합니다. 이때 사용하는 것이 @ObjectModel.exposedAssociations입니다.

@AbapCatalog.sqlViewName: 'ZPPURCHASEORD'
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Purchase Order - Projection'
@VDM.viewType: #CONSUMPTION
@ObjectModel.semanticKey: ['PurchaseOrder']
@ObjectModel.representativeKey: 'PurchaseOrder'
@ObjectModel.exposedAssociations: ['_Item', '_Supplier']
define view Z_P_PurchaseOrder
  as select from Z_C_PurchaseOrder
  association [1..1] to Z_I_Supplier as _Supplier
    on $projection.Supplier = _Supplier.Supplier
{
  key PurchaseOrder,
      @EndUserText.label: 'Company'
      CompanyCode,
      @ObjectModel.foreignKey.association: '_Supplier'
      Supplier,
      PurchaseOrderType,
      CreationDate,
      NetAmount,
      _Item,
      _Supplier
}

여기서 두 가지를 짚어둡니다. 첫째, exposedAssociationsSELECT 리스트에 association 이름을 포함한 뒤에만 의미가 있습니다. 선언만 하고 SELECT에서 빠뜨리면 활성화는 되지만 OData에 노출되지 않습니다. 둘째, @ObjectModel.foreignKey.association으로 외래 키 컬럼과 association을 묶어 두면, Fiori Elements가 자동으로 Value Help/내비게이션을 인식하기 쉬워집니다.

6. 4단계: 실전 VDM 레이어 구성 예제 — 구매 발주

네 레이어를 한 번에 보면 구조가 분명해집니다. EKKO/EKPO를 기반으로 한 일반적인 분기는 다음과 같습니다.

  • Raw(선택): Z_R_EKKO — EKKO를 1:1로 감싸고 ABAP 친화적 컬럼명 부여
  • Basic: Z_I_PurchaseOrder, Z_I_PurchaseOrderItem, Z_I_Supplier — 각자 representativeKey 명시
  • Composite: Z_C_PurchaseOrder — 헤더+아이템 합산, 금액 단위 통일
  • Projection: Z_P_PurchaseOrder — Fiori/OData v2/v4 소비용, exposedAssociations로 항해 제한

아이템 Basic 예시는 다음과 같이 헤더 키를 부분 키로 갖고, 자체 키(PurchaseOrderItem)를 representativeKey로 둡니다.

@VDM.viewType: #BASIC
@ObjectModel.representativeKey: 'PurchaseOrderItem'
define view Z_I_PurchaseOrderItem
  as select from ekpo
  association [1..1] to Z_I_PurchaseOrder as _Header
    on $projection.PurchaseOrder = _Header.PurchaseOrder
{
  key ebeln as PurchaseOrder,
  key ebelp as PurchaseOrderItem,
      matnr as Material,
      menge as Quantity,
      netwr as NetAmount,
      waers as Currency,
      _Header
}

이렇게 분리해 두면 추후 매입 데이터 분석용 Analytical Query, 모바일용 Slim Projection, 통합용 OData v4 Service 등 새로운 소비자가 등장해도 Basic/Composite는 건드리지 않고 Projection만 새로 만드는 진화 경로가 열립니다.

7. 자주 겪는 함정과 FAQ — 활성화 에러 3가지

Q1. "Annotation @ObjectModel.representativeKey: value does not match key field" 에러가 납니다.

A. key로 선언한 컬럼명과 representativeKey 값이 일치하지 않을 때 발생합니다. 두 가지를 확인하세요. 첫째, alias로 바꾼 이름(PurchaseOrder)을 써야 하지 원본 컬럼명(ebeln)을 쓰면 안 됩니다. 둘째, 복합 키일 경우 representativeKey는 하나의 대표 키만 받으므로, 가장 식별성 높은 컬럼을 선택합니다.

Q2. "Exposed association _Item is not selected in the SELECT list" 에러가 납니다.

A. @ObjectModel.exposedAssociations에 이름을 적었지만 SELECT 리스트에서 _Item을 빠뜨린 경우입니다. Projection 뷰의 컬럼 목록 끝에 _Item처럼 association 이름을 그대로 포함해야 합니다. 또한 association이 source 뷰(Z_C_PurchaseOrder)에 정의되어 있어야 상속받아 노출할 수 있습니다.

Q3. 표준 SAP VDM과 이름이 충돌하거나, 활성화 시 "Foreign Key Association is missing" 경고가 납니다.

A. 우선 커스텀 뷰는 Z_/Y_ 접두어와 함께 Z_I_(Basic), Z_C_(Composite), Z_P_(Projection) 접두어 컨벤션을 따르세요. 또한 SAP 표준 Basic이 이미 있는 도메인(예: I_Supplier)이라면 재정의 대신 재사용하는 것이 권장됩니다. Foreign Key 경고는 외래 키 컬럼에 @ObjectModel.foreignKey.association을 붙이면 해소되는 경우가 많습니다.

그 밖에 자주 묻는 질문

  • Composite에서 representativeKey를 꼭 써야 하나요? — 단일 도메인을 확장한 Composite라면 권장됩니다. 다중 도메인 조인이면 생략하거나 가장 강한 키를 명시합니다.
  • Raw 뷰가 꼭 필요한가요? — 일반적으로 작은 도메인에서는 Raw를 생략하고 Basic부터 시작해도 충분합니다.
  • Projection에서 @AccessControl.authorizationCheck는 어떻게 잡나요? — 외부 노출 단계이므로 일반적으로 #CHECK를 권장하고, Basic/Composite는 상황에 따라 #NOT_REQUIRED를 둡니다.

8. 응용 패턴 — CDS View Entity 전환 시 변화

S/4HANA 2020 이후 등장한 CDS View Entity(DEFINE VIEW ENTITY)는 기존 DEFINE VIEW(DDIC 기반)의 후속 모델입니다. ABAP 플랫폼 2022 이상이나 BTP ABAP Environment에서는 신규 개발 시 View Entity가 권장됩니다. 어노테이션 측면에서 주요 변화는 다음과 같습니다.

  • @AbapCatalog.sqlViewName제거됩니다(SQL View가 생성되지 않으므로 불필요).
  • @AbapCatalog.compiler.compareFilter, @AbapCatalog.preserveKey 등 DDIC 호환 어노테이션도 사라집니다.
  • @VDM.viewType@ObjectModel.* 어노테이션은 대부분 그대로 유효합니다. 다만 @ObjectModel.usageType 일부 하위 속성은 RAP(Restful ABAP Programming) 관점에서 재해석됩니다.
  • 외부 노출은 점차 Service Definition + Service Binding(RAP) 모델로 이동합니다. Projection 뷰는 여전히 만들되, OData 노출은 Binding으로 분리하는 것이 일반적인 패턴입니다.
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PO Header - View Entity'
@VDM.viewType: #BASIC
@ObjectModel.representativeKey: 'PurchaseOrder'
define view entity Z_I_PurchaseOrder_E
  as select from ekko
{
  key ebeln as PurchaseOrder,
      bukrs as CompanyCode,
      lifnr as Supplier,
      bsart as PurchaseOrderType,
      aedat as CreationDate
}

정리하자면, VDM은 레이어를 나누는 규율이고 @ObjectModel은 그 규율을 코드에 박아 두는 어휘입니다. Basic은 representativeKey로 정체성을 확정하고, Composite는 의미를 합성하며, Projection은 exposedAssociations로 외부 경로를 통제합니다. 이 세 가지 패턴을 일관되게 적용하면, 표준 SAP VDM과 충돌하지 않으면서 Fiori/OData/Analytics 어떤 소비자가 오더라도 같은 Basic을 재사용하는 확장 가능한 데이터 모델을 만들 수 있습니다. 신규 개발이라면 처음부터 define view entity 기반으로 시작하고, 추후 RAP Business Object와 자연스럽게 연결되도록 Projection 레이어를 얇게 유지하는 것을 권장합니다.

댓글 0

아직 댓글이 없습니다.