ABAP

CDS View 수정 없이 커스텀 필드 추가하는 3가지 방법 #shorts #SAP #ABAP

1. 왜 표준 CDS View를 직접 수정하면 안 되는가

S/4HANA 도입 프로젝트에서 가장 흔하게 마주치는 유혹 중 하나는 "SAP 표준 CDS View에 필드 하나만 슬쩍 추가하면 되지 않을까"라는 발상입니다. 예를 들어 I_SalesOrderItem에 자사 특화 프로모션 코드 필드 하나만 넣으면 Fiori 앱, OData 서비스, 임베디드 애널리틱스가 한 번에 해결될 것처럼 보입니다. 그러나 표준 오브젝트 수정(Modification)은 다음 세 가지 근본 문제를 피할 수 없습니다.

  • 업그레이드 충돌: SAP가 다음 FPS(Feature Pack Stack)에서 동일 뷰를 변경하면 SPAU_ENH에서 매번 수동 병합이 필요합니다. Clean Core 원칙에 정면으로 위배됩니다.
  • 지원 리스크: 표준 오브젝트 수정 상태에서 SAP Note를 적용하거나 인시던트를 제기하면 일반적으로 "원복 후 재현" 요구를 받습니다.
  • RAP/Fiori 의존성 파괴: 표준 뷰는 CDS View Entity, Metadata Extension, Behavior Definition이 서로 얽혀 있어 한 지점의 수정이 전체 프레임워크의 재활성화를 유발합니다.

이 글에서는 표준 뷰를 건드리지 않고도 커스텀 필드를 노출하는 정공법인 CDS View ExtensionExtension Field BAdI 조합을 다룹니다.

2. CDS Extension View 개념과 동작 원리

CDS Extension View는 개념적으로 "원본 뷰의 SELECT 리스트에 컬럼을 이어 붙이는 별도의 개발 오브젝트"입니다. ABAP Dictionary의 Append Structure가 테이블에 필드를 이어 붙이는 것과 같은 발상이 CDS 레이어로 올라온 것으로 이해하면 편합니다. 다만 결정적으로 다른 점은, Extension은 소스 뷰의 활성 버전을 물리적으로 바꾸지 않고, 활성화 시점에 프레임워크가 두 소스를 병합한 결과물을 런타임 뷰로 만든다는 점입니다.

비유하자면 표준 뷰는 완성된 액자이고, Extension은 그 액자 옆에 덧대는 확장 프레임입니다. 액자 원본을 뜯지 않고도 새 그림 조각을 오른쪽에 붙여 하나의 그림처럼 보이게 만드는 셈입니다.

내부 동작은 다음 순서로 요약됩니다.

  1. Activation 요청이 오면 프레임워크가 extend view entity 소스를 파싱합니다.
  2. 원본 뷰의 SELECT 리스트에 Extension의 필드가 논리적으로 추가됩니다.
  3. HANA에서는 원본 뷰의 CDS 런타임 아티팩트가 재생성되며, 추가된 필드가 컬럼 뷰에 포함됩니다.
  4. OData/RAP 서비스 노출 시, Metadata Extension 또는 Service Definition에서 해당 필드를 참조할 수 있게 됩니다.

Extension은 필드 정의만 담당하고, 값이 어디서 오는지는 별도의 계약(Association 재사용 또는 BAdI)을 통해서만 결정됩니다.

[표준 뷰 I_SalesOrderItem]
        │  (extend view entity)
        ▼
[Z_SalesOrderItem_Ext]  ── 커스텀 필드 정의 + Association
        │
        ▼
[Extension Field BAdI]  ── 커스텀 테이블에서 값 조회
        │
        ▼
[Fiori Elements / OData]  ── Annotation으로 UI 노출

3. Extension Field 추가 준비 — 커스텀 테이블 설계

판매 오더 아이템별 프로모션 코드와 로열티 포인트를 저장할 커스텀 테이블을 준비합니다. 표준 아이템의 키(VBELN + POSNR)와 동일한 키 구조를 갖는 것이 Join 편의상 유리합니다.

