RAP

관리형 RAP만 쓰다 놓치는 이유 3가지 #shorts #SAP #RAP

📖 개요 — 이 글에서 얻어갈 것

RAP(ABAP RESTful Application Programming Model)으로 트랜잭셔널 앱을 설계할 때 가장 먼저 마주치는 갈림길이 관리형(managed)과 비관리형(unmanaged)의 선택입니다. 관리형은 프레임워크가 CRUD·버퍼·저장을 대신 처리해 주지만, 비관리형은 개발자가 트랜잭션 버퍼부터 저장 시퀀스까지 전부 직접 구현합니다. 이 글은 "언제 비관리형을 선택해야 하는가"라는 실무 판단 기준을 중심으로, 실제 동작하는 구매요청(Purchase Requisition) 비관리형 BO 예제를 단계별로 구현합니다.

  • 관리형 vs 비관리형의 런타임 책임 분담 차이를 설명할 수 있다
  • 레거시 자산(함수모듈·기존 버퍼·비표준 잠금) 재사용 여부로 선택 기준을 세울 수 있다
  • 비관리형 behavior definition과 handler/saver 클래스를 직접 작성할 수 있다
  • SAVE 시퀀스(finalize → check_before_save → save → cleanup)의 각 phase 역할을 구분할 수 있다

📚 미리 갖춰두면 좋은 배경

이 글은 advanced 난이도를 전제로 합니다. CDS 뷰 엔티티 정의, EML(Entity Manipulation Language) 기본 문법, ABAP OO(상속·리디파인), 그리고 관리형 RAP으로 간단한 BO를 한 번이라도 만들어 본 경험이 있으면 내용을 따라오기 수월합니다. 클래식 ABAP의 ENQUEUE/DEQUEUE 잠금 개념과 함수모듈 기반 업데이트 로직을 알고 있다면 비관리형의 설계 의도가 더 명확하게 보입니다.

🔧 환경 · 버전 · 준비물

비관리형 시나리오는 다음 환경에서 검증할 수 있습니다. 버전에 따라 문법 지원 범위가 다르므로 주의가 필요합니다.

  • SAP BTP ABAP Environment(Steampunk) 또는 SAP S/4HANA 2021 이상(ABAP Platform 7.56+) — strict(2) 모드와 late numbering 문법을 온전히 쓰려면 S/4HANA 2022 이상이 일반적으로 권장됩니다
  • ADT(ABAP Development Tools for Eclipse) — behavior definition 편집은 SAP GUI가 아닌 ADT에서만 가능합니다
  • 기반 CDS 뷰 엔티티(예: ZI_PurchReq)와 레거시 테이블/함수모듈 접근 권한
  • 서비스 노출 테스트용 Service Binding(OData V4 UI) 및 EML 기반 ABAP Unit 실행 환경

온프레미스 구버전(7.54 이하)에서는 unmanaged가 사실상 유일한 RAP 구현 방식이었다는 점도 기억해 두면 기존 코드 해석에 도움이 됩니다.

💡 핵심 개념 — 프레임워크가 운전하는가, 내가 운전하는가

관리형과 비관리형의 차이는 비유하자면 자동변속기와 수동변속기의 차이입니다. 관리형에서는 RAP 런타임이 트랜잭션 버퍼 생성, CRUD 처리, 데이터베이스 저장을 자동으로 수행하고 개발자는 결정(determination)·검증(validation)·액션 같은 비즈니스 로직만 끼워 넣습니다. 비관리형에서는 이 모든 기어 변속 — 버퍼 관리, READ/MODIFY 처리, 잠금, 저장 — 을 개발자가 직접 조작합니다.

요청 처리 흐름을 도식화하면 다음과 같습니다.

[OData 요청] → RAP 런타임 → 인터랙션 phase(create/update/delete/read/lock 핸들러가 자체 버퍼에 기록) → SAVE 시퀀스(finalize → check_before_save → save → cleanup) → DB 커밋

