📖 개요 — 이 글에서 다루는 문제
RAP(ABAP RESTful Application Programming Model)에서 precheck으로 수정 요청을 분명히 차단했는데, Fiori Elements 화면에는 사용자가 입력한 값이 그대로 남아 있는 현상을 다룹니다. 서버 로그를 보면 요청은 거부됐는데 화면은 바뀐 것처럼 보이니, 사용자도 개발자도 "막힌 게 맞나?"를 놓고 혼란에 빠지기 쉽습니다. 이 글은 그 원인을 RAP 트랜잭셔널 처리 단계에서 찾고, 실무에서 검증된 해결 패턴을 정리합니다.
- precheck과 validation이 각각 어느 단계에서 실행되는지 구분할 수 있다
- precheck 거부 후에도 화면에 값이 남는 이유를 버퍼/드래프트 관점에서 설명할 수 있다
- feature control, 타깃 메시지, side effects로 화면 상태와 서버 상태를 일치시킬 수 있다
📚 미리 알아두면 좋은 배경
Managed RAP 비즈니스 오브젝트의 기본 구조(Behavior Definition/Implementation), EML(Entity Manipulation Language)의 MODIFY/READ ENTITIES 문법, 드래프트(draft) 개념을 이미 접해본 중급 수준의 독자를 가정합니다. Fiori Elements가 OData 요청을 통해 백엔드와 통신한다는 큰 그림만 알고 있어도 따라올 수 있습니다.
🔧 실습 환경과 준비물
이 글의 예제는 SAP BTP ABAP Environment(ABAP Cloud) 및 SAP S/4HANA 2023(ABAP Platform 2023) 이상, strict(2) 모드 기준으로 작성했습니다. 개발 도구는 ADT(ABAP Development Tools for Eclipse)를 사용하며, 미리보기는 ADT의 Fiori Elements Preview로 충분합니다. 시나리오 도메인은 물류센터의 자재 반출증(Outbound Pass) 관리입니다. 승인 완료된 반출증은 수량(ReleaseQty)을 더 이상 고칠 수 없어야 한다는 요구사항을 precheck과 validation으로 각각 구현하면서 두 메커니즘의 차이를 관찰합니다.
💡 핵심 개념 — precheck과 validation은 "다른 시간대"에 산다
RAP 트랜잭션은 크게 두 시간대로 나뉩니다. 비유하자면 공항 보안검색대와 탑승구 최종 확인의 차이입니다.
- Interaction Phase(상호작용 단계): 화면이나 EML에서 들어온 create/update 요청이 트랜잭셔널 버퍼(드래프트 앱이라면 드래프트 테이블)에 쌓이는 단계
- Save Sequence(저장 시퀀스): FINALIZE → CHECK_BEFORE_SAVE(여기서 validation 실행) → SAVE로 이어지는 확정 단계
precheck은 보안검색대입니다. 수정 요청이 버퍼에 도달하기 전에 실행되어, 거부된 인스턴스를 요청 자체에서 걸러냅니다. 즉 거부된 값은 버퍼에 아예 기록되지 않습니다. 반면 validation은 탑승구 확인입니다. 값은 이미 버퍼(드래프트)에 들어간 상태이고, 저장 시퀀스에서 활성화(activation)만 막습니다.
여기서 화면 현상이 갈립니다. validation이 실패하면 잘못된 값이 드래프트에 남아 있으므로 화면에 값이 보이는 것이 정합적입니다 — 에러 메시지와 함께 "고쳐서 다시 저장하라"는 UX가 됩니다. 그런데 precheck이 거부하면 서버 버퍼에는 이전 값, 화면 컨트롤에는 사용자가 방금 타이핑한 값이 남습니다. Fiori Elements는 PATCH 요청이 거부되어도 입력 필드의 클라이언트 측 값을 자동으로 되돌리지 않기 때문입니다. 데이터를 다시 읽어오는 이벤트(재조회, 페이지 재진입, side effect)가 발생해야 비로소 이전 값으로 돌아갑니다. 이것이 "precheck으로 막았는데 화면에 값이 남는" 현상의 정체입니다. 서버는 정확히 막았지만, 화면과 버퍼가 일시적으로 어긋난 상태인 것입니다.
💻 실전 코드 3단계
1단계 — 기본: precheck과 validation을 나란히 선언
Behavior Definition에서 update에 precheck을 걸고, 수량 검증은 validation으로 선언합니다.
managed implementation in class zbp_r_outboundpass unique;
strict ( 2 );
with draft;
define behavior for ZR_OutboundPass alias OutboundPass
persistent table zoutpass
draft table zoutpass_d
lock master total etag LastChangedAt
authorization master ( global )
{
create;
update ( precheck );
delete;
field ( readonly ) PassId, PassStatus;
field ( features : instance ) ReleaseQty;
validation checkReleaseQty on save { create; update; field ReleaseQty; }
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare { validation checkReleaseQty; }
}
2단계 — 실무: 구현부에서 필드 타깃 에러 메시지까지 남기기
precheck에서 거부만 하고 reported를 비워두면 사용자는 아무 피드백 없이 값만 남은 화면을 보게 됩니다. 반드시 %element로 필드를 지목한 메시지를 함께 반환해야 UI가 해당 입력란에 에러 상태를 표시합니다.
METHOD precheck_update.
" 버퍼에 반영되기 전이므로, 현재 상태는 반드시 READ로 조회한다
READ ENTITIES OF ZR_OutboundPass IN LOCAL MODE
ENTITY OutboundPass
FIELDS ( PassStatus ) WITH CORRESPONDING #( entities )
RESULT DATA(current_states).
LOOP AT entities INTO DATA(entity).
DATA(current) = current_states[ KEY id %tky = entity-%tky ].
IF current-PassStatus = 'A' " 승인 완료
AND entity-%control-ReleaseQty = if_abap_behv=>mk-on.
APPEND VALUE #( %tky = entity-%tky ) TO failed-outboundpass.
APPEND VALUE #(
%tky = entity-%tky
%element-releaseqty = if_abap_behv=>mk-on
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = '승인 완료된 반출증은 수량 변경이 불가합니다.' )
) TO reported-outboundpass.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD checkreleaseqty. " FOR VALIDATE ON SAVE
READ ENTITIES OF ZR_OutboundPass IN LOCAL MODE
ENTITY OutboundPass
FIELDS ( ReleaseQty ) WITH CORRESPONDING #( keys )
RESULT DATA(passes).
LOOP AT passes INTO DATA(pass).
" state area로 이전 에러를 먼저 무효화
APPEND VALUE #( %tky = pass-%tky
%state_area = 'CHECK_QTY' ) TO reported-outboundpass.
IF pass-ReleaseQty <= 0.
APPEND VALUE #( %tky = pass-%tky ) TO failed-outboundpass.
APPEND VALUE #( %tky = pass-%tky
%state_area = 'CHECK_QTY'
%element-releaseqty = if_abap_behv=>mk-on
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = '반출 수량은 0보다 커야 합니다.' )
) TO reported-outboundpass.
ENDIF.
ENDLOOP.
ENDMETHOD.
3단계 — 프로덕션: feature control로 입력 자체를 막아 화면-서버 불일치를 원천 차단
가장 안정적인 패턴은 "UX는 feature control, 방어는 precheck"의 이중 구조입니다. 승인 완료 상태라면 필드를 읽기 전용으로 내려서 사용자가 타이핑할 수 없게 하고, precheck은 화면을 우회하는 EML/API 호출을 막는 최후 방어선으로 유지합니다.
METHOD get_instance_features.
READ ENTITIES OF ZR_OutboundPass IN LOCAL MODE
ENTITY OutboundPass
FIELDS ( PassStatus ) WITH CORRESPONDING #( keys )
RESULT DATA(passes).
result = VALUE #(
FOR pass IN passes
( %tky = pass-%tky
%field-ReleaseQty = COND #(
WHEN pass-PassStatus = 'A'
THEN if_abap_behv=>fc-f-read_only
ELSE if_abap_behv=>fc-f-unrestricted ) ) ).
ENDMETHOD.
여기에 상태를 바꾸는 액션 뒤 화면 갱신이 필요하다면 Behavior Definition에 side effects를 추가해 상태 필드 변경이 다른 필드와 메시지를 재조회하도록 선언합니다.
side effects
{
field PassStatus affects field ReleaseQty, messages;
}
테스트는 EML로 직접 검증할 수 있습니다. precheck이 동작하면 아래 update는 failed에 담기고 버퍼는 변하지 않습니다.
MODIFY ENTITIES OF ZR_OutboundPass
ENTITY OutboundPass
UPDATE FIELDS ( ReleaseQty )
WITH VALUE #( ( PassId = '4711' ReleaseQty = 999 ) )
FAILED DATA(failed)
REPORTED DATA(reported).
" failed-outboundpass가 채워져 있으면 precheck 차단 성공
⚠️ 흔한 실수와 트러블슈팅
Q1. precheck에서 failed만 채우고 reported를 비웠더니 화면에 아무 반응이 없어요.
차단은 됐지만 메시지가 없으면 UI는 침묵합니다. 반드시 %msg와 함께 %element로 대상 필드를 지목하세요. 필드에 빨간 테두리와 메시지가 표시되어 사용자가 "입력값이 반영되지 않았다"를 인지할 수 있습니다.
Q2. precheck 안에서 entity의 필드값으로 현재 상태를 판단했는데 오동작합니다.
precheck의 entities에는 들어오는 변경 요청만 담겨 있습니다. 현재 저장된 상태(예: PassStatus)는 반드시 READ ENTITIES ... IN LOCAL MODE로 조회해야 합니다. 또한 %control을 확인하지 않으면 해당 필드를 건드리지 않은 요청까지 차단하는 부작용이 생깁니다.
Q3. 값을 막았는데 화면을 새로 열면 이전 값으로 돌아와 있어요. 버그인가요?
버그가 아니라 precheck의 정상 동작입니다. 거부된 값은 드래프트/버퍼에 기록되지 않았으므로, 재조회 시 이전 값이 보이는 것이 서버 기준의 진실입니다. 사용자 혼란이 문제라면 3단계의 feature control로 입력 자체를 막는 것이 일반적으로 권장되는 방향입니다.
Q4. 그럼 validation은 언제 쓰나요?
사용자가 값을 고쳐서 다시 제출할 수 있는 데이터 품질 검증(수량 범위, 필수값 등)은 validation이 적합합니다. 값이 드래프트에 남아 있어야 수정이 자연스럽기 때문입니다. 반대로 "이 상태에서는 아예 바꾸면 안 된다"는 상태 기반 차단은 feature control + precheck 조합이 어울립니다.
🚀 더 깊이 파고들 주제
이번 예제를 확장하려면 (1) get_global_features와 인스턴스 authorization을 결합한 다층 권한 제어, (2) 드래프트 Prepare 액션에서 validation을 묶어 활성화 전 일괄 검증하는 패턴, (3) side effects의 determine action 트리거를 활용한 계산 필드 즉시 갱신, (4) EML 기반 ABAP Unit 테스트(CDS Test Double, Botd)로 precheck/validation을 자동 검증하는 구성을 이어서 살펴보길 권합니다. 특히 unmanaged 시나리오에서는 버퍼 관리를 직접 구현하므로 이 글의 시점 차이가 더 극적으로 드러납니다.
📚 함께 보면 좋은 문서
댓글 0
아직 댓글이 없습니다.