RAP

버튼 하나로 100건 일괄처리 — Mass Action 3가지 #shorts #SAP #RAP

▶ YouTube에서 보기

RAP Mass Action이란 무엇인가

RAP(RESTful Application Programming Model)에서 Mass Action은 List Report의 여러 행을 선택한 뒤 단일 액션 호출로 한꺼번에 처리하는 메커니즘입니다. 예를 들어 주문 목록에서 100건을 체크박스로 선택하고 [일괄 승인] 버튼을 누르면, 각 행을 개별 호출하지 않고 서버에 한 번의 배치 요청으로 전달됩니다. 이는 UX 관점에서 반복 클릭을 없애고, 서버 관점에서는 DB 라운드트립을 최소화해 처리 시간을 크게 줄이는 효과를 가집니다.

RAP의 액션(Action)은 기본적으로 단일 엔티티에 대해 동작하지만, mass enabled 속성을 켜면 프레임워크가 선택된 인스턴스 키 집합을 한 번에 핸들러로 전달합니다. 개발자는 FOR <instance> IN keys 구문으로 반복 처리하되, 트랜잭션 컨텍스트·락·메시지 반환은 배치 단위로 관리해야 합니다.

이 글에서 다룰 핵심 체크리스트는 다음과 같습니다.

  • Mass Action의 3가지 유형(Instance-bound / Factory / Static)의 차이 이해
  • Behavior Definition(BDEF)에서 action ... action 선언 시 옵션 조합
  • Behavior Implementation(BIL)에서 FOR 루프와 REPORTED / FAILED / MAPPED 반환 패턴
  • SAP Fiori Elements List Report Annotation에서 버튼 노출 규칙
  • Lock, Draft, Authorization과의 상호작용

미리 알아두면 좋은 배경

이 글은 RAP의 기본 구조 — CDS View Entity, Behavior Definition, Behavior Implementation Class, Service Definition/Binding — 를 이미 접해본 개발자를 대상으로 합니다. ABAP CDS의 @Metadata.allowExtensions, define behavior for 문법과 FOR MODIFY 처리 방식(ENTITY ... UPDATE FIELDS)에 익숙하다면 편안하게 따라올 수 있습니다. Fiori Elements List Report의 Line Item Annotation과 UI.LineItemDataFieldForAction 개념도 알아두면 좋습니다.

실습 환경과 사전 준비물

이 글의 예제는 다음 환경을 전제로 작성되었습니다.

  • ABAP Platform: SAP S/4HANA 2023 On-Premise 또는 ABAP Cloud 2024 이상 (ABAP Platform 2308+)
  • SAP BTP ABAP Environment: ABAP Environment Release 2402 이상 권장
  • 개발 도구: ADT(ABAP Development Tools) for Eclipse 3.42 이상
  • Fiori Elements: SAPUI5 1.120 LTS 이상, List Report Object Page 템플릿
  • 권한 오브젝트: 액션 실행에 필요한 S_RFC, 커스텀 Z_ORDER_MASS

사전에 준비해야 할 오브젝트는 다음과 같습니다. 데모용 테이블 zsales_order(필드: sales_order_id, customer_id, order_status, total_amount, changed_at)와 이 위에 정의된 CDS Root View Entity ZR_SalesOrder, 그리고 대응하는 Behavior Definition ZR_SalesOrder가 있어야 합니다. Draft-Enabled 시나리오는 별도 언급이 있는 경우에만 다룹니다.

Mass Action의 동작 원리와 3가지 유형

Mass Action을 제대로 이해하려면 우선 RAP 액션이 서버에서 어떻게 호출되는지를 그려봐야 합니다. Fiori Elements List Report에서 사용자가 여러 행을 선택하고 액션 버튼을 누르면, OData V4 프로토콜은 각 인스턴스별로 POST /Entity(key)/Action을 호출하거나, batch 요청으로 묶어 전송합니다. 여기서 mass enabled가 켜져 있으면 RAP 런타임은 이 호출들을 하나로 합쳐 FOR <fs> IN keys 형태의 단일 핸들러 호출로 변환합니다. 즉, 100건 선택 시 100번의 SELECT/UPDATE가 아니라 1번의 벌크 처리로 전환할 기회가 생깁니다.

Mass Action은 크게 3가지 유형으로 나뉩니다.

  1. Instance-bound Mass Action: 각 인스턴스에 바인딩되며 선택된 여러 행에 대해 동일 액션을 수행. List Report 툴바에 자동 노출되는 가장 일반적인 형태.
  2. Static Action (mass 없음): 특정 엔티티 타입에 속하지만 인스턴스 키에 얽매이지 않는 액션. 파라미터를 통해 필터 조건을 받아 서버 측에서 대량 처리.
  3. Factory Action (mass enabled): 여러 개의 새로운 인스턴스를 한 번에 생성하는 액션. result 절이 [0..*]로 정의되어 다수의 반환 인스턴스를 취급.

