RAP Validation 삽질 끝 — on SAVE·on MODIFY 이벤트 완벽 분리 실전 #shorts #SAP #RAP
on SAVE vs on MODIFY — 핵심 차이부터 잡기
RAP에서 Validation을 짤 때 가장 먼저 마주치는 갈림길이 바로 on SAVE와 on MODIFY입니다. 이름만 보면 "저장할 때 vs 수정할 때"라는 단순한 구분 같지만, 실제 트랜잭션 흐름에서 이 둘은 전혀 다른 시점에 호출되며 책임 범위도 다릅니다.
비유하자면 on MODIFY는 입국 심사대입니다. 사용자가 필드를 바꾸는 순간(MODIFY 오퍼레이션)마다 즉시 게이트를 통과시켜 잘못된 값이 BO 인스턴스에 들어오는 것을 빨리 알려줍니다. 반면 on SAVE는 출국장 최종 보안검색입니다. 모든 변경이 누적된 뒤, COMMIT 직전에 한 번만 실행되어 인스턴스 전체의 일관성을 본 단계에서 확정합니다.
학습 체크리스트:
- on SAVE/on MODIFY 트리거 시점을 트랜잭션 단위로 설명할 수 있다
- Behavior Definition에서 두 모드를 올바른 문법으로 선언한다
- REPORTED, FAILED 구조를 채워 메시지를 UI에 노출한다
- 두 이벤트를 혼용했을 때 발생하는 성능/UX 이슈를 구분한다
on SAVE 검증: 저장 시점에만 실행되어야 하는 규칙들
on SAVE는 RAP 트랜잭션의 Save Sequence 안에서 호출됩니다. ADJUST_NUMBERS → SAVE → CLEANUP 직전 단계에서 한 번만 실행되므로, 이 시점에는 인스턴스의 모든 필드가 최종 값으로 채워져 있다고 봐도 됩니다. 트랜잭션 도중 임시로 비어 있던 값들이 후속 Determination을 통해 채워졌을 수도 있기 때문입니다.
일반적으로 다음 같은 규칙은 on SAVE가 적합합니다.
- 필수 필드(Mandatory) 검증: 사용자가 타이핑 중에 잠깐 비어 있는 것은 자연스럽기 때문에 MODIFY마다 빨갛게 띄우면 UX가 나쁩니다.
- 외부 시스템 조회가 필요한 검증: RFC, OData, DB 조인 등 비용이 큰 호출은 SAVE 한 번에 모아서 처리합니다.
- 여러 자식(Child) 노드가 합쳐져야 가능한 합계 검증: 자식이 모두 채워진 뒤 부모 한도와 비교해야 하는 경우.
- create 트리거 한정 검증: 신규 생성 시점에만 한 번 보고 싶은 데이터 무결성 체크.
on SAVE는 권장적으로 "확정된 데이터에 대한 최종 게이트"라는 관점에서 설계하면 모델이 깔끔해집니다.
on MODIFY 검증: 필드 변경 즉시 반응이 필요한 경우
on MODIFY는 매 MODIFY 오퍼레이션(Create/Update)마다 호출됩니다. 즉, OData PATCH 한 번이 떨어질 때마다 프레임워크가 해당 키를 모아 핸들러에 넘겨줍니다. 그래서 빠른 피드백이 필요하거나, 잘못된 값이 BO 인스턴스에 머무는 것 자체가 위험한 경우에 적합합니다.
- 날짜 범위 검증: BeginDate > EndDate 같은 필드 간 정합성은 즉시 잡아야 후속 Determination이 엉뚱한 계산을 하지 않습니다.
- 형식/패턴 검증: 이메일, 전화번호, 코드 패턴.
- 참조 무결성: 입력한 CustomerID가 실제 마스터에 존재하는지.
- 필드 의존 enum 검증: Status 필드가 'X'일 때만 허용되는 값.
주의할 점은, on MODIFY에서 "필수 입력"을 강제하면 사용자가 다른 필드를 먼저 채우려고 잠깐 비워둔 상태에서도 에러가 뜬다는 것입니다. 이래서 mandatory류는 on SAVE 쪽으로 미루는 패턴이 자연스럽습니다.
Behavior Definition 선언 문법 비교
Behavior Definition(BDEF)에서는 트리거와 필드 의존성을 함께 적습니다. 트리거는 create, update, delete, field <name>의 조합이며, 두 모드의 의미가 살짝 다릅니다.
managed implementation in class zbp_r_traveltp unique;
strict ( 2 );
define behavior for ZR_TravelTP alias Travel
persistent table ztravel
lock master
authorization master ( instance )
{
// on SAVE: 저장 시점에 한 번만, create 트리거에서 AgencyID 확인
validation validateAgency on save { create; field AgencyID; }
// on MODIFY: BeginDate 또는 EndDate가 변경될 때마다
validation validateDates on modify { field BeginDate, EndDate; }
// on SAVE: 자식 합계가 부모 한도 이하인지 (자식 변경 시점에만 평가)
validation validateBookingFee on save { update; field TotalPrice; }
}
핵심 포인트는 트리거는 평가 조건일 뿐이라는 것입니다. on save에 create만 적었더라도, 해당 인스턴스가 트랜잭션 내에서 create로 분류되어 있으면 SAVE 시점에 평가됩니다. on modify에서는 나열된 field가 실제로 변경되었을 때만 호출되므로 불필요한 호출을 줄일 수 있습니다.
ABAP 구현 — validateXxx 메서드 시그니처와 REPORTED/FAILED
핸들러 클래스에서는 두 모드를 메서드 어노테이션으로 명확히 구분합니다. 시그니처가 다르므로 카피&페이스트 시 헷갈리기 쉽습니다.
CLASS lhc_travel DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS validate_agency FOR VALIDATE ON SAVE
IMPORTING keys FOR Travel~validateAgency.
METHODS validate_dates FOR VALIDATE ON MODIFY
IMPORTING keys FOR Travel~validateDates.
ENDCLASS.
구현부에서는 READ ENTITIES로 현재 값(트랜잭션 버퍼 기준)을 읽고, 실패한 키는 FAILED에, 사용자에게 보여줄 메시지는 REPORTED에 누적합니다. %state_area는 같은 검증 그룹의 이전 메시지를 자동으로 교체해주는 키 역할이라 항상 채워두는 것을 권장합니다.
METHOD validate_agency.
READ ENTITIES OF ZR_TravelTP
ENTITY Travel
FIELDS ( AgencyID ) WITH CORRESPONDING #( keys )
RESULT DATA(travels) FAILED failed.
LOOP AT travels INTO DATA(travel).
IF travel-AgencyID IS INITIAL.
APPEND VALUE #( %tky = travel-%tky ) TO failed-travel.
APPEND VALUE #( %tky = travel-%tky
%state_area = 'VALIDATE_AGENCY'
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = 'Agency ID는 필수 입력 항목입니다' )
%element-AgencyID = if_abap_behv=>mk-on )
TO reported-travel.
ENDIF.
ENDLOOP.
ENDMETHOD.
on MODIFY 메서드의 차이는 거의 없지만, 변경 즉시 호출되므로 READ에서 가져오는 필드를 최소화하는 것이 성능에 유리합니다. 또한 같은 인스턴스가 한 트랜잭션에서 여러 번 modify 될 수 있으므로 idempotent하게 작성해야 합니다.
METHOD validate_dates.
READ ENTITIES OF ZR_TravelTP
ENTITY Travel
FIELDS ( BeginDate EndDate ) WITH CORRESPONDING #( keys )
RESULT DATA(travels).
LOOP AT travels INTO DATA(t).
IF t-BeginDate IS NOT INITIAL AND
t-EndDate IS NOT INITIAL AND
t-BeginDate > t-EndDate.
APPEND VALUE #( %tky = t-%tky ) TO failed-travel.
APPEND VALUE #( %tky = t-%tky
%state_area = 'VALIDATE_DATES'
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = '시작일은 종료일보다 늦을 수 없습니다' )
%element-BeginDate = if_abap_behv=>mk-on
%element-EndDate = if_abap_behv=>mk-on )
TO reported-travel.
ENDIF.
ENDLOOP.
ENDMETHOD.
흔한 실수 3가지 — 이 차이 모르면 버그 납니다
실수 1. 모든 검증을 on MODIFY에 몰아넣기. "빠르게 보여주면 좋잖아?"라는 생각으로 mandatory 검증까지 on MODIFY에 두면, 사용자가 신규 행을 만든 직후 모든 빈 필드에 빨간 메시지가 폭발합니다. UX도 나쁘고, 후속 Determination이 채워줄 값까지 잘못 잡아냅니다. 일반적으로 mandatory와 외부 조회는 on SAVE로 옮기는 편이 좋습니다.
실수 2. on SAVE에서 사용자 즉시 피드백을 기대. on SAVE는 Save Sequence에서만 호출되므로, 사용자가 저장 버튼을 누르기 전에는 메시지가 뜨지 않습니다. 날짜 정합성 같은 즉시 피드백이 필요한 검증을 on SAVE에 두면 "저장하니까 그제서야 에러가 뜬다"는 컴플레인이 옵니다.
실수 3. FAILED 누락. REPORTED에 메시지만 넣고 FAILED에 키를 추가하지 않으면, 메시지는 떠도 인스턴스가 정상 저장되어 버립니다. 반대로 FAILED만 채우고 REPORTED를 비우면 사용자는 "왜 저장이 안 되는지" 알 수 없습니다. 두 테이블은 항상 짝으로 채우는 것을 권장합니다.
FAQ:
- Q. on MODIFY 검증이 호출되지 않습니다. A. BDEF의
field목록에 실제 변경된 필드가 없거나, draft 시나리오에서 draft 측 핸들러가 따로 필요한지 확인하세요. - Q. 메시지가 두 번 보입니다. A. 같은
%state_area값을 사용하지 않으면 이전 메시지가 교체되지 않고 누적됩니다. - Q. additional save에서 검증해도 되나요? A. 비추천입니다. additional save는 저장 트랜잭션이 거의 끝난 시점이라 사용자에게 되돌릴 수 없는 경험을 주기 쉽습니다.
복합 시나리오: on SAVE + on MODIFY 함께 쓰는 패턴
실무에서는 같은 BO에 두 모드의 검증을 섞어 쓰는 것이 일반적입니다. 권장 분리 기준은 다음과 같습니다.
- 즉시 피드백이 필요한 정합성 → on MODIFY (필드 단위, 가벼운 로직)
- 인스턴스 전체 일관성, 외부 조회 → on SAVE (인스턴스 단위, 무거운 로직)
- 같은 의미의 검증을 양쪽에 중복 작성하지 않기 — 의도가 흐려지고 메시지가 두 번 뜹니다.
예를 들어 Travel BO에서 BeginDate/EndDate 정합성은 on MODIFY로, AgencyID 존재 여부와 자식 Booking 합계 검증은 on SAVE로 분리하면 사용자에게는 빠른 피드백을, 시스템에는 한 번의 무거운 검증을 줄 수 있습니다. 또한 RAP의 strict(2) 모드를 쓰면 메서드 시그니처가 더 엄격해지므로, 새 프로젝트는 처음부터 strict(2)로 시작하는 것이 권장됩니다.
참고 링크:
- help.sap.com — Validations in RAP
- help.sap.com — Behavior Definition Syntax
- help.sap.com — Save Sequence
- SAP Developers — RAP Managed Tutorial
- SAP Community — ABAP RAP Topics
- GitHub — SAP-samples/cloud-abap-rap
핵심 한 줄
on MODIFY는 "잘못된 값이 머무르지 않게" 하는 게이트, on SAVE는 "최종 일관성"을 잠그는 게이트 — 검증의 성격에 맞는 시점을 고르는 것이 RAP Validation 설계의 출발점입니다.