이 글의 개요와 도달 지점
ABAP RESTful Application Programming Model(RAP)에서 Bound Action은 특정 비즈니스 객체 인스턴스에 묶여 동작하는 동작 단위입니다. 단순한 토글 버튼이라면 파라미터 없이도 충분하지만, 실무에서는 "승인하되 코드와 코멘트를 함께 남겨라" 같은 요구가 끊임없이 들어옵니다. 이 글은 SalesOrder를 다루는 가상의 유통 시나리오를 통해 파라미터를 받는 Bound Action을 BDEF에 선언하고, behavior implementation 클래스에서 수신하며, EML로 호출하는 전 과정을 단계별로 다룹니다.
아래 체크리스트를 모두 직접 구현할 수 있다면 이 글의 목표를 달성한 것입니다.
- Bound Action과 Unbound Action의 차이를 BDEF 시그니처 수준에서 설명할 수 있다
- abstract entity로 입력 파라미터 구조를 정의하고 BDEF에 연결할 수 있다
- behavior pool 메서드에서
keys와keys-%param을 분리해 처리할 수 있다 EML EXECUTE ACTION으로 파라미터를 채워 호출할 수 있다- 액션 결과(result)를 엔터티 인스턴스로 반환하고 RAP 트랜잭션 라이프사이클과 정합성을 맞출 수 있다
먼저 알고 있어야 하는 것
이 글은 RAP의 기본 빌딩 블록(Root entity, Behavior Definition, Behavior Implementation, Service Definition, Service Binding)을 이미 만들어 본 독자를 가정합니다. CDS view, managed/unmanaged 시나리오의 차이, FOR BEHAVIOR OF 클래스 구조, ABAP 7.55 이상에서의 EML 문법(MODIFY ENTITIES, READ ENTITIES)에 익숙해야 합니다. S/4HANA Cloud Public/Private Edition 또는 ABAP Cloud 개발 모델 위에서 진행한다고 가정합니다.
환경, 버전, 사전 준비물
이 글은 다음 환경을 전제로 작성되었습니다. 버전이 다르면 일부 구문이 거부될 수 있으므로 사용 중인 릴리스 노트를 함께 확인하시기 바랍니다.
- ABAP Platform: ABAP Platform 2023 또는 S/4HANA 2023 (FPS02 이상 권장)
- ABAP Development Tools(ADT): Eclipse 2024-03 + 최신 ABAP Tools
- RAP 시나리오: managed with internal numbering, 트랜잭션 동작 활성화
- 패키지:
ZRAP_SO_DEMO(소프트웨어 컴포넌트는 HOME 또는 사용자 정의) - 권한:
S_DEVELOP, RAP 비즈니스 객체 생성 권한, OData v4 미리보기 권한
사전 준비로 SalesOrder, SalesOrderItem 두 개의 root/child CDS 모델과 동명의 BDEF가 이미 존재한다고 가정합니다. 실제 코드에서는 명명 규칙을 사내 기준에 맞춰 조정해야 합니다.
핵심 개념 정리
Bound Action을 한 문장으로 설명하면 "특정 인스턴스에 매여 실행되는 비즈니스 동작"입니다. 비유하자면 회의실 예약 시스템에서 "이 회의실"의 컨텍스트가 항상 따라다니는 버튼과 같습니다. 반대로 Unbound Action은 "신규 회의실 자동 배정"처럼 어떤 단일 인스턴스에도 묶이지 않은 전역 동작입니다.
RAP에서 Bound Action의 시그니처는 다음 네 가지 요소로 구성됩니다.
- 키(key): 어떤 인스턴스가 대상인지를 식별
- 입력 파라미터(parameter): 추상 엔터티 또는 단순 타입 컬렉션
- 결과(result): 한 건 또는 여러 건의 엔터티 인스턴스, 혹은 단순 타입
- 옵션(static, factory, instance authorization 등)
파라미터를 전달할 때 권장되는 방식은 abstract entity를 정의해 구조를 명시적으로 묶는 것입니다. 단순 타입을 직접 나열하는 방식도 가능하지만, OData 메타데이터에 안정적으로 노출되고 추후 필드 확장에도 강한 구조 기반 접근이 일반적으로 선호됩니다.
BDEF에서 파라미터를 받는 Bound Action을 선언하면 behavior implementation에서는 IMPORTING keys의 각 행에서 keys-%param으로 입력값에 접근합니다. %param은 RAP 런타임이 자동 생성하는 구조 컴포넌트로, BDEF에 명시한 abstract entity와 동일한 형태를 갖습니다. 결과(result)는 RESULT 인자에 채워 호출자에게 돌려주며, 보통 변경된 엔터티의 최신 스냅샷을 담아 UI가 즉시 갱신될 수 있게 합니다.
도식으로 보면:
키(SalesOrderUUID) + %param(ApprovalCode, Comment, ApprovedBy) → 액션 로직 → result(SalesOrder 최신 인스턴스)형태의 단방향 데이터 흐름이 만들어집니다.
1단계 — 가장 단순한 형태로 시작하기
먼저 파라미터의 그릇이 될 abstract entity를 만듭니다. abstract entity는 데이터베이스 테이블이 없는 "타입 정의용" CDS 뷰로, RAP에서는 액션 파라미터/결과의 형태를 선언적으로 표현하는 데 자주 쓰입니다.
@EndUserText.label: 'Approval input for SalesOrder'
define abstract entity ZA_SO_ApprovalInput
{
ApprovalCode : abap.char(10);
Comment : abap.string(255);
ApprovedBy : abap.char(12);
}
그다음 SalesOrder의 BDEF에 Bound Action을 선언합니다. parameter 키워드로 위에서 만든 abstract entity를 연결하고, result로 SalesOrder 자기 자신을 반환하도록 합니다.
managed implementation in class ZBP_R_SalesOrder unique;
strict ( 2 );
define behavior for ZR_SalesOrder alias SalesOrder
persistent table zso_header
lock master
authorization master ( instance )
{
field ( readonly ) SalesOrderUUID, OverallStatus;
action ( features : instance ) ApproveSalesOrder
parameter ZA_SO_ApprovalInput
result [1] $self;
}
이제 behavior pool 클래스에 메서드 구현 스텁을 추가합니다. ADT의 Quick Fix를 사용하면 자동 생성되지만, 시그니처를 직접 이해해 두는 편이 디버깅에 유리합니다.
METHODS ApproveSalesOrder FOR MODIFY
IMPORTING keys FOR ACTION SalesOrder~ApproveSalesOrder
RESULT result.
2단계 — 실무 로직, 에러 처리, 로깅 추가하기
가장 흔한 실무 요구는 "잘못된 승인 코드를 입력하면 메시지로 거부하고, 정상이면 상태를 EM/AP 같은 코드로 변경하라"입니다. 다음 구현은 %param을 검사하고, RAP 메시지 인프라를 활용해 사용자 친화적인 오류를 돌려주며, 변경된 결과를 다시 읽어 반환합니다.
METHOD ApproveSalesOrder.
DATA updates TYPE TABLE FOR UPDATE ZR_SalesOrder.
DATA failures TYPE TABLE FOR FAILED ZR_SalesOrder.
DATA messages TYPE TABLE FOR REPORTED ZR_SalesOrder.
LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>).
DATA(input) = <key>-%param.
IF input-ApprovalCode IS INITIAL OR strlen( input-ApprovalCode ) < 3.
APPEND VALUE #(
%tky = <key>-%tky
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |Approval code '{ input-ApprovalCode }' is too short| )
%element-OverallStatus = if_abap_behv=>mk-on
) TO messages-salesorder.
CONTINUE.
ENDIF.
APPEND VALUE #(
%tky = <key>-%tky
OverallStatus = 'AP'
ApprovedByUser = input-ApprovedBy
ApprovalCode = input-ApprovalCode
ApprovalComment = input-Comment
LastChangedAt = cl_abap_context_info=>get_system_date( )
) TO updates.
ENDLOOP.
MODIFY ENTITIES OF ZR_SalesOrder IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( OverallStatus ApprovedByUser
ApprovalCode ApprovalComment LastChangedAt )
WITH updates
FAILED failures
REPORTED messages.
READ ENTITIES OF ZR_SalesOrder IN LOCAL MODE
ENTITY SalesOrder
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(read_result).
result = VALUE #( FOR row IN read_result
( %tky = row-%tky
%param = row ) ).
reported = CORRESPONDING #( DEEP messages ).
failed = CORRESPONDING #( DEEP failures ).
ENDMETHOD.
몇 가지 포인트가 있습니다. 첫째, IN LOCAL MODE는 권한 검사를 우회하므로 액션이 정책상 허용된 상태 변경만 다룬다고 확신할 때 사용합니다. 둘째, %tky는 트랜잭셔널 키로, RAP 런타임이 임시 생성/변경 인스턴스를 식별하는 데 사용하는 안전한 키 표현입니다. 셋째, result 구조에 %param을 채우는 패턴은 result 타입을 abstract entity로 잡았을 때 적용되며, $self로 잡았다면 SalesOrder 필드들을 그대로 채워야 합니다.
3단계 — 프로덕션을 위한 보강 (성능, 보안, 테스트)
실제 운영에서는 단건 호출보다 다건 호출, 권한 검증, 단위 테스트가 필수입니다. 다음은 EML로 여러 SalesOrder를 한 번에 승인하는 호출 측 코드입니다.
DATA targets TYPE TABLE FOR ACTION IMPORT ZR_SalesOrder\ApproveSalesOrder.
targets = VALUE #(
( %key-SalesOrderUUID = uuid_1
%param = VALUE #( ApprovalCode = 'AP-PRIME'
Comment = 'Q2 priority batch'
ApprovedBy = sy-uname ) )
( %key-SalesOrderUUID = uuid_2
%param = VALUE #( ApprovalCode = 'AP-STD'
Comment = 'Standard flow'
ApprovedBy = sy-uname ) )
).
MODIFY ENTITIES OF ZR_SalesOrder
ENTITY SalesOrder
EXECUTE ApproveSalesOrder FROM targets
RESULT DATA(action_result)
FAILED DATA(action_failed)
REPORTED DATA(action_reported).
IF action_failed IS NOT INITIAL.
ROLLBACK ENTITIES.
ELSE.
COMMIT ENTITIES RESPONSE OF ZR_SalesOrder
FAILED DATA(commit_failed)
REPORTED DATA(commit_reported).
ENDIF.
권한 측면에서는 BDEF의 authorization master ( instance )와 함께 instance authorization 메서드를 구현해 "특정 사용자가 특정 SalesOrder에 대해 ApproveSalesOrder를 호출할 수 있는가"를 결정합니다. 권한 코드를 액션 본문에 박지 말고 instance authorization으로 분리하는 것이 일반적으로 권장됩니다.
테스트는 RAP 비즈니스 객체 테스트 더블 프레임워크(CL_CDS_TEST_ENVIRONMENT + 액션 호출용 EML)를 사용합니다. 핵심은 모킹된 인스턴스에 대해 EXECUTE ACTION을 호출하고 result/reported/failed의 내용을 단언하는 것입니다.
METHOD approve_with_invalid_code.
DATA(targets) = VALUE TABLE_FOR_ACTION_IMPORT(
( %key-SalesOrderUUID = test_uuid
%param = VALUE #( ApprovalCode = 'AB'
Comment = 'too short'
ApprovedBy = 'TESTER' ) ) ).
MODIFY ENTITIES OF ZR_SalesOrder
ENTITY SalesOrder
EXECUTE ApproveSalesOrder FROM targets
REPORTED DATA(reported).
cl_abap_unit_assert=>assert_not_initial( reported-salesorder ).
ENDMETHOD.
성능 측면에서는 LOOP 내부에서 단건 MODIFY ENTITIES를 호출하지 말고, 위 예시처럼 변경 테이블을 누적한 뒤 한 번에 호출하는 패턴을 따르는 편이 안전합니다. 대량 호출 시에는 result 반환을 최소화해 페이로드를 줄이는 것도 고려할 수 있습니다.
자주 마주치는 실수와 트러블슈팅
Q1. "Parameter %param not known" 컴파일 에러가 납니다. 무엇이 문제일까요?
A1. BDEF에 parameter 절을 추가했지만 behavior implementation 클래스가 활성화되지 않은 경우가 가장 흔합니다. BDEF를 먼저 activate한 뒤 behavior pool 클래스를 재생성/활성화하면 %param 컴포넌트가 인식됩니다. ADT에서 "Synchronize" 메뉴를 사용하는 것도 도움이 됩니다.
Q2. OData v4 미리보기에서 액션을 호출할 때 파라미터가 비어 있다고 나옵니다.
A2. 요청 바디의 JSON 키가 abstract entity 필드명과 정확히 일치하는지 확인하세요. 또 service binding을 재생성하지 않으면 metadata에 새 파라미터가 반영되지 않습니다. 변경 후 반드시 binding을 재게시(publish)해야 합니다.
Q3. result가 비어 있어 UI가 갱신되지 않습니다.
A3. BDEF에 result [1] $self 또는 result [1] ZA_...이 선언되어 있어도 구현에서 RESULT를 채우지 않으면 비어 옵니다. 변경 직후 READ ENTITIES ... IN LOCAL MODE로 최신 상태를 다시 읽어 채우는 패턴을 일반적으로 권장합니다. $self 결과 타입을 사용한다면 result 행에 %param 대신 SalesOrder 필드를 직접 할당해야 한다는 점도 자주 헷갈리는 부분입니다.
추가로, managed 시나리오에서 액션 내부에서 직접 DB 테이블에 UPDATE를 날리는 코드는 RAP 트랜잭션 버퍼와 충돌해 데이터가 덮어써질 수 있습니다. 항상 EML을 거치는 습관을 들이는 편이 안전합니다.
또 하나, static action과 instance action을 혼동하지 마세요. static 키워드가 붙으면 인스턴스 키가 빠지고 %param만 전달됩니다. "여러 건을 한 번에 처리"는 static action으로 흐름이 달라지므로 요구사항을 다시 분석할 필요가 있습니다.
여기서 더 깊이 들어가고 싶다면
이 글에서 다룬 패턴을 확장할 수 있는 자연스러운 다음 주제는 다음과 같습니다. 첫째, 액션 결과를 abstract entity로 잡아 SalesOrder 필드뿐 아니라 "승인 영수증 번호" 같은 부가 정보를 함께 반환하는 패턴입니다. 둘째, draft 활성/비활성 시나리오에서 액션이 어떻게 동작하는지(특히 draft action vs. active action)와 관련된 BDEF 옵션 학습입니다. 셋째, 액션을 Fiori Elements 리스트 리포트의 빠른 액션으로 노출할 때 필요한 annotation(@UI.lineItem.dataAction, @UI.identification.dataAction) 구성입니다. 넷째, factory action을 통해 파라미터 기반으로 새로운 인스턴스를 생성하는 패턴도 함께 익혀 두면 좋습니다.
더 읽어볼 만한 자료들
댓글 0
아직 댓글이 없습니다.
💬 댓글 작성은 UI5 모드에서 사용할 수 있습니다.
UI5 모드에서 사용하기