개요 및 이 글에서 다루는 것
ABAP RAP(RESTful Application Programming Model)에서 비즈니스 로직을 구현할 때 가장 자주 마주치는 함정 중 하나가 바로 FAILED와 REPORTED 테이블의 오용입니다. 두 구조는 이름이 비슷하지만 역할이 완전히 다르며, 잘못 조합하면 사용자에게는 에러 메시지가 표시되는 것 같지만 실제 트랜잭션은 성공 처리되는 미묘한 버그가 발생합니다. 이 글에서는 두 구조의 동작 원리, 프레임워크가 이를 해석하는 방식, 그리고 실전에서 자주 발생하는 오류 패턴을 다룹니다.
- FAILED 테이블의 구조와 인스턴스 키 전달 방식 이해
- REPORTED 테이블의 메시지 전달 메커니즘 파악
- 둘을 함께 써야 하는 이유와 단독 사용 시 부작용 분석
- SalesOrder 승인 시나리오로 조건별 메시지 패턴 구현
- cl_abap_behv_aux 및 NEW_MESSAGE 헬퍼 활용
- ABAP Unit으로 FAILED/REPORTED 검증하는 테스트 패턴
읽기 전 알아두면 좋은 것
이 글은 RAP의 Managed/Unmanaged Behavior Implementation 클래스를 작성해 본 경험이 있는 ABAP 개발자를 대상으로 합니다. CDS Behavior Definition(BDEF)에서 action, validation, determination을 선언해 본 경험, FOR MODIFY/FOR READ 메서드 시그니처를 본 경험이 필요합니다. 또한 ABAP Cloud 또는 S/4HANA 2022 이상의 ADT(ABAP Development Tools) 환경에서 RAP BO를 개발해 본 적이 있다고 가정합니다.
환경 및 준비물
이 글의 예제는 다음 환경을 기준으로 검증되었습니다.
- SAP S/4HANA Cloud Public Edition 2402 또는 S/4HANA on-premise 2023 이상
- ABAP Platform 2023 (Cloud-Ready ABAP 언어 버전)
- ADT (Eclipse 기반 ABAP Development Tools) 최신 빌드
- RAP 권한:
S_DEVELOP, BTP ABAP 환경에서는 Developer 역할 - 예제 BO: SalesOrder를 모델로 한 ZRAP_SO_ROOT 루트 엔티티 (managed scenario)
코드 스타일은 Clean ABAP 가이드라인을 따르며, 모든 핸들러 메서드는 FOR MODIFY 패턴을 기반으로 합니다. 권장 빌드 환경은 SAP BTP ABAP Environment의 release 2402 이상이지만, on-premise S/4HANA 2022부터도 거의 동일한 구조로 동작합니다.
핵심 개념: FAILED와 REPORTED의 역할 분리
RAP의 핸들러 메서드(예: FOR MODIFY, FOR ACTION, FOR VALIDATE ON SAVE)는 일반적으로 세 가지 출력 매개변수를 가집니다.
RESULT— 액션 결과 등 핸들러가 돌려주는 데이터FAILED— 처리에 실패한 인스턴스의 키 목록REPORTED— 사용자에게 전달할 메시지 목록
비유하자면 FAILED는 "이 건은 부도 처리되었습니다"라고 은행이 거래원장에 기록하는 행위이고, REPORTED는 "고객님께 다음과 같은 사유를 안내드립니다"라는 안내문 발송에 해당합니다. 둘은 독립적이기 때문에, 안내문만 보내고 실제 부도 처리는 하지 않을 수도 있고(REPORTED만 채움), 반대로 부도 처리만 하고 안내문은 안 보낼 수도 있습니다(FAILED만 채움). 하지만 실무에서 "에러"라고 부르는 상황은 거의 항상 둘 다 채워야 합니다.
RAP 프레임워크는 트랜잭션 종료 시점에 FAILED 테이블을 검사합니다. FAILED에 항목이 있으면 해당 인스턴스의 변경 사항은 롤백되고, 트랜잭션 결과는 실패로 마킹됩니다. 만약 FAILED를 채우지 않고 REPORTED에 severity가 'E'(Error)인 메시지만 추가하면, 사용자 화면에는 빨간 메시지가 표시되지만 백엔드 트랜잭션은 정상 커밋됩니다. 이것이 RAP 개발자가 가장 자주 빠지는 함정입니다.
FAILED 구조는 일반적으로 %CID, %KEY, %FAIL 컴포넌트를 가지며, %FAIL-CAUSE에는 if_abap_behv=>cause-unspecific, cause-conflict, cause-not_found 등의 상수를 지정합니다. REPORTED는 %MSG 컴포넌트에 if_abap_behv_message 인스턴스를 담는 구조이며, new_message() 또는 new_message_with_text() 팩토리 메서드로 생성합니다.
실전 코드 3단계
1단계: 기본 패턴 — Approve 액션의 단순 검증
SalesOrder를 승인하는 액션에서 주문 금액이 0 이하이면 거부하는 가장 단순한 형태를 살펴봅니다. BDEF에는 다음과 같이 액션이 선언되어 있다고 가정합니다.
define behavior for ZRAP_SO_ROOT alias SalesOrder
{
action ( features : instance ) approveOrder result [1] $self;
field ( readonly ) OrderStatus;
...
}
핸들러 클래스의 구현은 다음과 같이 작성합니다.
METHOD approveOrder.
READ ENTITIES OF zrap_so_root IN LOCAL MODE
ENTITY SalesOrder
FIELDS ( OrderId TotalAmount OrderStatus )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
IF ls_order-TotalAmount <= 0.
" 1) FAILED 테이블에 실패 인스턴스 기록
APPEND VALUE #(
%cid = ls_order-%cid_ref
OrderId = ls_order-OrderId
%fail-cause = if_abap_behv=>cause-unspecific
) TO failed-salesorder.
" 2) REPORTED 테이블에 메시지 추가
APPEND VALUE #(
%cid = ls_order-%cid_ref
OrderId = ls_order-OrderId
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |주문 { ls_order-OrderId }의 금액이 유효하지 않아 승인할 수 없습니다.| )
) TO reported-salesorder.
ENDIF.
ENDLOOP.
ENDMETHOD.
핵심은 같은 인스턴스에 대해 FAILED와 REPORTED 양쪽에 항목을 추가한다는 점입니다. 만약 REPORTED만 추가했다면 사용자에게 빨간색 에러 토스트는 표시되지만, OrderStatus 변경이 그대로 커밋되어 데이터 무결성이 깨집니다.
2단계: 실무 시나리오 — 조건별 Error/Warning/Info 조합
실무에서는 단순 에러 외에도 경고(Warning)와 정보(Information) 메시지를 함께 사용합니다. 예를 들어 "재고는 부족하지만 백오더로 처리 가능"한 경우는 Warning이고, 트랜잭션은 진행되어야 합니다. 이런 시나리오에서는 REPORTED만 채우고 FAILED는 비워두는 것이 정답입니다.
METHOD approveOrder.
READ ENTITIES OF zrap_so_root IN LOCAL MODE
ENTITY SalesOrder
FIELDS ( OrderId TotalAmount CustomerId InventoryStatus )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
LOOP AT lt_orders INTO DATA(ls_order).
" 케이스 A: 치명적 에러 → FAILED + REPORTED(E)
IF ls_order-TotalAmount <= 0.
APPEND VALUE #(
%cid_ref = ls_order-%cid_ref
OrderId = ls_order-OrderId
%fail-cause = if_abap_behv=>cause-unspecific
) TO failed-salesorder.
APPEND VALUE #(
%cid_ref = ls_order-%cid_ref
OrderId = ls_order-OrderId
%msg = NEW zcx_so_message(
textid = zcx_so_message=>invalid_amount
severity = if_abap_behv_message=>severity-error
order_id = ls_order-OrderId )
) TO reported-salesorder.
CONTINUE.
ENDIF.
" 케이스 B: 경고 → REPORTED(W)만, 트랜잭션은 진행
IF ls_order-InventoryStatus = 'SHORT'.
APPEND VALUE #(
%cid_ref = ls_order-%cid_ref
OrderId = ls_order-OrderId
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-warning
text = |재고 부족 — 백오더로 처리됩니다.| )
) TO reported-salesorder.
ENDIF.
" 케이스 C: 정보 메시지 + 정상 처리
APPEND VALUE #(
%cid_ref = ls_order-%cid_ref
OrderId = ls_order-OrderId
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-information
text = |주문 { ls_order-OrderId } 승인 완료| )
) TO reported-salesorder.
" 정상 처리 — MODIFY로 상태 변경
MODIFY ENTITIES OF zrap_so_root IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( OrderStatus )
WITH VALUE #( ( %tky = ls_order-%tky
OrderStatus = 'A' ) )
REPORTED DATA(ls_update_reported).
INSERT LINES OF ls_update_reported-salesorder INTO TABLE reported-salesorder.
ENDLOOP.
ENDMETHOD.
여기서 주목할 점은 케이스 B에서 FAILED를 채우지 않았다는 것입니다. Warning은 사용자에게 주의를 환기하지만 비즈니스 프로세스를 막지는 않습니다. 또한 사용자 정의 예외 클래스 zcx_so_message를 message 클래스로 사용해 메시지 텍스트를 번역 가능한 메시지 클래스로 외부화한 점도 실무 패턴입니다.
3단계: 프로덕션 — 메시지 헬퍼 캡슐화와 ABAP Unit 검증
프로덕션 코드에서는 메시지 생성 로직을 별도 헬퍼 메서드로 캡슐화하고, 단위 테스트로 FAILED/REPORTED 채움 여부를 검증합니다.
CLASS lhc_salesorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS append_failure
IMPORTING is_order TYPE zrap_so_root
iv_text_id TYPE symsgno
iv_v1 TYPE clike OPTIONAL
CHANGING ct_failed TYPE TABLE
ct_reported TYPE TABLE.
ENDCLASS.
CLASS lhc_salesorder IMPLEMENTATION.
METHOD append_failure.
APPEND VALUE #(
%cid_ref = is_order-%cid_ref
OrderId = is_order-OrderId
%fail-cause = if_abap_behv=>cause-unspecific
) TO ct_failed.
APPEND VALUE #(
%cid_ref = is_order-%cid_ref
OrderId = is_order-OrderId
%msg = new_message(
id = 'ZRAP_SO'
number = iv_text_id
severity = if_abap_behv_message=>severity-error
v1 = iv_v1 )
) TO ct_reported.
ENDMETHOD.
ENDCLASS.
ABAP Unit 테스트로 핸들러를 검증할 때는 cl_abap_behv_test_aux 또는 EML(Entity Manipulation Language)을 사용합니다.
CLASS ltc_approve_order DEFINITION FOR TESTING
RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION.
METHODS:
reject_when_amount_zero FOR TESTING.
ENDCLASS.
CLASS ltc_approve_order IMPLEMENTATION.
METHOD reject_when_amount_zero.
MODIFY ENTITIES OF zrap_so_root
ENTITY SalesOrder
EXECUTE approveOrder
FROM VALUE #( ( %cid = 'C1' OrderId = '4500' ) )
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
cl_abap_unit_assert=>assert_not_initial(
act = ls_failed-salesorder
msg = 'FAILED 테이블이 비어 있으면 트랜잭션이 커밋되어 위험합니다' ).
cl_abap_unit_assert=>assert_equals(
exp = if_abap_behv_message=>severity-error
act = ls_reported-salesorder[ 1 ]-%msg->m_severity ).
ENDMETHOD.
ENDCLASS.
이 테스트 패턴의 핵심은 FAILED와 REPORTED를 따로 검증하는 것입니다. REPORTED에 에러 메시지가 있다고 안심하지 말고 FAILED가 실제로 채워졌는지 별도 assert로 확인해야 회귀 버그를 막을 수 있습니다.
흔한 실수와 트러블슈팅
FAQ 1. 에러 메시지는 분명히 표시되는데 데이터가 저장되어 있습니다. 가장 전형적인 증상으로, REPORTED에 severity-error 메시지만 추가하고 FAILED 테이블을 채우지 않은 경우입니다. 프레임워크는 REPORTED의 severity를 보고 트랜잭션을 롤백하지 않습니다. 반드시 FAILED-{entity_alias} 테이블에 해당 인스턴스를 APPEND하세요.
FAQ 2. %cid와 %cid_ref 중 무엇을 써야 하나요? %cid는 새로 생성되는 인스턴스(CREATE)에 사용하고, %cid_ref는 이미 존재하는 인스턴스(UPDATE/DELETE/Action)를 참조할 때 사용합니다. Action 핸들러에서는 거의 항상 %cid_ref입니다. 잘못 쓰면 메시지가 OData 응답에서 잘못된 인스턴스에 매핑되어 UI에 표시되지 않거나 다른 행에 붙는 현상이 발생합니다.
FAQ 3. Validation에서 메시지가 두 번 표시됩니다. Validation 핸들러와 Action 핸들러가 동시에 같은 조건을 검사하거나, MODIFY ENTITIES 내부 호출 결과의 reported를 외부 reported에 그대로 INSERT LINES OF로 합치면서 중복 추가하는 경우입니다. SORT ... BY %cid_ref %msg->m_severity 후 DELETE ADJACENT DUPLICATES로 중복 제거하거나, 책임 영역을 validation/action 중 한 곳으로 정리하세요.
FAQ 4. cause-unspecific 외에 어떤 cause를 써야 하나요? 일반적으로 데이터 무결성 충돌은 cause-conflict, 키가 존재하지 않는 경우는 cause-not_found, 권한 부족은 cause-unauthorized, 락 실패는 cause-locked를 권장합니다. UI 측에서 적절한 처리 분기를 할 수 있게 도와줍니다.
관련 주제와 더 깊이 파볼 만한 영역
FAILED/REPORTED 패턴에 익숙해졌다면 다음 주제로 학습을 확장하기를 권장합니다. RAP 예외 클래스(CX_ABAP_BEHV)를 활용한 message class 통합, Precheck 기능으로 OData $batch 요청에서 사전 검증 수행, Determine Action과 Side Effects를 통한 자동 재검증 트리거, Draft-enabled BO에서 메시지 영속성 처리, 그리고 ETag 충돌 시 cause-conflict 활용 패턴 등이 자연스러운 확장 주제입니다. 또한 OData v4 응답의 SAP_Message annotation이 어떻게 REPORTED 항목과 매핑되는지를 추적하면 UI Fiori Elements 측 디버깅에도 큰 도움이 됩니다.
외부 자료
댓글 0
아직 댓글이 없습니다.