ABAP

RAP Behavior Implementation — 비즈니스 로직 어디에 짜야 해? #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 및 이 글에서 얻어갈 것

ABAP RESTful Application Programming Model(RAP)에서 Behavior Implementation은 비즈니스 객체의 CRUD 및 액션 로직을 ABAP 클래스로 구현하는 핵심 계층입니다. 이 글에서는 BDEF(Behavior Definition)에서 시작해 Local Handler Class까지 이어지는 전체 흐름을 구매요청(PurchaseRequisition) 시나리오로 따라 만들어 봅니다.

  • BDEF의 implementation type managedunmanaged 차이 이해
  • Local Handler Class(lhc_*) 자동 생성 및 수동 메서드 오버라이드
  • Validation, Determination, Action 핸들러의 구조와 호출 시점
  • EML(Entity Manipulation Language)을 활용한 보조 호출 패턴
  • 실무에서 자주 만나는 권한·트랜잭션 버퍼 관련 함정 회피

알아두면 좋은 배경 지식

이 예제를 원활히 따라가려면 ABAP OO 클래스 작성 경험, CDS View와 어노테이션 기본 문법, 그리고 RAP의 Service Definition / Service Binding 구조에 대한 개략적 이해가 필요합니다. SAP Gateway나 OData V4의 노출 흐름을 한 번이라도 다뤄봤다면 핸들러 메서드 시그니처가 훨씬 자연스럽게 읽힙니다.

환경 및 준비물

본 글의 코드는 다음 환경을 기준으로 작성·검증을 권장합니다.

  • ABAP Platform: SAP S/4HANA 2022 이상 또는 ABAP Environment(BTP) 2308 릴리스 이상
  • 개발 도구: ADT(ABAP Development Tools) for Eclipse 2024-06 빌드 이상
  • RAP 버전: Managed/Unmanaged/Draft 모두 지원되는 최신 모델
  • 권한: 패키지 생성, 클래스 생성, Behavior Definition 활성화 권한
  • 기초 객체: 트랜잭션 테이블 zpr_header, 데이터 모델 CDS ZI_PurchaseRequisition, 프로젝션 뷰 ZC_PurchaseRequisition가 미리 정의되어 있다고 가정

일반적으로 BDEF는 데이터 모델 CDS(루트 뷰)에 1:1로 매핑되며, 프로젝션 BDEF는 노출용 별도 파일로 분리하는 것이 권장됩니다.

핵심 개념 정리

RAP Behavior는 비즈니스 객체에 "무엇을 할 수 있는가"를 선언적으로 정의하는 BDEF와, "어떻게 동작하는가"를 절차적으로 구현하는 Behavior Pool 클래스의 조합으로 이루어집니다. 식당으로 비유하자면 BDEF는 메뉴판이고, Behavior Pool은 주방장이며, Local Handler Class(lhc_*)는 주방장이 손에 든 레시피 노트입니다.

BDEF에는 세 가지 구현 타입이 있습니다.

  • managed: 프레임워크가 INSERT/UPDATE/DELETE를 자동 처리. 표준 필드 매핑·잠금·번호 채번을 자동 위임
  • unmanaged: 모든 영속화 로직을 개발자가 직접 코딩. 레거시 함수 모듈 재활용 시 유용
  • managed with additional save: 기본은 managed지만 save 단계에서 사용자 정의 로직 삽입

Behavior Pool은 CLASS ... DEFINITION FOR BEHAVIOR OF ... 구문으로 선언되며, 내부에 LOCAL CLASS lhc_엔티티명들이 위치합니다. 각 lhc 클래스는 다음 핸들러 메서드를 포함할 수 있습니다.

  • FOR MODIFY — Create/Update/Delete 처리
  • FOR READ — 비표준 읽기 로직(주로 unmanaged)
  • FOR VALIDATE ON SAVE — 저장 시점 유효성 검사
  • FOR DETERMINE ON MODIFY / ON SAVE — 파생 필드 자동 계산
  • FOR ACTION — 비즈니스 액션 실행

호출 흐름은 일반적으로 Modify → Determine on Modify → (사용자 SAVE 요청) → Determine on Save → Validate on Save → Save Sequence(adjust_numbers → save → cleanup) 순으로 진행됩니다.

1단계 — BDEF 선언과 기본 핸들러 골격

먼저 구매요청 루트 엔티티에 대한 Behavior Definition을 작성합니다. implementation type managed를 선언하면 ADT가 Behavior Pool 골격을 자동 제안합니다.

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

define behavior for ZI_PurchaseRequisition alias PurchaseReq
persistent table zpr_header
lock master
authorization master ( instance )
etag master LastChangedAt
{
  field ( numbering : managed, readonly ) RequisitionUUID;
  field ( readonly ) RequisitionNo, CreatedAt, CreatedBy,
                     LastChangedAt, LastChangedBy;
  field ( mandatory ) MaterialCode, RequestedQty, PlantCode;

  create;
  update;
  delete;

  action ( features : instance ) submitForApproval result [1] $self;
  validation validateQuantity on save { field RequestedQty; }
  determination calcTotalValue on modify { field RequestedQty, UnitPrice; }
}

