RAP

Behavior Pool 몰아넣기 vs 그룹 분리 #shorts #SAP #RAP

▶ YouTube에서 보기

📖 개요

RAP(ABAP RESTful Application Programming Model)에서 Behavior Pool은 BO의 두뇌 역할을 합니다. 프로젝트가 커지면 LHC_ 로컬 핸들러 클래스 하나에 FOR MODIFY, FOR READ, FOR DETERMINE, FOR VALIDATE가 전부 쌓이면서 3,000줄짜리 괴물 클래스가 탄생하곤 합니다. 이 글은 수주(SalesOrder) BO를 소재로, 핸들러를 엔티티·연산 단위로 쪼개는 설계 기준을 단계별 실전 예제로 다룹니다.

  • 단일 핸들러 클래스가 유발하는 협업·리뷰·테스트 문제를 짚는다
  • 엔티티(Root/Child)와 연산(CUD/Action/Validation) 기준의 분리 원칙을 세운다
  • 로컬 클래스 다중화, 엔티티별 구현 클래스, group 구문까지 3단계로 적용한다

📚 읽기 전에 갖추면 좋은 배경

CDS 뷰 엔티티와 BDEF의 기본 구조, managed/unmanaged 시나리오 차이, EML(MODIFY ENTITIES, READ ENTITIES) 문법을 다뤄본 경험을 전제로 합니다. RAP 자체가 처음이라면 단일 엔티티 managed BO를 먼저 만들어 본 뒤 돌아오는 편이 효율적입니다.

🔧 환경과 버전

이 글의 코드는 SAP BTP ABAP Environment 또는 SAP S/4HANA 2022 이상 기준입니다. BDEF의 group 기반 구현 분할은 최근 릴리스에서 지원되므로 온프레미스라면 지원 여부를 먼저 확인하세요. 편집은 ADT에서만 가능하며, 로컬 클래스 다중화 패턴(1~2단계)은 구버전에서도 동작합니다.

💡 핵심 개념 — 프레임워크는 클래스가 아니라 '메서드 선언'을 본다

Behavior Pool(ZBP_*)의 글로벌 클래스는 FOR BEHAVIOR OF가 붙은 빈 껍데기이고, 실제 로직은 CCIMP(로컬 타입) 안의 핸들러 클래스(cl_abap_behavior_handler 상속)와 세이버 클래스(cl_abap_behavior_saver 상속)에 들어갑니다. 핵심 원리는 하나입니다. RAP 런타임은 "어떤 클래스에 있는가"가 아니라 "어떤 메서드 선언(FOR MODIFY ... FOR CREATE SalesOrder)이 존재하는가"로 디스패치합니다. 즉 로컬 핸들러 클래스를 여러 개 두어도, 각 연산이 전체에서 정확히 한 번만 구현되어 있으면 프레임워크가 올바른 클래스를 호출합니다.

비유하자면 Behavior Pool은 병원, 핸들러 클래스는 진료과입니다. 의사를 "통합진료실" 하나에 몰아넣어도 병원은 돌아가지만, 의사들끼리는 같은 방(클래스)을 쓰느라 계속 부딪힙니다. 실제로 단일 거대 핸들러는 다음 문제를 만듭니다.

  • 책임 경계 붕괴 — Header 검증 로직과 Item 수량 계산이 한 클래스의 private 메서드 더미 속에 섞여 소속 추적이 어렵습니다
  • 협업 충돌 — 팀원 A가 액션을, B가 Validation을 수정하면 같은 include를 건드려 전송·머지 충돌이 상시 발생합니다
  • 리뷰·테스트 비용 증가 — PR 단위가 커지고, ABAP Unit에서 특정 연산만 격리 검증하기 어려워집니다

분리 기준은 두 축입니다. 1축은 엔티티(Root인 Header와 Child인 Item은 별도 클래스), 2축은 연산 성격(CUD vs Action vs Determination/Validation)입니다. 소규모 BO는 1축만으로 충분하고, 액션이 5개 이상이거나 검증이 많아지면 2축까지 적용하는 것이 일반적으로 권장됩니다.

💻 실전 코드 3단계

1단계 — 기본형: 하나의 풀, 엔티티별 로컬 클래스

가장 손쉬운 첫 분리는 BDEF는 그대로 두고 CCIMP 안에서 로컬 클래스만 엔티티별로 나누는 것입니다.

" ZBP_R_SALESORDERTP 의 로컬 타입(CCIMP)
CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS calculatetotal FOR DETERMINE ON MODIFY
      IMPORTING keys FOR SalesOrder~calculateTotal.
    METHODS validatecustomer FOR VALIDATE ON SAVE
      IMPORTING keys FOR SalesOrder~validateCustomer.
ENDCLASS.

CLASS lhc_salesorderitem DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS validatequantity FOR VALIDATE ON SAVE
      IMPORTING keys FOR SalesOrderItem~validateQuantity.
ENDCLASS.

클래스가 하나든 둘이든 런타임 동작은 동일하므로 기존 BO에도 리스크 없이 적용 가능한 리팩터링입니다. 단, 같은 연산을 두 클래스에서 중복 구현하면 오류가 나므로 "연산당 구현은 전체 풀에서 단 1회" 원칙을 지켜야 합니다.

2단계 — 실무형: 엔티티별 Pool 분리 + 에러·로그 처리

팀 단위 개발이라면 전송 충돌을 줄이기 위해 BDEF에서 엔티티마다 구현 클래스를 지정해 풀을 나눕니다.

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