비유하자면 Instance-bound Mass Action은 "체크박스로 선택한 서류에 도장 100번 찍기"이고, Static은 "부서 전체 서류를 필터로 걸러 서명받기", Factory는 "빈 서류철 100개를 한 번에 발급받기"에 가깝습니다. 각각 트랜잭션 경계, 락 범위, 메시지 처리 방식이 달라 설계 초기에 유형을 정확히 선택해야 합니다.

BDEF에서의 선언 예는 다음과 같습니다.

define behavior for ZR_SalesOrder alias SalesOrder
persistent table zsales_order
lock master
authorization master ( instance )
etag master changed_at
{
  create;
  update;
  delete;

  " (1) Instance-bound Mass Action
  action ( features : instance ) approveOrder result [1] $self;

  " (2) Static Action - 여러 행을 파라미터로 받음
  static action bulkArchive parameter ZI_BulkArchiveParam;

  " (3) Factory Mass Action - 다수 인스턴스 생성
  factory action createFromTemplate [0..*] parameter ZI_TemplateParam;
}

여기서 approveOrder는 기본적으로 mass가 활성화되어 있는 인스턴스 액션입니다. List Report의 선택 툴바에서 자동으로 활성화되며, 별도의 mass 키워드 없이도 다중 호출을 하나의 핸들러로 받게 됩니다.

List Report에서 mass enabled 설정과 UI 노출

서버 측 BDEF만으로는 화면에 버튼이 나타나지 않습니다. Metadata Extension(Annotation)을 통해 List Report의 UI.LineItemDataFieldForAction을 등록해야 합니다.

@Metadata.layer: #CORE
@UI: { headerInfo: { typeName: 'Sales Order',
                     typeNamePlural: 'Sales Orders' } }
annotate view ZC_SalesOrder with
{
  @UI.lineItem: [
    { position: 10, label: 'Order ID' },
    { position: 20, label: 'Customer',
      type: #FOR_ACTION,
      dataAction: 'approveOrder',
      label: 'Approve Selected' }
  ]
  SalesOrderId;
}

이 Annotation이 적용되면 List Report 툴바에 [Approve Selected] 버튼이 나타나고, 사용자가 하나 이상의 행을 체크한 순간 활성화됩니다. features : instance가 BDEF에 걸려 있으면 상태에 따라 버튼 활성/비활성을 동적으로 제어할 수 있습니다. 예컨대 이미 승인된 주문에는 버튼을 회색 처리하도록 GET FEATURES 핸들러를 구현합니다.

실전 예제 1: 주문 일괄 승인 구현

가장 자주 등장하는 시나리오인 주문 일괄 승인을 먼저 살펴봅니다. BIL 클래스에서 FOR MODIFY 메서드를 아래처럼 구현합니다.

CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS approveOrder FOR MODIFY
      IMPORTING keys FOR ACTION SalesOrder~approveOrder RESULT result.
ENDCLASS.

CLASS lhc_salesorder IMPLEMENTATION.
  METHOD approveOrder.
    DATA: updates TYPE TABLE FOR UPDATE ZR_SalesOrder,
          lt_reported LIKE reported-salesorder,
          lt_failed   LIKE failed-salesorder.

    " 1) 현재 상태 조회 (일괄)
    READ ENTITIES OF ZR_SalesOrder IN LOCAL MODE
      ENTITY SalesOrder
        FIELDS ( SalesOrderId OrderStatus TotalAmount )
        WITH CORRESPONDING #( keys )
      RESULT DATA(lt_orders).

    " 2) 비즈니스 규칙 검증 + 승인 대상만 필터
    LOOP AT lt_orders REFERENCE INTO DATA(lr_order).
      IF lr_order->OrderStatus <> 'N'.
        APPEND VALUE #( %key = lr_order->%key
                        %msg = new_message( id = 'ZORDER'
                                            number = '010'
                                            severity = if_abap_behv_message=>severity-error )
                        %element-orderstatus = if_abap_behv=>mk-on
                      ) TO lt_failed.
        CONTINUE.
      ENDIF.

      APPEND VALUE #( %tky = lr_order->%tky
                      OrderStatus = 'A'
                      %control-orderstatus = if_abap_behv=>mk-on
                    ) TO updates.
    ENDLOOP.

    " 3) 단일 MODIFY 호출로 벌크 업데이트
    MODIFY ENTITIES OF ZR_SalesOrder IN LOCAL MODE
      ENTITY SalesOrder
        UPDATE FIELDS ( OrderStatus )
        WITH updates
      REPORTED lt_reported
      FAILED   lt_failed.

    " 4) result에 승인된 인스턴스 채우기
    result = VALUE #( FOR ls_upd IN updates
                      ( %tky = ls_upd-%tky
                        %param-SalesOrderId = ls_upd-SalesOrderId ) ).

    reported = CORRESPONDING #( DEEP lt_reported ).
    failed   = CORRESPONDING #( DEEP lt_failed ).
  ENDMETHOD.