위 선언만으로도 OData CRUD가 동작합니다. 이제 Behavior Pool 클래스 zbp_i_purchasereq를 생성하면 ADT가 다음과 같은 빈 골격을 제안합니다.

CLASS zbp_i_purchasereq DEFINITION
  PUBLIC ABSTRACT FINAL FOR BEHAVIOR OF ZI_PurchaseRequisition.
ENDCLASS.

CLASS zbp_i_purchasereq IMPLEMENTATION.
ENDCLASS.

2단계 — Validation, Determination, 그리고 로깅

실무에서는 단순 CRUD만으로 끝나지 않습니다. 수량이 0 이하이면 차단하고, 총액 필드를 자동 계산하며, 예외 발생 시 메시지 로그를 남겨야 합니다. Local Handler Class에 다음과 같이 구현합니다.

CLASS lhc_purchasereq DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS validateQuantity FOR VALIDATE ON SAVE
      IMPORTING keys FOR PurchaseReq~validateQuantity.

    METHODS calcTotalValue FOR DETERMINE ON MODIFY
      IMPORTING keys FOR PurchaseReq~calcTotalValue.
ENDCLASS.

CLASS lhc_purchasereq IMPLEMENTATION.

  METHOD validateQuantity.
    READ ENTITIES OF ZI_PurchaseRequisition IN LOCAL MODE
      ENTITY PurchaseReq
        FIELDS ( RequisitionNo RequestedQty )
        WITH CORRESPONDING #( keys )
      RESULT DATA(lt_req).

    LOOP AT lt_req INTO DATA(ls_req).
      IF ls_req-RequestedQty <= 0.
        APPEND VALUE #( %tky = ls_req-%tky ) TO failed-purchasereq.
        APPEND VALUE #(
          %tky        = ls_req-%tky
          %msg        = NEW zcm_purchasereq(
                          severity = if_abap_behv_message=>severity-error
                          textid   = zcm_purchasereq=>qty_not_positive
                          reqno    = ls_req-RequisitionNo )
          %element-RequestedQty = if_abap_behv=>mk-on
        ) TO reported-purchasereq.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD calcTotalValue.
    READ ENTITIES OF ZI_PurchaseRequisition IN LOCAL MODE
      ENTITY PurchaseReq
        FIELDS ( RequestedQty UnitPrice )
        WITH CORRESPONDING #( keys )
      RESULT DATA(lt_req).

    LOOP AT lt_req ASSIGNING FIELD-SYMBOL(<fs>).
      <fs>-TotalValue = <fs>-RequestedQty * <fs>-UnitPrice.
    ENDLOOP.

    MODIFY ENTITIES OF ZI_PurchaseRequisition IN LOCAL MODE
      ENTITY PurchaseReq
        UPDATE FIELDS ( TotalValue )
        WITH VALUE #( FOR row IN lt_req
                      ( %tky        = row-%tky
                        TotalValue  = row-TotalValue ) )
      REPORTED DATA(ls_reported).
  ENDMETHOD.

ENDCLASS.

여기서 핵심은 READ ENTITIES ... IN LOCAL MODE입니다. 권한 검사를 우회하여 트랜잭션 버퍼에서 현재 값을 안전하게 조회합니다. 메시지 클래스(zcm_purchasereq)는 일반적으로 if_t100_dyn_msg를 구현해 다국어 메시지를 깔끔하게 노출하는 패턴을 권장합니다.

3단계 — Action 구현과 프로덕션 고려사항

이제 submitForApproval 액션을 구현하여 결재 상태를 변경하고, 결과 인스턴스를 반환합니다. 동시에 단위 테스트가 가능하도록 의존성을 분리하는 것이 권장됩니다.

CLASS lhc_purchasereq DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS submitForApproval FOR MODIFY
      IMPORTING keys FOR ACTION PurchaseReq~submitForApproval
                          RESULT result.

    METHODS get_instance_features FOR INSTANCE FEATURES
      IMPORTING keys REQUEST requested_features FOR PurchaseReq
                          RESULT result.
ENDCLASS.

