UI5

SideEffects로 필드 자동 갱신하는 3가지 방법 #shorts #SAP #Fiori

▶ YouTube에서 보기

개요: 저장 버튼 없이 화면이 스스로 갱신되는 원리

Fiori Elements 앱에서 수량 필드를 바꿨는데 총액이 그대로라면, 사용자는 "이 앱이 계산을 안 하나?"라고 느낍니다. 백엔드 Determination은 이미 값을 다시 계산했지만, 브라우저가 그 사실을 모르는 것이 문제입니다. SideEffects Annotation은 "이 필드가 바뀌면 저 필드를 다시 읽어라"라는 계약을 메타데이터에 선언해, 커스텀 JavaScript 없이 UI를 자동 갱신하는 메커니즘입니다. 이 글을 끝까지 읽으면 다음을 할 수 있습니다.

  • SourceProperties / TargetProperties / TargetEntities 3가지 속성의 역할 구분
  • 구매오더(Purchase Order) 시나리오로 수량 → 총액·세금·배송일 자동 갱신 구현
  • RAP BDEF의 side effects 구문과 로컬 Annotation XML 방식 비교
  • 갱신이 안 될 때 네트워크 탭과 EML로 원인을 좁히는 디버깅 절차

미리 알아두면 좋은 것

CDS View Entity 기본 문법, RAP(ABAP RESTful Application Programming Model)의 Behavior Definition과 Determination 개념, OData V4 요청 구조($batch, PATCH)를 알고 있으면 수월합니다. Fiori Elements Object Page를 한 번이라도 생성해 본 경험을 전제로 합니다.

환경 및 준비물

이 글의 코드는 다음 환경 기준입니다.

  • SAP BTP ABAP Environment 또는 S/4HANA 2022 이상 (온프레미스) — BDEF side effects 구문은 ABAP Platform 2022부터 사용 가능합니다
  • OData V4 기반 Fiori Elements (Object Page / List Report), SAPUI5 1.108 이상 권장
  • ADT(ABAP Development Tools) 최신 버전, 필요 시 SAP Business Application Studio
  • OData V2 구형 앱이라면 로컬 annotation.xml 방식(2단계 참고)을 사용합니다

클라우드와 온프레미스에서 구문 지원 범위가 다를 수 있으므로, 사용 중인 릴리스의 Feature Availability 문서를 먼저 확인하는 것이 일반적으로 안전합니다.

핵심 개념: 3가지 속성이 만드는 "갱신 계약"

SideEffects를 식당의 주문 벨에 비유할 수 있습니다. 주방(백엔드)이 요리를 다시 만들어도 벨을 누르지 않으면 손님(UI)은 모릅니다. SideEffects Annotation은 "수량이 바뀌면 벨을 눌러서 총액 접시를 다시 가져가라"라고 미리 약속해 두는 것입니다. 프런트엔드는 이 약속을 메타데이터에서 읽고, 해당 필드 변경 직후 PATCH 응답에 이어 필요한 프로퍼티만 $select로 다시 조회합니다.

계약의 구성 요소는 세 가지입니다.

속성역할예시
SourceProperties변경을 감지할 트리거 필드 목록수량, 할인율
TargetProperties다시 읽어올 개별 필드 목록순액, 세액
TargetEntities통째로 다시 읽어올 연관 엔티티(_Assoc)아이템 → 헤더 합계

동작 순서는 이렇습니다. (1) 사용자가 수량 필드에서 포커스를 벗어남 → (2) UI가 PATCH 전송, 백엔드 Determination이 총액 재계산 → (3) UI가 Annotation을 보고 TargetProperties/TargetEntities에 대한 GET을 같은 $batch에 묶어 전송 → (4) 응답으로 화면 바인딩 갱신. 즉 SideEffects는 계산 로직이 아니라 재조회 지시서입니다. 계산 자체는 여전히 Determination의 몫이며, 이 역할 분리를 이해하는 것이 핵심입니다.