ENDCLASS.

포인트는 세 가지입니다. 첫째, READ ENTITIES ... WITH CORRESPONDING #( keys )로 선택된 모든 키를 한 번에 조회합니다. 둘째, 개별 실패는 failed 테이블에 담아 UI에 개별 메시지로 반환합니다. 셋째, MODIFY ENTITIES도 단일 호출로 처리해 DB 왕복을 최소화합니다.

실전 예제 2: 재고 일괄 업데이트와 파라미터 액션

두 번째는 창고 관리 시나리오로, 선택된 자재의 안전재고 수치를 일괄 조정하는 경우입니다. 이때는 파라미터가 필요하므로 아래처럼 파라미터 구조를 정의합니다.

action ( features : instance ) adjustSafetyStock
   parameter ZI_SafetyStockParam
   result [1] $self;

파라미터 CDS는 @ObjectModel.query.implementedBy 없이 단순 abstract entity로 만듭니다.

@EndUserText.label: 'Safety Stock Adjustment Param'
define abstract entity ZI_SafetyStockParam {
  key AdjustmentType : abap.char(1);
  AdjustmentValue    : abap.dec(13,3);
  Reason             : abap.char(60);
}

핸들러에서는 파라미터가 keys-%param으로 각 인스턴스마다 동일하게 전달됩니다.

METHOD adjustSafetyStock.
  DATA updates TYPE TABLE FOR UPDATE ZR_Material.

  READ ENTITIES OF ZR_Material IN LOCAL MODE
    ENTITY Material
      FIELDS ( MaterialId SafetyStock )
      WITH CORRESPONDING #( keys )
    RESULT DATA(lt_mat).

  LOOP AT keys INTO DATA(ls_key).
    ASSIGN lt_mat[ %tky = ls_key-%tky ] TO FIELD-SYMBOL(<fs_mat>).
    CHECK sy-subrc = 0.

    DATA(lv_new) = COND #(
      WHEN ls_key-%param-AdjustmentType = 'A'  " 절대값
        THEN ls_key-%param-AdjustmentValue
      WHEN ls_key-%param-AdjustmentType = 'P'  " 퍼센트
        THEN <fs_mat>-SafetyStock * ( 1 + ls_key-%param-AdjustmentValue / 100 )
      ELSE <fs_mat>-SafetyStock ).

    APPEND VALUE #( %tky = ls_key-%tky
                    SafetyStock = lv_new
                    %control-safetystock = if_abap_behv=>mk-on ) TO updates.
  ENDLOOP.

  MODIFY ENTITIES OF ZR_Material IN LOCAL MODE
    ENTITY Material UPDATE FIELDS ( SafetyStock )
    WITH updates
    REPORTED reported FAILED failed.

  " 감사 로그 (Application Log)
  cl_application_log_writer=>log_bulk(
    scenario = 'STOCK_ADJUST'
    keys     = keys
    reason   = keys[ 1 ]-%param-Reason ).
ENDMETHOD.

실무 팁으로, 이런 액션은 반드시 Application Log 또는 커스텀 감사 테이블에 처리 이력을 남기는 것이 좋습니다. 감사 이슈가 발생했을 때 누가 언제 어떤 값을 얼마나 조정했는지 추적할 수 있어야 합니다.

실전 예제 3: 상태 일괄 변경과 Lock 처리

세 번째는 구매요청서 상태 일괄 변경 시나리오로, 락 충돌이 빈번한 케이스입니다. RAP은 기본적으로 lock master 정의 시 ENQUEUE_/DEQUEUE_ 함수 모듈을 자동 생성하지만, Mass 처리에서는 락 획득 실패를 우아하게 다뤄야 합니다.

