ABAP

VBAP 직접 조회 그만 — CDS 전환 3단계 #shorts #SAP #ABAP

이 글이 답하는 질문

  • VBAP를 직접 SELECT하던 코드는 왜 S/4HANA에서 문제가 되는가?
  • I_SalesOrderItem CDS 뷰가 제공하는 필드와 관계 구조는 어떻게 되어 있나?
  • 기존 VBAP 쿼리를 I_SalesOrderItem으로 옮길 때 필드 매핑은 어떻게 하나?
  • 필터/집계/JOIN 각 시나리오에서 성능을 어떻게 확보할까?
  • Association 오남용, 클라이언트 종속, Currency Conversion 등 실무 함정은 어떻게 피하나?

사전 가정

이 글은 ABAP OpenSQL과 CDS View 기본 문법(define view entity, association, @Semantics)을 이미 접해본 개발자를 대상으로 한다. VBAP/VBAK 테이블 구조와 판매 오더 프로세스(오더 헤더-아이템-스케줄 라인) 개념도 알고 있다고 가정한다. S/4HANA Virtual Data Model(VDM) 계층(Basic/Composite/Consumption)의 명명 규칙(I_, C_, R_ 프리픽스)에 대한 최소 이해가 있으면 이해가 빠르다.

테스트 환경

  • SAP S/4HANA 2023 FPS02 (on-premise) — I_SalesOrderItem은 2020 이후 지속 확장 중이며 릴리스별 필드 차이가 존재한다.
  • ABAP Platform 2023, ABAP Development Tools (Eclipse ADT) 3.36 이상
  • SAP HANA 2.0 SPS07, Column Store 기반
  • 확인 도구: ADT의 View Browser, Data Preview, SQL Console, 트랜잭션 SE16N은 CDS 엔티티에 대해서도 조회 가능
  • 권한: S_RS_AUTH, S_TABU_DIS(테이블 접근), CDS Access Control이 걸린 경우 DCL 확인 필요

왜 VBAP 직접 SELECT를 걷어내야 하는가

VBAP는 R/3 시절부터 판매 오더 아이템을 저장해 온 물리 테이블이다. S/4HANA로 넘어오면서 물리 구조 자체는 남아 있지만, 애플리케이션 로직이 참조해야 할 "정합적 뷰"는 더 이상 VBAP 단독이 아니다. 이유는 크게 네 가지다.

  • 필드 종속 문제: 금액/수량은 통화·단위 컨버전이 없으면 UI 표시나 리포트에서 오차를 유발한다. VBAP의 NETWR는 문서 통화 기준이며, 회사 코드 통화로 환산하려면 TCURR 룩업 로직을 직접 짜야 한다.
  • 클라이언트 종속: 순수 OpenSQL은 client-safe하지만, 예전 코드는 CLIENT SPECIFIED를 남발하거나 조인 조건에 MANDT를 명시해 이식성을 떨어뜨렸다. CDS는 기본 client-handling이 명시적이라 혼란이 적다.
  • 어소시에이션 부재: VBAK-VBAP-VBEP-VBKD-KONV로 이어지는 조인을 매번 손으로 써야 했고, 조인 조건이 통일되지 않아 부서마다 다른 결과가 나오기 십상이었다.
  • 비즈니스 로직 이중화: 취소 아이템(ABGRU 필터), Rejection 상태, 미결/완결 판정 로직이 프로그램마다 흩어져 있었다. CDS는 이런 파생 필드를 View 레벨에서 표준화한다.

I_SalesOrderItem은 이 부담을 흡수한 Basic Interface View다. Fiori 앱, Analytical Query, RAP Business Object가 공통 참조하므로 커스텀 리포트도 이 View를 기준으로 삼으면 결과의 일관성이 보장되는 경향이 있다.

I_SalesOrderItem 구조 스케치

비유하자면 VBAP가 "창고 선반에 쌓인 원자재 상자"라면, I_SalesOrderItem은 "라벨·중량·유통기한이 표기된 완성 팔레트"다. 팔레트 자체에도 필드가 붙어 있고, 사이드에는 헤더/스케줄/파트너/픽업 위치 등 관련 팔레트로 이동할 수 있는 통로(association)가 부착되어 있다.

  • 키 필드: SalesOrder, SalesOrderItem
  • 양/금액: OrderQuantity, OrderQuantityUnit, NetAmount, TransactionCurrency
  • 제품 정보: Material, MaterialGroup, ProductionPlant
  • 상태/파생: SalesDocumentRjcnReason, ItemIsBillingRelevant
  • 주요 Association: _SalesOrder(I_SalesOrder), _Material(I_Product), _ScheduleLine(I_SalesOrderScheduleLine), _Partner(I_SalesOrderItemPartner), _PricingElement(I_SalesOrderItemPrcgElmnt)