TargetProperties는 필드 단위라 페이로드가 작고, TargetEntities는 연관 전체를 다시 읽어 확실하지만 무겁습니다. "좁게 시작해서 필요할 때만 넓히는" 전략이 권장됩니다.

실전 구현 3단계: 구매오더 수량 → 총액·세금·배송일 갱신

1단계 — RAP BDEF에 side effects 선언 (기본형)

구매오더 아이템에서 수량이 바뀌면 순액과 세액을 다시 읽도록 Behavior Definition에 선언합니다.

managed implementation in class zbp_r_purchaseorder unique;
strict ( 2 );

define behavior for ZR_PurchaseOrder alias PurchOrd
{
  update; delete;
  association _Item { create; }
}

define behavior for ZR_PurchaseOrderItem alias PurchItem
{
  update;
  field ( readonly ) NetAmount, TaxAmount, ExpectedDeliveryDate;

  determination calcAmounts on modify { field OrderedQty; }

  side effects
  {
    field OrderedQty affects field NetAmount,
                             field TaxAmount,
                             field ExpectedDeliveryDate,
                             entity _PurchOrd;
  }
}

마지막 줄 entity _PurchOrd가 TargetEntities에 해당합니다. 아이템 수량이 헤더 합계에 영향을 주므로 헤더 엔티티 전체를 재조회 대상으로 지정한 것입니다. 이 선언만으로 서비스 메타데이터에 Common.SideEffects가 자동 생성되고, Fiori Elements V4 앱은 별도 코드 없이 이를 따릅니다.

2단계 — Determination 구현과 메시지 처리 (실무형)

재조회할 값이 실제로 갱신되도록 Determination을 구현합니다. 재계산 실패 시 사용자에게 메시지를 돌려주는 처리까지 포함합니다.

CLASS lhc_purchitem IMPLEMENTATION.
  METHOD calcAmounts.
    READ ENTITIES OF zr_purchaseorder IN LOCAL MODE
      ENTITY PurchItem
        FIELDS ( OrderedQty UnitPrice TaxRate )
        WITH CORRESPONDING #( keys )
      RESULT DATA(lt_items).

    LOOP AT lt_items INTO DATA(ls_item).
      IF ls_item-OrderedQty <= 0.
        APPEND VALUE #( %tky = ls_item-%tky ) TO failed-purchitem.
        APPEND VALUE #( %tky = ls_item-%tky
                        %msg = new_message_with_text(
                          severity = if_abap_behv_message=>severity-error
                          text     = '수량은 0보다 커야 합니다' ) )
               TO reported-purchitem.
        CONTINUE.
      ENDIF.

      DATA(lv_net) = ls_item-OrderedQty * ls_item-UnitPrice.
      MODIFY ENTITIES OF zr_purchaseorder IN LOCAL MODE
        ENTITY PurchItem
          UPDATE FIELDS ( NetAmount TaxAmount ExpectedDeliveryDate )
          WITH VALUE #( ( %tky      = ls_item-%tky
                          NetAmount = lv_net
                          TaxAmount = lv_net * ls_item-TaxRate / 100
                          ExpectedDeliveryDate =
                            cl_abap_context_info=>get_system_date( ) + 7 ) ).
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.

OData V2 구형 앱이라면 BDEF 구문 대신 프런트엔드 프로젝트의 로컬 annotation 파일에 같은 계약을 선언합니다.

<Annotations Target="ZPO_SRV.PurchaseOrderItemType">
  <Annotation Term="com.sap.vocabularies.Common.v1.SideEffects"
              Qualifier="QtyChange">
    <Record>
      <PropertyValue Property="SourceProperties">
        <Collection><PropertyPath>OrderedQty</PropertyPath></Collection>
      </PropertyValue>
      <PropertyValue Property="TargetProperties">
        <Collection><String>NetAmount</String><String>TaxAmount</String></Collection>
      </PropertyValue>
      <PropertyValue Property="TargetEntities">
        <Collection><NavigationPropertyPath>to_Header</NavigationPropertyPath></Collection>
      </PropertyValue>
    </Record>
  </Annotation>
</Annotations>