@EndUserText.label : 'SO Item Custom Attributes'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
define table zso_item_cust {
  key client       : mandt not null;
  key sales_order  : vbeln_va not null;
  key item_number  : posnr_va not null;
  promo_code       : abap.char(20);
  loyalty_point    : abap.dec(13,2);
  campaign_id      : abap.char(10);
  created_at       : timestampl;
}

테이블 설계 시 중요한 원칙은 키 필드 순서입니다. client → sales_order → item_number 순으로 표준 뷰 키와 완전히 일치시켜야 Association ON 조건에서 불필요한 Full Scan이 발생하지 않습니다. 특히 HANA에서는 컬럼 스토어 기반이라 순서 자체가 I/O 비용에 직접 영향을 줍니다.

Extension View를 작성하기 전에 대상 표준 뷰 소스에서 @Metadata.allowExtensions: true가 선언되어 있는지 반드시 먼저 확인하세요. ADT에서 I_SalesOrderItem을 열고 Properties 탭의 Annotations를 확인하거나, Source를 직접 열어 어노테이션 목록을 스캔하는 것이 가장 확실합니다. 이 어노테이션이 없으면 활성화 시 Extension of view is not allowed 오류가 발생합니다.

4. extend view 문법 실전 예제 — SalesOrder 시나리오

이제 Z-테이블을 표준 뷰에 연결하는 Extension View를 정의합니다. 핵심은 extend view entity 구문과 왼쪽 조인용 Association입니다.

@EndUserText.label: 'Extension: SO Item Promotion Fields'
extend view entity I_SalesOrderItem
  with Z_SalesOrderItem_PromoExt
  association [0..1] to zso_item_cust as _PromoData
    on  $projection.SalesOrder     = _PromoData.sales_order
    and $projection.SalesOrderItem = _PromoData.item_number
{
  _PromoData.promo_code    as PromotionCode,
  _PromoData.loyalty_point as LoyaltyPoint,
  _PromoData.campaign_id   as CampaignId,
  _PromoData
}

활성화 후 ADT의 Data Preview로 I_SalesOrderItem을 열면 오른쪽에 세 개의 새 컬럼이 나타납니다. 원본 소스는 한 줄도 바뀌지 않았지만 결과 뷰는 확장된 상태입니다.

Association의 Cardinality를 [0..1]로 지정하는 이유가 있습니다. 커스텀 데이터가 없는 오더 아이템도 존재할 수 있기 때문에 [1..1]로 설정하면 해당 아이템이 결과에서 누락될 위험이 있습니다. JOIN semantics 관점에서 [0..1]은 LEFT OUTER JOIN을 의미하므로, 커스텀 데이터 없는 아이템도 NULL 값으로 정상 반환됩니다.

필드 이름을 as PromotionCode처럼 CamelCase로 alias하는 것도 중요합니다. 표준 CDS View의 필드명 컨벤션과 일치시켜야 OData 서비스 노출 시 자동으로 camelCase property명으로 변환되는 RAP 프레임워크 규칙에 맞게 됩니다.

5. BAdI 연동: 커스텀 필드 값을 동적으로 채우기

모든 커스텀 필드가 테이블에서 곧바로 오는 것은 아닙니다. LoyaltyPoint가 오더 금액과 캠페인 정책에 따라 실시간 계산되어야 한다면, 테이블 조인 대신 확장 BAdI를 사용해야 합니다.

CLASS zcl_so_item_promo_calc DEFINITION
  PUBLIC FINAL CREATE PUBLIC.

  PUBLIC SECTION.
    INTERFACES if_badi_sd_so_item_extens.

  PRIVATE SECTION.
    METHODS calc_loyalty
      IMPORTING iv_net_amount    TYPE netwr_ap
                iv_campaign_id   TYPE char10
      RETURNING VALUE(rv_point)  TYPE p LENGTH 13 DECIMALS 2.
ENDCLASS.