관리형은 이 흐름 전체를 프레임워크가 소유하고, 비관리형은 각 단계마다 개발자 코드가 호출됩니다. 그렇다면 언제 수동변속기를 잡아야 할까요? 일반적으로 다음 조건 중 하나라도 해당하면 비관리형이 유력한 후보입니다.

  • 레거시 로직 재사용 — 이미 검증된 함수모듈(BAPI 스타일 저장 로직, 자체 번호 채번, 변경문서 기록)이 있고 이를 버릴 수 없는 경우
  • 기존 트랜잭션 버퍼 존재 — 클래식 앱이 쓰던 전역 버퍼/메모리 구조를 그대로 트랜잭션 소스로 삼아야 하는 경우
  • 비표준 잠금 전략 — 표준 ENQUEUE 오브젝트가 아닌 애플리케이션 레벨 잠금, 외부 시스템 잠금 등을 써야 하는 경우
  • 외부 시스템이 데이터 소유 — 저장 대상이 로컬 테이블이 아니라 원격 API/타 시스템이라 프레임워크가 대신 저장할 수 없는 경우
  • 복잡한 저장 시퀀스 — 저장 시점에 여러 테이블·큐·이벤트를 특정 순서로 갱신해야 하는 경우

반대로 그린필드(신규 개발)이고 데이터 모델이 CDS와 테이블로 깔끔하게 정리되며 표준 잠금·ETag로 충분하다면, 관리형이 개발 속도와 유지보수 면에서 일반적으로 유리합니다. 참고로 관리형에도 with unmanaged save라는 절충안이 있어 "버퍼는 프레임워크, 저장만 내 코드"가 필요할 때는 완전 비관리형까지 갈 필요가 없습니다. 이 절충안으로도 부족할 때 — 즉 버퍼 자체를 직접 소유해야 할 때 — 비로소 비관리형을 선택합니다.

💻 실전 코드 3단계 — 비관리형 구매요청 BO 구현

1단계: 비관리형 behavior definition과 뼈대 클래스

루트 엔티티 ZI_PurchReq에 대한 behavior definition입니다. unmanaged 키워드가 핵심이며, 관리형과 달리 persistent table 지정이 없습니다. 저장 책임이 프레임워크에 없기 때문입니다.

unmanaged implementation in class zbp_i_purchreq unique;
strict ( 2 );

define behavior for ZI_PurchReq alias PurchReq
lock master
authorization master ( instance )
etag master LastChangedAt
{
  create;
  update;
  delete;
  field ( readonly ) PurchReqId, LastChangedAt;
}

구현 클래스 zbp_i_purchreq의 로컬 핸들러 뼈대입니다. 관리형에서는 만들 일이 없는 create/update/delete/read/lock 메서드가 전부 개발자 몫으로 선언됩니다.

CLASS lhc_purchreq DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    METHODS create FOR MODIFY IMPORTING entities FOR CREATE PurchReq.
    METHODS update FOR MODIFY IMPORTING entities FOR UPDATE PurchReq.
    METHODS delete FOR MODIFY IMPORTING keys    FOR DELETE PurchReq.
    METHODS read   FOR READ   IMPORTING keys    FOR READ   PurchReq RESULT result.
    METHODS lock   FOR LOCK   IMPORTING keys    FOR LOCK   PurchReq.
ENDCLASS.

2단계: 레거시 함수모듈 재사용과 실패 처리 — 인터랙션 phase

비관리형 선택의 대표 사유인 "검증된 레거시 로직 재사용"을 create 핸들러로 구현합니다. 기존 시스템의 검증 함수모듈 Z_PR_VALIDATE_ITEM을 그대로 호출하고, 결과는 트랜잭션 버퍼(싱글톤 lcl_pr_buffer)에만 기록합니다. 이 시점에 절대 DB를 갱신하지 않는 것이 비관리형의 제1원칙입니다. 오류는 failed/reported 파라미터로 반환해 RAP 런타임이 표준 방식으로 클라이언트에 전달하게 합니다.

METHOD create.
  LOOP AT entities INTO DATA(ls_entity).
    " 레거시 검증 로직 재사용 — 비관리형을 선택한 핵심 이유
    CALL FUNCTION 'Z_PR_VALIDATE_ITEM'
      EXPORTING iv_material  = ls_entity-MaterialId
                iv_quantity  = ls_entity-Quantity
      IMPORTING ev_rc        = DATA(lv_rc)
                ev_message   = DATA(lv_msg).

    IF lv_rc <> 0.
      APPEND VALUE #( %cid = ls_entity-%cid
                      %create = if_abap_behv=>mk-on )
             TO failed-purchreq.
      APPEND VALUE #( %cid = ls_entity-%cid
                      %msg  = new_message_with_text(
                                severity = if_abap_behv_message=>severity-error
                                text     = lv_msg ) )
             TO reported-purchreq.
      CONTINUE.
    ENDIF.

    " DB가 아닌 자체 트랜잭션 버퍼에만 기록
    lcl_pr_buffer=>get_instance( )->stage_create( ls_entity ).

    APPEND VALUE #( %cid = ls_entity-%cid
                    PurchReqId = ls_entity-PurchReqId )
           TO mapped-purchreq.
  ENDLOOP.