실전 예제 3단계

1. 기본 — VBAP SELECT를 그대로 옮기기

가장 흔한 시작점은 "특정 판매 오더의 라인 아이템 리스트"다. VBAP 코드는 다음과 같은 모양이었다.

" Legacy - VBAP 직접 조회
SELECT vbeln, posnr, matnr, kwmeng, vrkme, netwr, waerk
  FROM vbap
  INTO TABLE @DATA(lt_legacy_items)
  WHERE vbeln = @iv_order_id
    AND abgru = @space.        " 취소 아이템 제외

동일한 결과를 I_SalesOrderItem으로 옮기면 필드명이 비즈니스 의미 그대로 드러난다.

SELECT SalesOrder,
       SalesOrderItem,
       Material,
       OrderQuantity,
       OrderQuantityUnit,
       NetAmount,
       TransactionCurrency
  FROM I_SalesOrderItem
  WHERE SalesOrder            = @iv_order_id
    AND SalesDocumentRjcnReason = @space
  INTO TABLE @DATA(lt_order_items).

필드명은 길어졌지만 주석이 필요 없다는 점, MANDT 걱정이 사라졌다는 점, ABGRU=''라는 관용구가 SalesDocumentRjcnReason이라는 자체 설명적 이름으로 바뀐 점이 이득이다.

2. 실무 — 헤더 조인 + 통화 환산 + 로깅

영업 리포트에서 자주 요구되는 "특정 판매 조직의 미결 오더 아이템 합계"를 만들 때, VBAP 접근은 VBAK 조인과 TCURR 룩업까지 얹혀야 했다. I_SalesOrderItem은 association을 통해 헤더 필드를 그대로 끌어온다.

DATA:
  lt_open_items TYPE STANDARD TABLE OF ty_open_item,
  lo_log        TYPE REF TO if_recording_log_writer.

TRY.
    SELECT itm~SalesOrder,
           itm~SalesOrderItem,
           itm~Material,
           itm~NetAmount,
           itm~TransactionCurrency,
           _SalesOrder~SalesOrganization,
           _SalesOrder~SoldToParty,
           _SalesOrder~CreationDate
      FROM I_SalesOrderItem AS itm
      WHERE itm~SalesDocumentRjcnReason = @space
        AND _SalesOrder~SalesOrganization = @iv_sales_org
        AND _SalesOrder~CreationDate      BETWEEN @iv_date_from AND @iv_date_to
      INTO TABLE @lt_open_items.

    IF sy-subrc <> 0.
      lo_log->write( iv_level = 'W'
                     iv_msg   = |No open items for { iv_sales_org }| ).
    ENDIF.

  CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
    lo_log->write( iv_level = 'E'
                   iv_msg   = lx_sql->get_text( ) ).
    RAISE SHORTDUMP TYPE cx_abap_message_digest EXPORTING previous = lx_sql.
ENDTRY.

포인트는 세 가지다. 첫째, _SalesOrder~SalesOrganization처럼 association path를 SELECT 절과 WHERE 절 양쪽에 쓸 수 있다. 컴파일러가 필요한 조인을 알아서 걸어준다. 둘째, 조인 조건에 MANDT를 손대지 않는다. 셋째, DB 예외는 cx_sy_open_sql_db로 잡아 애플리케이션 로그에 남긴다 — Fiori Notification에서 원인 추적이 쉬워진다.

3. 프로덕션 — 집계 + 성능 + 단위 테스트

월별·자재별 판매 수량 집계를 만들 때 VBAP 시절에는 ABAP 레벨에서 LOOP+COLLECT로 처리하곤 했다. HANA 위에서는 SQL 레벨 집계가 훨씬 빠르다.

SELECT FROM I_SalesOrderItem AS itm
  FIELDS itm~Material,
         itm~ProductionPlant,
         SUM( itm~OrderQuantity )                 AS total_qty,
         MAX( _SalesOrder~CreationDate )          AS last_order_date,
         COUNT( DISTINCT itm~SalesOrder )         AS order_count
  WHERE itm~SalesDocumentRjcnReason = @space
    AND _SalesOrder~SalesOrganization IN @rt_sales_org
    AND _SalesOrder~CreationDate      >= @iv_since
  GROUP BY itm~Material, itm~ProductionPlant
  HAVING SUM( itm~OrderQuantity ) > @iv_min_qty
  ORDER BY total_qty DESCENDING
  INTO TABLE @DATA(lt_material_summary)
  UP TO @iv_top_n ROWS.