CLASS lhc_purchasereq IMPLEMENTATION.

  METHOD submitForApproval.
    READ ENTITIES OF ZI_PurchaseRequisition IN LOCAL MODE
      ENTITY PurchaseReq
        FIELDS ( ApprovalStatus TotalValue )
        WITH CORRESPONDING #( keys )
      RESULT DATA(lt_req)
      FAILED failed.

    DATA lt_update TYPE TABLE FOR UPDATE ZI_PurchaseRequisition.

    LOOP AT lt_req INTO DATA(ls_req).
      IF ls_req-ApprovalStatus <> 'D'.  " Draft 상태만 제출 가능
        APPEND VALUE #( %tky = ls_req-%tky ) TO failed-purchasereq.
        APPEND VALUE #(
          %tky = ls_req-%tky
          %msg = NEW zcm_purchasereq(
                   severity = if_abap_behv_message=>severity-error
                   textid   = zcm_purchasereq=>invalid_status )
        ) TO reported-purchasereq.
        CONTINUE.
      ENDIF.

      APPEND VALUE #(
        %tky           = ls_req-%tky
        ApprovalStatus = 'S'           " Submitted
        SubmittedAt    = cl_abap_context_info=>get_system_date( )
      ) TO lt_update.
    ENDLOOP.

    MODIFY ENTITIES OF ZI_PurchaseRequisition IN LOCAL MODE
      ENTITY PurchaseReq
        UPDATE FIELDS ( ApprovalStatus SubmittedAt )
        WITH lt_update.

    READ ENTITIES OF ZI_PurchaseRequisition IN LOCAL MODE
      ENTITY PurchaseReq ALL FIELDS
        WITH CORRESPONDING #( lt_update )
      RESULT DATA(lt_final).

    result = VALUE #( FOR row IN lt_final
                      ( %tky   = row-%tky
                        %param = row ) ).
  ENDMETHOD.

  METHOD get_instance_features.
    READ ENTITIES OF ZI_PurchaseRequisition IN LOCAL MODE
      ENTITY PurchaseReq
        FIELDS ( ApprovalStatus )
        WITH CORRESPONDING #( keys )
      RESULT DATA(lt_req).

    result = VALUE #(
      FOR row IN lt_req
      ( %tky                           = row-%tky
        %action-submitForApproval = COND #(
          WHEN row-ApprovalStatus = 'D'
          THEN if_abap_behv=>fc-o-enabled
          ELSE if_abap_behv=>fc-o-disabled ) ) ).
  ENDMETHOD.

ENDCLASS.

프로덕션 단계에서는 다음을 추가로 검토하는 것이 일반적으로 권장됩니다.

  • 단위 테스트: CL_CDS_TEST_ENVIRONMENT와 EML 더블 프레임워크를 활용해 BDEF 동작을 격리 테스트
  • 권한: FOR GLOBAL AUTHORIZATION 핸들러에서 AUTHORITY-CHECK 또는 PFCG 권한 객체 적용
  • 성능: READ ENTITIES는 일괄 키로 한 번에 호출(루프 안에서 단건 호출 금지)
  • 로깅: 액션 결과를 별도 zlog_audit 테이블에 기록하여 감사 추적성 확보

흔한 실수와 트러블슈팅 FAQ

Behavior Implementation을 작성하다 보면 비슷한 함정에 반복적으로 빠집니다. 자주 마주치는 사례를 정리합니다.

Q1. SELECT로 DB를 직접 읽었는데 새로 입력한 값이 보이지 않아요.
A. RAP는 트랜잭션 버퍼에 변경 사항을 모아두었다가 SAVE 시점에 DB에 반영합니다. 따라서 validation/determination에서는 반드시 READ ENTITIES ... IN LOCAL MODE로 버퍼 상태를 조회해야 합니다. SELECT는 커밋되지 않은 변경분을 보지 못합니다.

Q2. validation에서 에러를 reported에 넣었는데도 저장이 진행됩니다.
A. 에러로 인정받으려면 failed 테이블에도 동일 키를 함께 추가해야 합니다. reported는 메시지 전달용일 뿐이며, 트랜잭션을 실제로 롤백시키는 것은 failed입니다. 또한 메시지 severity가 error여야 합니다.

Q3. Action을 호출했는데 "Behavior implementation for action not found" 덤프가 발생합니다.
A. BDEF의 alias와 핸들러 메서드의 FOR ACTION 별칭~액션명이 정확히 일치하는지 확인하세요. 흔히 BDEF에서는 별칭을 쓰면서 핸들러에서는 원래 엔티티 이름을 쓰거나 그 반대 실수가 잦습니다. 또한 BDEF를 수정한 뒤 클래스를 다시 활성화하지 않으면 메타데이터 불일치가 발생합니다.

Q4. Determine on modify가 기대보다 자주 호출됩니다.
A. field 트리거 목록에 들어간 필드가 한 번이라도 수정되면 같은 트랜잭션 안에서 여러 번 호출될 수 있습니다. 멱등성(idempotency)이 보장되도록 작성하고, 외부 API 호출 같은 무거운 작업은 determine on save로 옮기는 편이 안전합니다.

이어서 확장해 볼 주제

이 글에서 다룬 패턴은 단일 루트 엔티티 기준입니다. 실무 비즈니스 객체는 보통 헤더-아이템 컴포지션 구조를 가지므로 Composition 선언과 FOR MODIFY 핸들러의 CREATE_BA 분기 처리로 확장해 보시기를 권장합니다. 이어서 Draft 활성화, Side Effects, Numbering 전략(early/late numbering), 그리고 EML을 활용한 외부 컨슈머 시나리오로 학습을 이어가면 RAP 전체 그림이 완성됩니다. BTP ABAP Environment 사용자라면 Service Binding을 OData V4 UI로 노출하여 SAP Fiori Elements 앱과 연결하는 단계까지 직접 체험해 볼 만합니다.

더 깊이 읽어볼 자료

댓글 0

아직 댓글이 없습니다.