RAP

Static vs Dynamic — 상태별 UI 통제 기준 #shorts #SAP #RAP

▶ YouTube에서 보기

Feature Control이란 무엇이며 왜 필요한가

RAP(ABAP RESTful Application Programming Model)에서 비즈니스 객체를 설계할 때 가장 자주 마주치는 요구사항 중 하나는 "어떤 상태에서는 특정 필드를 수정할 수 없게 막아야 한다", "특정 권한이 없으면 액션 버튼을 숨겨야 한다" 같은 동적 UI 통제입니다. Feature Control은 바로 이러한 요구를 RAP 런타임이 표준 방식으로 처리하도록 만들어진 메커니즘입니다.

이 글을 통해 확인할 수 있는 내용은 다음과 같습니다.

  • Static Feature Control과 Dynamic Feature Control의 본질적 차이
  • BDEF(Behavior Definition)에서의 선언 방법과 핸들러 구현 패턴
  • SalesOrder/PurchaseRequisition 같은 실무 시나리오로 보는 적용 사례
  • 성능, 유지보수, Fiori Elements 호환성 관점의 선택 기준
  • 흔한 트러블슈팅 포인트와 안티패턴

이 글을 읽기 전에 알고 있으면 좋은 것

본 내용은 RAP의 기본 흐름(BDEF, Behavior Implementation Class, Projection Layer)을 이미 접해본 개발자를 대상으로 합니다. CDS View Entity, Managed/Unmanaged 시나리오 구분, Draft 활성화 개념, 그리고 Fiori Elements가 OData V4 메타데이터를 어떻게 해석하는지를 한 번이라도 다뤄봤다면 이후 코드가 자연스럽게 읽힙니다. ABAP RESTful Programming Model의 BDEF 키워드(create, update, delete, action, field)에 대한 기본 인지가 전제됩니다.

실습 환경과 버전 정보

아래 예제는 일반적으로 다음 환경에서 검증됩니다.

  • SAP S/4HANA Cloud Private Edition 2023 또는 Public Edition (ABAP Cloud)
  • SAP BTP, ABAP Environment (Steampunk) — 릴리스 2402 이상 권장
  • ABAP Development Tools (ADT) for Eclipse 3.40 이상
  • RAP Managed Scenario 기준 (Unmanaged의 경우 핸들러 위치만 다름)
  • Fiori Elements (List Report / Object Page, OData V4)

예제는 ZRAP_SALES_ORDER라는 가상의 Sales Order 비즈니스 객체를 기준으로 진행합니다. 실제 SAP 표준 객체가 아닌 학습용 네임스페이스이므로, 사용자 환경의 패키지·테이블·CDS 명만 조정하면 그대로 적용해볼 수 있습니다.

두 가지 Feature Control의 동작 원리

Feature Control을 비유로 표현하면 다음과 같습니다.

Static은 "법령에 박혀 있는 규칙"이고, Dynamic은 "현장 매니저의 판단"입니다. 법령은 항상 동일하게 적용되어 컴파일 타임에 메타데이터로 박히지만, 매니저의 판단은 매 요청마다 데이터를 보고 결정합니다.

좀 더 기술적으로 정리하면 다음 표와 같습니다.

구분Static Feature ControlDynamic Feature Control
결정 시점설계 시점 (BDEF 컴파일)런타임 (인스턴스 단위)
위치BDEF의 field/actionBehavior Implementation의 GET 핸들러
적용 범위모든 인스턴스에 일괄각 row(인스턴스)마다 다르게
대표 키워드read only, mandatoryfeatures ( instance )
성능오버헤드 없음핸들러 호출 비용 발생

핵심 포인트는 "이 규칙이 데이터의 상태(예: 결재 완료, 잠금 플래그, 사용자 권한)에 의존하는가"입니다. 의존한다면 Dynamic, 아니라면 Static이 자연스러운 선택입니다. 두 방식은 동일한 필드/액션에 동시에 적용할 수 없습니다. 즉, 어떤 필드를 read only로 선언했다면 다시 read only ( features : instance )로 덮어쓸 수 없습니다.

또한 Feature Control이 결정한 결과는 OData V4의 Core.Computed, Core.Immutable, FieldControl, OperationControl 같은 annotation으로 매핑되어 Fiori Elements UI에 자동으로 반영됩니다. 이 자동 매핑이 RAP Feature Control을 사용해야 하는 가장 큰 이유 중 하나입니다.

Static Feature Control 실전 예제

먼저 가장 단순한 형태인 Static입니다. Sales Order 시나리오에서 다음 비즈니스 규칙을 가정합니다.

  • OrderID: 시스템이 자동 생성하므로 사용자가 절대 입력할 수 없다
  • CreatedAt, CreatedBy: 생성 시 시스템이 채우고 이후 절대 수정 불가
  • CustomerID: 생성 시점에만 입력 가능, 이후 변경 불가
  • Currency: 필수 입력
managed implementation in class zbp_rap_sales_order unique;
strict ( 2 );

