개요 및 이 글에서 다루는 내용
SAP RAP(RESTful ABAP Programming Model)에서 Action은 비즈니스 오브젝트에 상태 변경 로직을 담는 핵심 구성 요소입니다. 하지만 실무에서 Action을 구현할 때 RESULT 파라미터를 제대로 채우지 않아 Fiori Elements 화면이 새로고침되지 않거나, OData 응답에 빈 객체가 반환되어 후속 로직이 깨지는 사례가 매우 흔합니다. 이 글은 판매 주문 승인(SalesOrderApproval) 시나리오를 통해 Action 응답 반환의 3단계 — RESULT 필드 정의, 값 채우기, 매핑 확인 — 를 단계별로 설명합니다.
- RAP Action의 RESULT 파라미터 역할과 내부 동작 원리 이해
- BDEF에서 result 선언과 handler class에서의 매핑 규칙 파악
- 실무에서 자주 발생하는 빈 응답 / 매핑 누락 트러블슈팅 습득
- 프로덕션 관점의 검증 로직, 에러 처리, 성능 고려사항 정리
이 글을 읽기 전 알아두면 좋은 것
ABAP RESTful Application Programming Model의 기본 구조(BDEF, Behavior Implementation Class, CDS Projection View)에 대한 기초적인 이해가 필요합니다. Managed 또는 Unmanaged 시나리오 중 하나를 실습해본 경험이 있고, ABAP Development Tools(ADT)에서 CDS와 behavior definition 파일을 편집해본 적이 있다면 이 글의 코드 예제를 무리 없이 따라올 수 있습니다.
실습 환경과 준비물
이 글의 코드는 다음 환경 기준으로 검증되었습니다.
- SAP S/4HANA 2022 이상 또는 SAP BTP ABAP Environment (Steampunk) 최신 릴리스
- ABAP Development Tools (ADT) 3.34 이상, Eclipse 2023-09 권장
- ABAP Cloud 개발 모델 또는 On-Stack Developer Extensibility 활성화된 시스템
- Fiori Elements 프리뷰를 위한 Service Binding(OData V4) 구성
- 테이블:
zsalesorder(헤더),zsalesorder_item(아이템) — 실습용 커스텀 테이블 가정
ABAP Cloud 환경에서는 릴리스 상태 API만 사용 가능하다는 점에 유의하고, 온프레미스 환경이라면 SAP_BASIS 758 이상에서 최신 RAP 문법이 안정적으로 지원됩니다.
RAP Action과 RESULT 파라미터의 동작 원리
RAP에서 Action은 "특정 인스턴스(또는 정적 컨텍스트)에 대해 실행되는 사용자 정의 오퍼레이션"입니다. 표준 CRUD로는 표현하기 어려운 상태 전이(예: 승인, 반려, 결제 처리, 배송 요청)를 캡슐화하는 역할을 합니다. 여기서 클라이언트가 Action을 호출한 뒤 화면을 즉시 최신화하려면, 서버 측에서 변경된 인스턴스 정보를 응답으로 되돌려줘야 합니다. 이 반환값을 담는 곳이 바로 RESULT 파라미터입니다.
비유하자면 Action은 은행 창구에서의 "송금 처리 요청서"이고, RESULT는 처리 후 창구 직원이 돌려주는 "거래 명세표"입니다. 명세표가 없으면 고객은 거래가 실제로 반영됐는지 확인할 방법이 없죠. Fiori Elements도 마찬가지로 RESULT가 비어 있으면 화면을 갱신할 근거가 없어 이전 상태를 그대로 표시하거나 리프레시 호출을 추가로 요구하게 됩니다.
내부적으로 RAP 프레임워크는 다음 흐름으로 동작합니다.
- 클라이언트가 OData Action 호출 → 프레임워크가 keys 파라미터로 대상 인스턴스 식별
- Behavior Pool의
FOR MODIFY메서드가 실행 → 비즈니스 로직 수행 - 핸들러는
result반환 테이블에%cid_ref(또는%tky)와%param을 채움 - 프레임워크가
%param에 담긴 엔티티 구조를 OData 응답 페이로드로 직렬화
여기서 %param은 BDEF에서 result [1] $self 또는 result [1..*] entity SalesOrder처럼 선언한 타입에 맞는 구조체/테이블입니다. 이 값이 비어 있으면 프레임워크는 빈 객체를 반환하고, Fiori Elements의 자동 리프레시(Auto-Bind) 기능이 무력화됩니다. 특히 draft-enabled 시나리오에서는 draft/active 인스턴스 매핑까지 꼬여서 예상치 못한 UI 버그로 이어집니다.
실전 예제 1단계: 기본 Action 정의와 최소 RESULT 반환
가장 단순한 형태로 SalesOrder의 승인 상태를 변경하는 Action을 정의해봅니다. BDEF에는 다음처럼 선언합니다.
managed implementation in class zbp_r_salesorder unique;
strict ( 2 );
define behavior for ZR_SalesOrder alias SalesOrder
persistent table zsalesorder
lock master
authorization master ( instance )
{
field ( readonly ) SalesOrderId, OverallStatus;
field ( mandatory ) CustomerId;
create;
update;
delete;
action ( features : instance ) approveOrder result [1] $self;
}
여기서 result [1] $self는 "이 Action은 반드시 하나의 결과를 반환하며, 그 타입은 자기 자신(SalesOrder 엔티티)과 동일하다"는 뜻입니다. Behavior Implementation Class에서는 다음처럼 처리합니다.
METHOD approveorder.
" 1) 대상 인스턴스 읽기
READ ENTITIES OF zr_salesorder IN LOCAL MODE
ENTITY SalesOrder
FIELDS ( SalesOrderId OverallStatus )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" 2) 상태 변경
MODIFY ENTITIES OF zr_salesorder IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( OverallStatus )
WITH VALUE #( FOR order IN lt_orders
( %tky = order-%tky
OverallStatus = 'A' ) )
REPORTED DATA(ls_reported).
" 3) 변경 후 다시 읽어 RESULT 채우기
READ ENTITIES OF zr_salesorder IN LOCAL MODE
ENTITY SalesOrder
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_result).
result = VALUE #( FOR order IN lt_result
( %tky = order-%tky
%param = order ) ).
ENDMETHOD.
핵심 포인트는 UPDATE 이후 다시 READ해서 최신 상태를 %param에 넣는다는 점입니다. UPDATE 직후 lt_orders를 그대로 반환하면 OverallStatus가 변경 전 값으로 남아 있어 화면에 이전 상태가 표시됩니다.
실전 예제 2단계: 검증, 로깅, 에러 핸들링
실무에서는 승인 조건 검증과 로그 남기기가 필수입니다. 조건 미달 시 failed와 reported에 정보를 채우고, 성공 케이스만 RESULT에 담아야 합니다.
METHOD approveorder.
DATA: lt_result_final TYPE TABLE FOR ACTION RESULT zr_salesorder\\approveOrder.
READ ENTITIES OF zr_salesorder IN LOCAL MODE
ENTITY SalesOrder
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
" 검증: 이미 승인된 건은 재승인 불가
IF ls_order-OverallStatus = 'A'.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-salesorder.
APPEND VALUE #( %tky = ls_order-%tky
%msg = new_message(
id = 'ZSO_MSG'
number = '010'
severity = if_abap_behv_message=>severity-error
v1 = ls_order-SalesOrderId )
) TO reported-salesorder.
CONTINUE.
ENDIF.
" 검증: 금액이 0원이면 승인 불가
IF ls_order-TotalAmount <= 0.
APPEND VALUE #( %tky = ls_order-%tky ) TO failed-salesorder.
APPEND VALUE #( %tky = ls_order-%tky
%msg = new_message(
id = 'ZSO_MSG'
number = '020'
severity = if_abap_behv_message=>severity-error )
) TO reported-salesorder.
CONTINUE.
ENDIF.
" 상태 변경
MODIFY ENTITIES OF zr_salesorder IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( OverallStatus ApprovedBy ApprovedAt )
WITH VALUE #( ( %tky = ls_order-%tky
OverallStatus = 'A'
ApprovedBy = cl_abap_context_info=>get_user_technical_name( )
ApprovedAt = cl_abap_context_info=>get_system_date( ) ) ).
ENDLOOP.
" 성공한 인스턴스만 다시 읽어 RESULT 구성
READ ENTITIES OF zr_salesorder IN LOCAL MODE
ENTITY SalesOrder
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_refreshed).
result = VALUE #( FOR o IN lt_refreshed
WHERE ( OverallStatus = 'A' )
( %tky = o-%tky
%param = o ) ).
ENDMETHOD.
failed에 담긴 인스턴스는 프레임워크가 자동으로 RESULT에서 제외하기도 하지만, 위처럼 명시적으로 필터링하면 의도가 분명해집니다. reported에 메시지를 넣으면 Fiori Elements가 메시지 팝업으로 자동 표시합니다.
실전 예제 3단계: 프로덕션 관점 — 벌크 처리, 트랜잭션 안정성, 테스트
프로덕션 코드에서는 대량 호출과 트랜잭션 격리를 고려해야 합니다. keys가 수천 건 들어올 수 있으므로 반복문 안에서 개별 MODIFY를 호출하는 것은 지양하고, 벌크 UPDATE 후 한 번에 READ하는 패턴을 권장합니다.
METHOD approveorder.
" 1) 벌크 검증
READ ENTITIES OF zr_salesorder IN LOCAL MODE
ENTITY SalesOrder
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
DATA(lt_valid_keys) = VALUE tt_salesorder_key( ).
LOOP AT lt_orders INTO DATA(ls_order)
WHERE OverallStatus <> 'A' AND TotalAmount > 0.
APPEND VALUE #( %tky = ls_order-%tky ) TO lt_valid_keys.
ENDLOOP.
" 2) 벌크 업데이트 (단일 MODIFY)
MODIFY ENTITIES OF zr_salesorder IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( OverallStatus ApprovedBy ApprovedAt )
WITH VALUE #( FOR k IN lt_valid_keys
( %tky = k-%tky
OverallStatus = 'A'
ApprovedBy = cl_abap_context_info=>get_user_technical_name( )
ApprovedAt = cl_abap_context_info=>get_system_date( ) ) )
FAILED failed
REPORTED reported.
" 3) 승인된 인스턴스만 다시 읽어 RESULT 매핑
READ ENTITIES OF zr_salesorder IN LOCAL MODE
ENTITY SalesOrder
ALL FIELDS
WITH CORRESPONDING #( lt_valid_keys )
RESULT DATA(lt_refreshed).
result = VALUE #( FOR o IN lt_refreshed
( %tky = o-%tky
%param = o ) ).
" 4) 감사 로그 (Application Log)
IF lt_valid_keys IS NOT INITIAL.
zcl_salesorder_audit=>log_approval(
it_keys = lt_valid_keys
iv_user = cl_abap_context_info=>get_user_technical_name( ) ).
ENDIF.
ENDMETHOD.
테스트는 ABAP Unit + CDS Test Double Framework로 작성하는 것이 일반적으로 권장됩니다. Behavior Handler의 로직을 단위 테스트하려면 cl_cds_test_environment로 CDS 목업을 만들고, EML(Entity Manipulation Language)로 Action을 직접 호출한 뒤 result 테이블이 기대한 값으로 채워졌는지 assert하는 방식을 씁니다. 보안 측면에서는 authorization master를 통해 인스턴스 레벨 권한 검증이 자동 수행되도록 하고, ApprovedBy 필드는 클라이언트 입력을 받지 않고 서버에서만 세팅하도록 readonly 처리하는 것이 안전합니다.
실무에서 자주 만나는 실수와 해결법
Q1. Action을 호출하면 200 OK가 오는데 응답 body가 {}로 비어 있습니다.
가장 흔한 원인은 handler에서 result 테이블에 %param을 채우지 않은 경우입니다. result = VALUE #( ... %param = ... ) 라인이 실제로 실행되는지 디버거로 확인하세요. 또한 BDEF의 projection layer(use action approveOrder result;)에 result가 노출되어 있는지도 함께 점검해야 합니다. 프로젝션에서 result를 노출하지 않으면 서비스 응답에 포함되지 않습니다.
Q2. Fiori Elements 리스트 화면이 Action 실행 후 자동 갱신되지 않습니다.
result에 반환한 %tky와 화면에서 사용하는 키가 일치해야 프론트엔드의 auto-binding이 작동합니다. draft-enabled 시나리오라면 draft 인스턴스의 키를 반환해야 하는지, active 인스턴스의 키를 반환해야 하는지 %is_draft 값을 명확히 세팅했는지 확인하세요. %param의 필드 중 ETag 필드가 최신값으로 채워지지 않으면 프레임워크가 stale로 판단해 UI를 갱신하지 않을 수 있습니다.
Q3. BDEF에 result 타입을 잘못 선언해서 컴파일 에러가 납니다.
result [1] $self는 단일 인스턴스, result [0..1] $self는 옵션, result [1..*] entity 다른엔티티는 다른 엔티티 컬렉션 반환입니다. handler의 result 테이블 타입이 TABLE FOR ACTION RESULT로 정확히 선언되어야 하며, 잘못된 카디널리티는 Cardinality mismatch 에러로 이어집니다. 특히 result를 리스트 형태로 반환하려면 [1..*]로 선언하고 여러 row를 append하는 패턴을 사용해야 합니다.
이어서 살펴보면 좋은 주제
RESULT 반환의 기본기를 익혔다면 다음 주제로 확장해볼 수 있습니다. 첫째, Factory Action을 이용한 새 인스턴스 생성과 result 반환. 둘째, Static Action(인스턴스 없이 실행되는 Action)에서의 result 활용. 셋째, Determinations와 Validations를 조합해 Action 실행 전후 자동 로직을 배치하는 패턴. 넷째, RAP Managed with Unmanaged Save 시나리오에서 Action이 외부 API를 호출한 후 result를 어떻게 구성할지에 대한 설계. 마지막으로 Fiori Elements의 Side Effects 어노테이션과 result 반환을 연동해 관련 필드/엔티티까지 자동 리프레시하는 방법을 함께 학습하면 실무 완성도가 크게 올라갑니다.
더 깊이 파고들 수 있는 자료
- SAP Help Portal — ABAP RAP: Actions
- SAP Help Portal — Implementing Actions in the Behavior Pool
- SAP Help Portal — Behavior Definitions Reference
- SAP Developers — Add Actions to a RAP Business Object
- SAP Community Blogs — RAP 태그 모음
- SAP Help Portal — Entity Manipulation Language (EML)
- SAP Help Portal — CDS Test Double Framework
댓글 0
아직 댓글이 없습니다.