METHOD changeStatus.
  DATA lt_locked_keys LIKE keys.

  " 1) 락 시도 - 실패한 키는 failed로
  LOOP AT keys INTO DATA(ls_key).
    TRY.
        CALL FUNCTION 'ENQUEUE_EZ_PURCH_REQ'
          EXPORTING
            mode_zpurch_req = 'E'
            purch_req_id    = ls_key-PurchReqId
            _scope          = '2'
          EXCEPTIONS
            foreign_lock    = 1
            system_failure  = 2
            OTHERS          = 3.

        IF sy-subrc <> 0.
          APPEND VALUE #( %tky = ls_key-%tky
                          %fail-cause = if_abap_behv=>cause-locked
                        ) TO failed-purchreq.
        ELSE.
          APPEND ls_key TO lt_locked_keys.
        ENDIF.
      CATCH cx_root INTO DATA(lx).
        APPEND VALUE #( %tky = ls_key-%tky
                        %fail-cause = if_abap_behv=>cause-unspecific
                      ) TO failed-purchreq.
    ENDTRY.
  ENDLOOP.

  " 2) 락 성공한 것만 상태 변경
  DATA(updates) = VALUE TABLE FOR UPDATE ZR_PurchReq(
      FOR ls IN lt_locked_keys
        ( %tky = ls-%tky
          Status = ls-%param-TargetStatus
          %control-status = if_abap_behv=>mk-on ) ).

  MODIFY ENTITIES OF ZR_PurchReq IN LOCAL MODE
    ENTITY PurchReq UPDATE FIELDS ( Status )
    WITH updates
    REPORTED reported FAILED failed.
ENDMETHOD.

이 패턴은 부분 성공(Partial Success)을 명시적으로 다룹니다. 100건 중 3건이 다른 사용자에 의해 편집 중이면, 나머지 97건은 정상 처리되고 실패 3건만 UI 메시지로 표시됩니다. 이는 RAP Mass Action의 기본 철학이기도 합니다 — all-or-nothing이 아닌 best-effort.

보안 관점에서 인스턴스 인가 검증도 함께 수행하는 것이 일반적으로 권장됩니다. GET INSTANCE AUTHORIZATIONS를 별도 구현해 조작 권한이 없는 인스턴스는 failed-<alias>에 담아 반환합니다.

흔한 실수와 트러블슈팅 FAQ

실무에서 자주 마주치는 문제들을 정리합니다.

Q1. 버튼이 List Report 툴바에 나타나지 않아요.
A. 3가지를 확인하세요. (1) BDEF에 action 선언이 되었는지, (2) Metadata Extension의 UI.lineItemtype: #FOR_ACTION, dataAction: '...'이 있는지, (3) Service Binding을 Publish/Activate 후 브라우저 캐시를 비웠는지. 특히 @UI.identification에 넣으면 Object Page에는 뜨지만 List Report 툴바에는 안 나타납니다.

Q2. 100건 처리인데 왜 100번 핸들러가 호출되나요?
A. 핸들러 시그니처가 FOR ACTION ... ~actionName으로 정의되면 keys 파라미터에 모든 키가 담겨 한 번만 호출됩니다. 만약 반복 호출된다면 클라이언트가 $batch가 아닌 개별 요청을 보내는 것이므로, Fiori Elements의 batch 그룹 설정과 manifest.jsonuseBatch: true를 확인하세요.

Q3. failed 테이블에 넣었는데 UI에 메시지가 안 보여요.
A. reported-<alias>%msg를 함께 채워야 UI에 표시됩니다. failed는 처리 대상에서 제외만 하고 별도 메시지 렌더링을 하지 않습니다. new_message()로 메시지를 만들어 reported%msg에 넣고, 심각도(severity)를 error로 지정하세요.

Q4. Draft-Enabled 엔티티에서 Mass Action이 동작하지 않아요.
A. Draft 시나리오에서는 %is_draft를 명시적으로 다뤄야 합니다. Active 인스턴스만 대상으로 하려면 WITH CORRESPONDING #( keys MAPPING %is_draft = if_abap_behv=>mk-off )로 필터링하고, Draft가 존재하는 인스턴스에 대해서는 별도 정책(자동 저장 또는 실패 처리)을 정의해야 합니다.

이어서 살펴보면 좋은 주제

Mass Action을 익혔다면 다음 주제로 확장해보세요. 첫째, Determinations & Validations를 Mass Action과 조합해 사전 검증 로직을 재사용하는 패턴. 둘째, 대량 처리 시 bgRFC나 Background Processing Framework를 활용한 비동기 Mass Action. 셋째, Custom Entity + Query Implementation으로 파라미터 기반 Static Action을 확장해 대시보드형 벌크 오퍼레이션 만들기. 넷째, Application LogBusiness Event를 연동해 감사 추적과 이벤트 브로커(예: SAP Event Mesh) 연계까지 확장하기.

Draft 모델과 Fiori Elements의 InlineCreationRows를 Factory Mass Action과 결합하면 UX 측면에서 훨씬 더 풍부한 벌크 생성 경험을 설계할 수 있습니다.

더 읽어볼 자료

댓글 0

아직 댓글이 없습니다.