define behavior for ZI_RAP_SalesOrder alias SalesOrder
persistent table zrap_sord
lock master
authorization master ( instance )
etag master LastChangedAt
{
  create;
  update;
  delete;

  field ( numbering : managed, readonly ) OrderID;
  field ( readonly ) CreatedAt, CreatedBy, LastChangedAt, LastChangedBy;
  field ( readonly : update ) CustomerID;
  field ( mandatory ) Currency, OrderType;

  mapping for zrap_sord
  {
    OrderID       = order_id;
    CustomerID    = customer_id;
    Currency      = currency;
    OrderType     = order_type;
    CreatedAt     = created_at;
    CreatedBy     = created_by;
    LastChangedAt = last_changed_at;
    LastChangedBy = last_changed_by;
  }
}

위 BDEF에서 주목할 부분은 field ( readonly : update )입니다. 이는 "생성 시에는 입력 가능하지만 update 오퍼레이션에서는 읽기 전용"이라는 정밀한 컨트롤입니다. numbering : managed와 함께 쓰면 OrderID는 시스템이 자동 채번하고 사용자는 절대 건드릴 수 없게 됩니다. 이 모든 결정은 BDEF 활성화 시점에 메타데이터로 굳어져 런타임 비용이 0에 가깝습니다.

Dynamic Feature Control 실전 예제

이제 Static으로는 표현할 수 없는 시나리오를 봅시다. 다음 규칙은 인스턴스의 상태에 따라 달라집니다.

  • 주문 상태(OrderStatus)가 'R'(Released)이면 모든 필드가 잠긴다
  • cancelOrder 액션은 OrderStatus'N'(New) 또는 'P'(Pending)일 때만 활성화
  • approveOrder 액션은 금액이 일정 한도를 넘으면 비활성화
  • Delete는 결재 전('N')일 때만 허용

BDEF는 다음과 같이 작성합니다.

define behavior for ZI_RAP_SalesOrder alias SalesOrder
persistent table zrap_sord
lock master
etag master LastChangedAt
{
  update ( features : instance );
  delete ( features : instance );

  action ( features : instance ) cancelOrder  result [1] $self;
  action ( features : instance ) approveOrder result [1] $self;

  field ( features : instance ) NetAmount, DeliveryDate, Note;
}

이렇게 features : instance로 선언하면 RAP 런타임은 해당 오퍼레이션/필드를 화면에 그릴 때마다 핸들러를 호출합니다. 다음은 Behavior Implementation Class의 GET 핸들러입니다.

CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS get_instance_features FOR INSTANCE FEATURES
      IMPORTING keys REQUEST requested_features FOR SalesOrder RESULT result.
ENDCLASS.

CLASS lhc_salesorder IMPLEMENTATION.
  METHOD get_instance_features.

    READ ENTITIES OF zi_rap_salesorder IN LOCAL MODE
      ENTITY SalesOrder
        FIELDS ( OrderStatus NetAmount )
        WITH CORRESPONDING #( keys )
      RESULT DATA(orders)
      FAILED failed.

    result = VALUE #(
      FOR order IN orders
      ( %tky                     = order-%tky

        %action-cancelOrder      = COND #(
          WHEN order-OrderStatus = 'R'
          THEN if_abap_behv=>fc-o-disabled
          ELSE if_abap_behv=>fc-o-enabled )

        %action-approveOrder     = COND #(
          WHEN order-OrderStatus = 'R' OR order-NetAmount > 100000
          THEN if_abap_behv=>fc-o-disabled
          ELSE if_abap_behv=>fc-o-enabled )

        %update                  = COND #(
          WHEN order-OrderStatus = 'R'
          THEN if_abap_behv=>fc-o-disabled
          ELSE if_abap_behv=>fc-o-enabled )

        %delete                  = COND #(
          WHEN order-OrderStatus = 'N'
          THEN if_abap_behv=>fc-o-enabled
          ELSE if_abap_behv=>fc-o-disabled )

        %field-NetAmount         = COND #(
          WHEN order-OrderStatus = 'R'
          THEN if_abap_behv=>fc-f-read_only
          ELSE if_abap_behv=>fc-f-unrestricted )

        %field-DeliveryDate      = COND #(
          WHEN order-OrderStatus = 'R'
          THEN if_abap_behv=>fc-f-read_only
          ELSE if_abap_behv=>fc-f-unrestricted )
      ) ).

  ENDMETHOD.
ENDCLASS.

여기서 사용된 상수는 RAP가 제공하는 표준 인터페이스 if_abap_behv의 멤버이며, 액션/오퍼레이션에는 fc-o-*를, 필드에는 fc-f-*를 사용합니다. 잘못 섞어 쓰면 활성화 시점에 오류가 발생합니다.

프로덕션 단계의 성능·보안 강화 패턴

실제 운영에 올릴 때는 다음 세 가지를 추가로 고려해야 합니다.