3단계 — 프로덕션 관점: 성능·테스트 (심화형)

운영 배포 전에 두 가지를 점검합니다. 첫째, 재조회 범위 최소화입니다. entity _PurchOrd처럼 넓은 타깃은 라운드트립당 페이로드를 키우므로, 헤더에서 실제 바뀌는 필드가 합계뿐이라면 헤더 뷰에 별도 side effects를 나눠 선언하거나 TargetProperties로 좁힙니다. 둘째, Determination 단위 테스트입니다. EML로 UI 없이 검증할 수 있습니다.

METHOD qty_change_recalculates_amount.
  MODIFY ENTITIES OF zr_purchaseorder
    ENTITY PurchItem
      UPDATE FIELDS ( OrderedQty )
      WITH VALUE #( ( %tky = ms_item_key OrderedQty = 10 ) )
    FAILED DATA(failed) REPORTED DATA(reported).

  READ ENTITIES OF zr_purchaseorder
    ENTITY PurchItem FIELDS ( NetAmount ) WITH VALUE #( ( %tky = ms_item_key ) )
    RESULT DATA(lt_result).

  cl_abap_unit_assert=>assert_equals(
    act = lt_result[ 1 ]-NetAmount  exp = '1500.00' ).
ENDMETHOD.

보안 측면에서는 field ( readonly )로 계산 필드의 클라이언트 조작을 차단했는지 확인합니다. SideEffects는 값을 다시 읽을 뿐이므로, readonly가 빠지면 사용자가 총액을 직접 고칠 수 있는 허점이 생깁니다.

흔한 실수와 트러블슈팅 FAQ

Q1. Annotation을 선언했는데 화면이 전혀 갱신되지 않습니다.

브라우저 네트워크 탭에서 PATCH 이후 GET(또는 $batch 내 재조회)이 나가는지 먼저 확인합니다. 재조회 요청 자체가 없다면 메타데이터에 SideEffects가 반영되지 않은 것입니다. V4는 서비스 메타데이터($metadata)에서 Common.SideEffects 텀을 검색하고, 캐시 문제일 수 있으니 메타데이터 캐시를 비운 뒤 재확인합니다. 재조회는 나가는데 값이 그대로라면 Determination 미실행이 원인이므로 EML 테스트나 ADT 디버깅으로 백엔드를 점검합니다.

Q2. Draft 앱에서 수량을 바꿔도 한 박자 늦게 갱신됩니다.

Draft 활성화 앱은 필드 변경 시 draft PATCH가 발생하고, SideEffects 재조회도 draft 인스턴스를 대상으로 합니다. Determination의 트리거 필드(on modify { field OrderedQty; })가 side effects의 SourceProperties와 일치하는지, Determination이 draft 단계에서 실행되도록 설계됐는지 확인합니다. 트리거 필드가 어긋나면 저장 시점에야 계산되는 "한 박자 늦은" 현상이 나타납니다.

Q3. TargetEntities로 헤더를 지정했더니 앱이 눈에 띄게 느려졌습니다.

연관 엔티티 재조회는 확장(expand) 포함 전체 읽기가 될 수 있습니다. 헤더에서 실제 변하는 필드가 총액 하나라면 affects entity 대신 헤더 필드를 직접 겨냥하도록 구조를 조정하고, 트리거 필드가 여러 개라면 하나의 side effects 블록에 묶어 재조회 횟수를 줄이는 편이 일반적으로 유리합니다.

더 나아가기

SideEffects의 트리거는 필드만이 아닙니다. determine action과 결합하면 버튼 클릭으로 재계산·재조회를 명시적으로 묶을 수 있고, 액션 실행 후 특정 목록을 갱신하는 패턴은 승인 워크플로 앱에서 자주 쓰입니다. 다음 주제로는 RAP Validation과 Determination의 실행 시점(save vs modify) 비교, Value Help 필드 변경 시 SideEffects 연계, OData V4 $batch 최적화를 이어서 살펴보길 권장합니다.

함께 보면 좋은 자료

댓글 0

아직 댓글이 없습니다.