define behavior for ZR_SalesOrderTP alias SalesOrder
implementation in class zbp_so_header unique
persistent table zso_header
lock master
authorization master ( instance )
{
  create; update; delete;
  action ( features : instance ) cancelOrder result [1] $self;
  association _Item { create; }
}

define behavior for ZR_SalesOrderItemTP alias SalesOrderItem
implementation in class zbp_so_item unique
persistent table zso_item
lock dependent by _Header
authorization dependent by _Header
{
  update; delete;
  field ( readonly ) HeaderUuid;
  association _Header;
}

zbp_so_headerzbp_so_item은 물리적으로 다른 오브젝트라 담당자별 전송이 완전히 분리됩니다. 액션 구현에는 failed/reported 처리와 애플리케이션 로그를 함께 넣습니다.

METHOD cancelorder.
  READ ENTITIES OF zr_salesordertp IN LOCAL MODE
    ENTITY SalesOrder
      FIELDS ( OverallStatus ) WITH CORRESPONDING #( keys )
    RESULT DATA(lt_orders)
    FAILED DATA(lt_read_failed).
  failed = CORRESPONDING #( DEEP lt_read_failed ).

  LOOP AT lt_orders INTO DATA(ls_order).
    IF ls_order-OverallStatus = 'D'.  " 이미 배송 완료
      APPEND VALUE #( %tky = ls_order-%tky ) TO failed-salesorder.
      APPEND VALUE #(
        %tky = ls_order-%tky
        %msg = new_message_with_text(
                 severity = if_abap_behv_message=>severity-error
                 text     = '배송 완료된 수주는 취소할 수 없습니다.' )
      ) TO reported-salesorder.
      zcl_so_app_log=>get( )->add_error(
        iv_text = |취소 거부: { ls_order-SalesOrderId }| ).
      CONTINUE.
    ENDIF.
    MODIFY ENTITIES OF zr_salesordertp IN LOCAL MODE
      ENTITY SalesOrder
        UPDATE FIELDS ( OverallStatus )
        WITH VALUE #( ( %tky = ls_order-%tky OverallStatus = 'X' ) ).
  ENDLOOP.
ENDMETHOD.

3단계 — group 구문으로 연산 단위 분할 + 테스트 격리

액션·검증이 늘어나면 엔티티 축만으로 부족합니다. 최근 BDEF는 group 블록으로 연산 묶음마다 별도 구현 클래스를 지정할 수 있습니다.

define behavior for ZR_SalesOrderTP alias SalesOrder
implementation in class zbp_so_header unique
...
{
  group cud implementation in class zbp_so_header_cud unique
  {
    create; update; delete;
  }
  group act implementation in class zbp_so_header_act unique
  {
    action ( features : instance ) cancelOrder result [1] $self;
    action releaseOrder result [1] $self;
  }
  " determination/validation 은 기본 클래스(zbp_so_header)에 유지
}

이렇게 나누면 ABAP Unit에서 액션 그룹만 대상으로 격리 검증이 쉬워집니다.

CLASS ltc_cancel_action DEFINITION FINAL FOR TESTING
  DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    METHODS cancel_rejected_when_shipped FOR TESTING.
ENDCLASS.

CLASS ltc_cancel_action IMPLEMENTATION.
  METHOD cancel_rejected_when_shipped.
    " 배송 완료(D) 상태 수주를 심은 뒤 cancelOrder 호출
    MODIFY ENTITIES OF zr_salesordertp
      ENTITY SalesOrder
        EXECUTE cancelOrder FROM VALUE #( ( SalesOrderUuid = mv_uuid ) )
      FAILED DATA(failed) REPORTED DATA(reported).
    cl_abap_unit_assert=>assert_not_initial( failed-salesorder ).
  ENDMETHOD.
ENDCLASS.

보안 관점에서는 get_instance_authorizations·get_instance_features를 CUD 그룹이 아닌 기본 클래스에 남겨 인가 로직이 연산 구현과 섞이지 않게 하는 구조가 일반적으로 안전합니다.

⚠️ 흔한 실수

Q1. 두 로컬 클래스에 같은 FOR MODIFY를 선언했더니 오류가 납니다.
연산별 구현은 풀 전체에서 유일해야 합니다. 메서드 이동은 "복사 → 원본 삭제"보다 "잘라내기 → 붙여넣기 → 활성화" 순서가 중복 선언 실수를 막습니다.

Q2. 엔티티별 풀로 나눴는데 Child 쪽 핸들러가 호출되지 않습니다.
Child 엔티티의 define behavior for 줄에 implementation in class ... unique가 지정됐는지, Header 풀에 옛 Item 핸들러 선언이 남아 디스패치를 가로채고 있진 않은지 확인하세요.

Q3. group 구문이 구문 오류로 표시됩니다.
릴리스가 구현 그룹핑을 지원하지 않을 가능성이 큽니다. S/4HANA 2021 이하라면 group 없이 로컬 클래스 다중화 + 엔티티별 풀 분리만으로도 대부분의 효과를 얻을 수 있습니다.

🚀 여기서 더 나아가기

구조 분리가 끝났다면 세이버 클래스(LSC_)의 additional save 분리, 재사용 로직의 글로벌 클래스 추출, BDEF 상속별 구현 클래스 전략으로 확장해 보세요. 대규모 BO일수록 이 세 가지를 반드시 마주치게 됩니다.

📚 근거 자료와 링크

댓글 0

아직 댓글이 없습니다.