METHOD get_instance_features.

  " 1) requested_features를 확인해 필요한 필드만 읽는다
  DATA(read_fields) = VALUE string_table( ( |OrderStatus| ) ).
  IF requested_features-%action-approveOrder = if_abap_behv=>mk-on
     OR requested_features-%field-NetAmount  = if_abap_behv=>mk-on.
    APPEND `NetAmount` TO read_fields.
  ENDIF.

  " 2) 권한 캐싱 — 같은 유저의 권한은 반복 조회하지 않는다
  IF authority_cache IS INITIAL.
    AUTHORITY-CHECK OBJECT 'Z_SORD_APP'
      ID 'ACTVT' FIELD '43'.
    authority_cache = sy-subrc.
  ENDIF.
  DATA(can_approve) = xsdbool( authority_cache = 0 ).

  READ ENTITIES OF zi_rap_salesorder IN LOCAL MODE
    ENTITY SalesOrder
      FIELDS ( OrderStatus NetAmount )
      WITH CORRESPONDING #( keys )
    RESULT DATA(orders).

  result = VALUE #(
    FOR order IN orders
    ( %tky                = order-%tky
      %action-approveOrder = COND #(
        WHEN can_approve = abap_false              THEN if_abap_behv=>fc-o-disabled
        WHEN order-OrderStatus = 'R'               THEN if_abap_behv=>fc-o-disabled
        WHEN order-NetAmount   > 100000            THEN if_abap_behv=>fc-o-disabled
        ELSE if_abap_behv=>fc-o-enabled ) ) ).

ENDMETHOD.

핵심은 세 가지입니다. 첫째, requested_features를 살펴 정말 필요한 필드만 DB에서 읽습니다. List Report가 100건을 한 번에 그릴 때 매번 모든 컬럼을 SELECT하면 성능이 급격히 떨어집니다. 둘째, 인증·권한은 인스턴스 루프 안이 아니라 메서드 시작 시 한 번만 평가합니다. 셋째, 평가 결과가 결정적(deterministic)이어야 합니다. 같은 입력에 다른 결과를 돌려주면 ETag 충돌과 클라이언트 캐시 문제가 발생합니다.

실무에서 자주 만나는 함정과 대처

아래 패턴은 RAP 도입 초기에 거의 모든 팀이 한 번씩 밟는 지뢰입니다.

  • Static과 Dynamic 중복 선언: 같은 필드에 field ( readonly )field ( features : instance )를 동시에 걸면 활성화 오류가 납니다. 둘 중 하나만 선택해야 합니다.
  • 핸들러에서 키 누락: result%tky를 채우지 않으면 Fiori UI에 컨트롤이 반영되지 않습니다. 모든 row에 키를 정확히 매핑해야 합니다.
  • fc-f와 fc-o 혼동: 액션·오퍼레이션에는 enabled/disabled, 필드에는 read_only/mandatory/unrestricted를 사용합니다. 잘못 넣으면 컴파일은 통과해도 런타임에서 의도와 다르게 동작합니다.
  • Draft 시나리오 누락: Draft가 활성화된 경우 Active 인스턴스뿐 아니라 Draft 인스턴스에 대해서도 동일한 판단이 이뤄지도록 핸들러가 두 단계 모두를 커버해야 합니다.

자주 묻는 질문은 다음과 같습니다.

Q1. Feature Control을 Validation으로 대체할 수 있지 않나요?
Validation은 "저장 직전에 거부"하는 사후 통제이고, Feature Control은 "처음부터 UI에 노출되지 않게" 하는 사전 통제입니다. 사용자 경험 측면에서 가능한 한 Feature Control로 막고, 비즈니스 무결성 검증은 Validation으로 이중 방어하는 패턴이 일반적으로 권장됩니다.

Q2. 핸들러가 너무 자주 호출되어 느립니다.
List Report에서 30~100건이 동시에 평가되므로 핸들러는 set-based로 작성해야 합니다. 루프 안에서 SELECT를 호출하는 패턴은 즉시 제거하고, READ ENTITIES ... WITH CORRESPONDING #( keys )로 한 번에 가져오는 것을 권장합니다.

Q3. Projection Layer에서도 Feature Control을 또 선언해야 하나요?
Projection BDEF는 노출 계층이므로 use 키워드로 상속만 받으면 기본 동작은 그대로 따라옵니다. 다만 특정 서비스(예: 외부 API용 vs 내부 Fiori용)에서만 추가 제약을 걸고 싶다면 Projection 레벨에 별도 field·action 선언을 추가할 수 있습니다.

선택 기준 한 줄 요약

판단 기준을 한 줄로 정리하면 이렇습니다. "데이터의 현재 상태 또는 사용자 권한에 따라 결과가 달라져야 한다면 Dynamic, 그렇지 않다면 Static." Static을 먼저 적용하고, Static으로 표현 불가능한 부분만 Dynamic으로 보강하는 순서가 코드 품질과 성능 양쪽에 가장 유리합니다.

이 글의 내용을 익혔다면 다음 주제로 넘어가는 것을 권장합니다.

  • Determinations: 상태 변경 시 자동 계산되는 필드 처리
  • Validations: 저장 전 비즈니스 규칙 검증
  • Authorization Control(get_global_authorizations, get_instance_authorizations)과의 결합
  • Side Effects 어노테이션: Feature Control 결과를 즉시 UI에 반영시키기
  • Draft-Enabled 시나리오에서의 Feature Control 일관성

댓글 0

아직 댓글이 없습니다.