ENDMETHOD.

METHOD lock.
  " 비표준 잠금: 레거시 ENQUEUE 함수모듈 재사용
  LOOP AT keys INTO DATA(ls_key).
    CALL FUNCTION 'ENQUEUE_EZ_PURCHREQ'
      EXPORTING purchreq_id    = ls_key-PurchReqId
      EXCEPTIONS foreign_lock  = 1 OTHERS = 2.
    IF sy-subrc <> 0.
      APPEND VALUE #( %tky = ls_key-%tky ) TO failed-purchreq.
      APPEND VALUE #( %tky = ls_key-%tky
                      %msg = new_message_with_text(
                               severity = if_abap_behv_message=>severity-error
                               text = '다른 사용자가 편집 중입니다' ) )
             TO reported-purchreq.
    ENDIF.
  ENDLOOP.
ENDMETHOD.

포인트 정리: %cid는 클라이언트가 부여한 임시 식별자로, create 성공 시 mapped를 통해 실제 키와 매핑됩니다. lock 핸들러는 관리형이라면 표준 잠금 오브젝트로 자동 처리되지만, 여기서는 레거시 잠금 정책을 그대로 이식했습니다 — 이것이 "비표준 잠금 때문에 비관리형을 쓴다"의 실체입니다.

3단계: 프로덕션 — SAVE 시퀀스, 성능, 테스트

저장은 saver 클래스에서 phase별로 구현합니다. 저장 순서 제어(헤더 → 아이템 → 변경문서 → 이벤트 큐)가 필요한 시나리오, 즉 관리형의 자동 저장으로는 표현하기 어려운 요구가 여기서 해결됩니다.

CLASS lsc_purchreq DEFINITION INHERITING FROM cl_abap_behavior_saver.
  PROTECTED SECTION.
    METHODS finalize          REDEFINITION.
    METHODS check_before_save REDEFINITION.
    METHODS save              REDEFINITION.
    METHODS cleanup           REDEFINITION.
ENDCLASS.

CLASS lsc_purchreq IMPLEMENTATION.
  METHOD finalize.
    " 최종 번호 채번: 레거시 번호범위 재사용 (late numbering 대안)
    lcl_pr_buffer=>get_instance( )->assign_final_numbers( ).
  ENDMETHOD.

  METHOD check_before_save.
    " 저장 직전 일관성 검사 — 실패 시 save_failed 로 전체 롤백 유도
    IF lcl_pr_buffer=>get_instance( )->is_consistent( ) = abap_false.
      APPEND VALUE #( ) TO failed-purchreq.
    ENDIF.
  ENDMETHOD.

  METHOD save.
    DATA(lo_buffer) = lcl_pr_buffer=>get_instance( ).
    " 성능: 단건 루프가 아닌 내부 테이블 일괄 처리
    DATA(lt_create) = lo_buffer->get_staged_creates( ).
    IF lt_create IS NOT INITIAL.
      INSERT zpr_header FROM TABLE @lt_create.
    ENDIF.
    " 외부 시스템 연동: 저장 확정분만 이벤트 아웃박스에 적재
    lo_buffer->enqueue_outbox_events( ).
    " 주의: COMMIT WORK 금지 — 커밋은 RAP 런타임 소관
  ENDMETHOD.

  METHOD cleanup.
    lcl_pr_buffer=>get_instance( )->clear( ).
  ENDMETHOD.
ENDCLASS.

프로덕션 관점 체크포인트 세 가지입니다. 첫째, 성능 — save phase에서 버퍼 내용을 내부 테이블로 모아 일괄 INSERT/UPDATE하고, 핸들러의 read도 키 테이블 단위 FOR ALL ENTRIES류 일괄 조회로 작성해 N+1 접근을 피합니다. 둘째, 테스트 — EML 덕분에 UI 없이 ABAP Unit으로 전체 라운드트립을 검증할 수 있습니다.