이 쿼리는 세 가지 프로덕션 규칙을 지킨다.

  • Selective WHERE: 인덱스 유리한 SalesOrganizationCreationDate가 앞선다. I_SalesOrderItem은 VBAP + VBAK 조인 뷰이므로 헤더 필드 선택도가 결과 성능을 좌우한다.
  • Row 제한: UP TO n ROWS로 예상 밖 폭주 방지. 개발자 테스트에서 SE16H 또는 SQL Console로 실행 계획을 미리 본다.
  • 단위 테스트: CL_OSQL_TEST_ENVIRONMENT로 CDS 엔티티를 목킹해 로직 회귀를 방어한다.
CLASS ltc_sales_summary DEFINITION FOR TESTING
      DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    DATA mo_env TYPE REF TO if_osql_test_environment.
    METHODS: setup, teardown, test_top_material FOR TESTING.
ENDCLASS.

CLASS ltc_sales_summary IMPLEMENTATION.
  METHOD setup.
    mo_env = cl_osql_test_environment=>create(
               VALUE #( ( 'I_SALESORDERITEM' ) ( 'I_SALESORDER' ) ) ).
  ENDMETHOD.

  METHOD teardown.
    mo_env->destroy( ).
  ENDMETHOD.

  METHOD test_top_material.
    " insert_test_data( ... ) 로 픽스처 세팅 후
    " zcl_sales_summary=>compute( ... ) 검증
  ENDMETHOD.
ENDCLASS.

자주 만나는 함정

  • Association을 SELECT만 하고 WHERE에는 안 쓰는데도 조인이 걸린다 — CDS는 참조된 association 필드가 있으면 조인을 유지한다. 필요 없다면 아예 path를 걷어내야 실행 계획이 가벼워진다.
  • NetAmount를 회사 통화로 바로 쓴다NetAmountTransactionCurrency 기준이다. 회사 코드 통화가 필요하면 상위 Composite View(예: C_SalesOrderItemDEX류)나 @Semantics.amount.currencyCode를 활용한 파생 뷰를 만들거나, CDS CURRENCY_CONVERSION 함수를 별도 뷰에서 정의한다.
  • SE16N에서 필드가 안 보인다 — I_SalesOrderItem은 Basic View이므로 릴리스에 따라 필드 커버리지가 다르다. 원하는 필드가 없다면 상위 Composite/Consumption 뷰나 커스텀 확장 뷰(ZI_SalesOrderItem_Ext)를 검토한다.
  • 취소 아이템 필터를 빠뜨림SalesDocumentRjcnReason = '' 필터를 놓치면 취소된 라인까지 합산돼 매출이 부풀려진다. 표준 필터 로직을 재사용하는 커스텀 뷰로 감싸는 것이 안전하다.
  • DCL Access Control 미고려 — I_SalesOrderItem에는 DCL이 걸려 있을 수 있다. 백그라운드 잡에서는 PRIVILEGED ACCESS 옵션을 검토하거나 별도 서비스 유저 권한을 설계한다.

FAQ

  • Q1. VBAP 직접 조회가 완전히 금지되나? 아니다. 확장 필드 저장 등 물리 테이블 쓰기 목적이거나 CDS로 노출되지 않은 필드 접근이 필요할 때는 여전히 VBAP를 다룬다. 다만 리포트/조회 로직은 CDS 우선이 권장된다.
  • Q2. 성능이 오히려 느려지는 사례가 있다? Composite View 여러 겹을 조인하면 옵티마이저가 힘들어질 수 있다. Basic Interface View인 I_SalesOrderItem을 기준으로 필요한 필드만 SELECT하고, 상위 뷰가 아닌 커스텀 뷰로 재조립하는 방식이 일반적으로 유리하다.
  • Q3. 커스텀 필드 확장은 어떻게 하나? Key User Extensibility로 VBAP에 필드를 추가하면 I_SalesOrderItem에 자동 반영되지 않는 경우가 있다. Extend View(EXTEND VIEW ENTITY)를 명시적으로 만들거나 커스텀 확장 CDS를 별도 유지하는 방식이 안전하다.

더 파볼 주제

  • I_SalesOrder / I_SalesOrderScheduleLine과의 3-way 조인 패턴
  • Analytical Query 뷰(C_SalesOrderItemQuery) 기반 KPI 리포트
  • RAP Business Object에서 I_SalesOrderItem 재사용 시 Behavior Definition 설계
  • CDS Table Function으로 통화 환산 로직 캡슐화
  • Access Control(DCL) 설계와 백그라운드 잡의 Privileged Access 전략

핵심 한 줄

VBAP는 저장소, I_SalesOrderItem은 판매 오더 아이템의 표준 조회 계약 — 리포트·API·확장은 이 계약을 기준으로 설계하는 것이 유지보수 관점에서 유리하다.

댓글 0

아직 댓글이 없습니다.