CLASS zcl_so_item_promo_calc IMPLEMENTATION.

  METHOD if_badi_sd_so_item_extens~enrich_read.
    LOOP AT it_items ASSIGNING FIELD-SYMBOL(<ls_item>).
      TRY.
          DATA(lv_point) = calc_loyalty(
            iv_net_amount  = <ls_item>-net_amount
            iv_campaign_id = <ls_item>-campaign_id ).

          APPEND VALUE #(
            sales_order   = <ls_item>-sales_order
            item_number   = <ls_item>-item_number
            field_name    = 'LOYALTYPOINT'
            field_value   = |{ lv_point NUMBER = USER }|
          ) TO ct_ext_fields.

        CATCH cx_root INTO DATA(lx_err).
          MESSAGE lx_err TYPE 'S' DISPLAY LIKE 'W'.
      ENDTRY.
    ENDLOOP.
  ENDMETHOD.

  METHOD calc_loyalty.
    DATA(lv_rate) = SWITCH decfloat16(
      iv_campaign_id
      WHEN 'SUMMER25' THEN CONV decfloat16( '0.03' )
      WHEN 'WINTER25' THEN CONV decfloat16( '0.05' )
      ELSE CONV decfloat16( '0.01' ) ).

    rv_point = iv_net_amount * lv_rate.
  ENDMETHOD.

ENDCLASS.

BAdI 구현 시 가장 중요한 원칙은 예외를 삼켜서 트랜잭션 흐름을 보호하는 것입니다. 커스텀 로직 오류가 표준 기능까지 중단시키면 사용자 신뢰가 무너집니다. CATCH 블록에서는 반드시 Application Log(BALI)를 활용해 오류를 추적 가능한 형태로 남기는 것을 권장합니다.

성능 관점에서는 아이템 100건짜리 오더 조회에서 BAdI가 100번 개별 호출되면 응답이 급격히 느려집니다. 캠페인 마스터 데이터처럼 빈번히 참조되는 데이터는 Class-level Static 변수로 버퍼링하는 패턴을 적용합니다.

CLASS zcl_promo_master_buffer DEFINITION
  PUBLIC FINAL CREATE PRIVATE.

  PUBLIC SECTION.
    CLASS-METHODS get_rate
      IMPORTING iv_campaign TYPE char10
      RETURNING VALUE(rv_rate) TYPE decfloat16.

  PRIVATE SECTION.
    CLASS-DATA gt_cache TYPE HASHED TABLE OF zpromo_rate
                       WITH UNIQUE KEY campaign_id.
    CLASS-METHODS load_cache.
ENDCLASS.

CLASS zcl_promo_master_buffer IMPLEMENTATION.
  METHOD get_rate.
    IF gt_cache IS INITIAL.
      load_cache( ).
    ENDIF.
    rv_rate = COND #(
      WHEN line_exists( gt_cache[ campaign_id = iv_campaign ] )
      THEN gt_cache[ campaign_id = iv_campaign ]-rate
      ELSE '0.01' ).
  ENDMETHOD.

  METHOD load_cache.
    SELECT campaign_id, rate
      FROM zpromo_rate
      INTO TABLE @gt_cache.
  ENDMETHOD.
ENDCLASS.

6. Fiori Elements UI 반영 — CDS Annotation으로 커스텀 필드 노출

필드가 CDS 레이어에 존재해도 Fiori Elements 앱이 자동으로 화면에 그려주지는 않습니다. Metadata Extension을 통해 UI Annotation을 별도로 붙여야 합니다. 이 역시 원본 CDS를 건드리지 않는 방식입니다.

@Metadata.layer: #CUSTOMER
annotate view I_SalesOrderItem with
{
  @UI.lineItem: [ { position: 110, label: 'Promo Code' } ]
  @UI.identification: [ { position: 110, label: 'Promo Code' } ]
  PromotionCode;

  @UI.lineItem: [ { position: 120, label: 'Loyalty Pt.' } ]
  @Semantics.amount.currencyCode: 'TransactionCurrency'
  LoyaltyPoint;
}

Metadata Extension은 #CUSTOMER 레이어에 두면 SAP 표준 레이어의 어노테이션을 덮어쓰지 않고 병합됩니다. Service Binding을 다시 활성화한 뒤 Fiori 앱 프리뷰를 열면 List Report 컬럼과 Object Page 필드에 새 열이 자연스럽게 붙습니다.