METHOD create_then_commit_persists.
  MODIFY ENTITIES OF ZI_PurchReq
    ENTITY PurchReq
    CREATE FIELDS ( MaterialId Quantity )
    WITH VALUE #( ( %cid = 'C1' MaterialId = 'MAT-100' Quantity = 5 ) )
    MAPPED   DATA(mapped)
    FAILED   DATA(failed)
    REPORTED DATA(reported).

  cl_abap_unit_assert=>assert_initial( failed-purchreq ).
  COMMIT ENTITIES RESPONSE OF ZI_PurchReq FAILED DATA(commit_failed).
  cl_abap_unit_assert=>assert_initial( commit_failed-purchreq ).
ENDMETHOD.

셋째, 보안 — 비관리형이라도 인스턴스 권한 체크는 FOR AUTHORIZATION 핸들러로 구현하고, 레거시 함수모듈 내부의 AUTHORITY-CHECK에만 의존하지 않는 것이 일반적으로 권장됩니다. RAP 런타임은 핸들러의 권한 결과를 표준 403 응답으로 변환해 주기 때문입니다.

⚠️ 흔한 실수와 트러블슈팅 FAQ

  • Q1. 핸들러(create/update)에서 바로 DB에 INSERT하면 안 되나요? — 안 됩니다. 인터랙션 phase의 변경은 반드시 트랜잭션 버퍼에만 머물러야 합니다. 사용자가 저장 없이 이탈하면 그 요청은 폐기되어야 하는데, 핸들러에서 DB를 건드리면 유령 데이터가 남습니다. DB 반영은 오직 save phase에서만 수행하세요.
  • Q2. save 메서드 안에서 COMMIT WORK를 호출했더니 덤프(BEHAVIOR_ILLEGAL_STATEMENT)가 납니다. — RAP에서는 커밋/롤백 주체가 런타임입니다. save 안에서는 COMMIT WORK, ROLLBACK WORK, 다이얼로그 호출이 모두 금지됩니다. 레거시 함수모듈이 내부에서 커밋을 수행한다면 커밋 없는 변형을 만들거나 IN UPDATE TASK 방식으로 재설계해야 합니다.
  • Q3. 값을 만들었는데 다시 조회하면 비어 있습니다. — read 핸들러가 DB만 보고 자체 버퍼를 무시하는 전형적 실수입니다. 비관리형의 read는 "버퍼 우선, 없으면 DB" 순서로 병합해서 결과를 돌려줘야 같은 트랜잭션 안의 미저장 변경이 화면에 반영됩니다.
  • Q4. Draft를 켜고 싶은데 가능한가요? — 비관리형에서도 draft를 활성화할 수 있지만, 추가 콜백(draft 테이블 동기화 등) 구현 부담이 커집니다. draft가 핵심 요구라면 관리형 + unmanaged save 절충안을 먼저 검토하는 편이 일반적으로 효율적입니다.

디버깅 팁: ADT의 EML 콘솔 클래스(if_oo_adt_classrun)로 MODIFY/COMMIT을 직접 실행하면 UI 레이어를 배제한 채 각 phase 브레이크포인트를 정확히 짚을 수 있습니다.

🚀 이후 학습 경로와 관련 주제

비관리형 기본기를 갖췄다면 다음 주제로 확장하는 흐름이 자연스럽습니다. (1) managed with unmanaged save — 버퍼는 프레임워크에 맡기고 저장만 커스터마이징하는 절충 패턴과의 비교 실습, (2) late numbering — 저장 시점 채번을 표준 문법으로 선언하는 방법, (3) RAP 비즈니스 이벤트 — save phase에서 적재한 아웃박스를 이벤트 메시로 전환하기, (4) 부모-자식 컴포지션이 있는 비관리형 BO에서의 버퍼 설계. 특히 레거시 마이그레이션 프로젝트라면 "비관리형으로 감싸서 서비스화 → 점진적으로 관리형 재작성"이라는 2단계 전략을 검토해 볼 가치가 있습니다.

📚 더 깊이 볼 자료

댓글 0

아직 댓글이 없습니다.