주의할 점은 position 값 설정입니다. 표준 필드들이 이미 10, 20, 30... 단위로 position을 점유하고 있으므로, 커스텀 필드는 100번대 이상의 높은 숫자를 부여하거나 표준 Metadata Extension의 position 목록을 먼저 확인한 뒤 겹치지 않는 번호를 선택해야 합니다.

7. 운영 환경 고려사항 — Transport, 활성화 순서, 의존성

Extension은 여러 오브젝트가 얽혀 있어 이송 순서가 어긋나면 QAS/PRD에서 활성화 오류가 납니다. 실무에서 권장하는 이송 순서는 다음과 같습니다.

  1. Domain, Data Element, Z-테이블 (DDIC)
  2. 커스텀 마스터 조회용 CDS View (있다면)
  3. Extension View (extend view entity ...)
  4. Metadata Extension (UI Annotation)
  5. BAdI 구현 클래스와 Enhancement Implementation
  6. Access Control(DCL)
  7. Service Definition/Binding 재활성화

운영 배포 전 체크리스트로 두면 좋은 항목들입니다.

  • 표준 뷰가 향후 SAP 업데이트로 allowExtensions: false로 바뀌지 않는지 릴리즈 노트 확인
  • HANA 컬럼 뷰 재생성 시간이 큰 테이블(VBAP 등)에서 다운타임에 미치는 영향 사전 측정
  • Where-Used 분석: 확장 필드가 다른 CDS의 SELECT에 포함되어 순환 의존을 만들지 않는지 점검
  • Access Control(DCL)로 민감 커스텀 필드에 대한 권한 제어 설정

Import Queue에서 다중 이송 시 Preselect + Import Together 옵션을 사용하면 순서 문제를 줄일 수 있습니다.

8. 실무에서 자주 발생하는 오류와 해결법

활성화 시 "Extensions are not allowed for view I_XXX" 오류
대상 표준 뷰가 @Metadata.allowExtensions: true를 선언하지 않은 경우입니다. 대안으로 상위/하위 계층의 다른 표준 뷰에서 확장을 시도하거나, 커스텀 Composite View를 직접 만들어 원본 뷰와 커스텀 필드를 합쳐 노출하는 방식으로 우회합니다.

Extension 활성화되었는데 Fiori 앱에서 필드가 보이지 않음
90%는 Service Binding을 재활성화하지 않아 OData 메타데이터가 옛 스키마인 경우입니다. Service Binding을 열어 Publish를 다시 눌러야 합니다. 그래도 안 보이면 브라우저 캐시($metadata)를 지우고 Fiori Launchpad 세션에서 확인하세요.

Association 걸었더니 성능이 급격히 나빠짐
[0..1]이라 하더라도 SELECT 리스트에서 실제로 참조되면 조인이 발생합니다. 커스텀 테이블의 클라이언트 필드와 키 순서가 표준 뷰와 정확히 일치하는지, 그리고 필요한 컬럼에 보조 인덱스가 잡혀 있는지 확인하세요. 값이 자주 조회되지 않는다면 Virtual Element로 바꾸고 BAdI에서 지연 계산하는 편이 유리합니다.

BAdI가 호출되지 않음
Enhancement Implementation의 Filter 조건(예: 특정 판매 조직)이 걸려 있거나, 잘못된 BAdI 인스턴스를 구현했을 가능성이 큽니다. GET BADI 시점에서 필터 값을 로그로 남기고, 표준 뷰가 어떤 BAdI를 실제로 호출하는지 Where-Used(SE84)로 재검증하세요.

개발 시스템에서는 잘 되는데 QAS에서 활성화 실패
이송 순서 문제가 대부분입니다. DDIC 오브젝트가 포함된 Task가 CDS Task보다 먼저 이송되어야 합니다. Import Queue에서 다중 이송 옵션을 활용해 의존 오브젝트를 함께 이송하는 방식으로 해결합니다.

댓글 0

아직 댓글이 없습니다.