SAP RAP 비즈니스 오브젝트 만들어보기

ABAP 개발자에게 최근 몇 년간 가장 큰 변화를 꼽으라면 단연 RAP(ABAP RESTful Application Programming Model) 일 것이다. 과거 BOPF나 클래식 다이나믹 프로그래밍으로 만들던 트랜잭션 애플리케이션이, 이제는 CDS 뷰와 선언적 동작 정의(Behavior Definition)로 대체되었다.

특히 S/4HANA 클린 코어(Clean Core) 전략과 ABAP Cloud가 표준이 되면서, RAP은 “선택 사항”이 아니라 “기본기”가 되었다. 이 글에서는 RAP 비즈니스 오브젝트(Business Object, 이하 BO)를 밑바닥부터 만들어 Fiori Elements 화면까지 띄우는 전 과정을 정리한다.

1. RAP 아키텍처를 먼저 이해하자

RAP BO는 여러 개의 개발 오브젝트가 레이어를 이루는 구조다. 순서대로 쌓아 올린다고 생각하면 된다.

[Service Binding]      ← OData V2/V4 프로토콜 노출, Fiori 프리뷰
        ↑
[Service Definition]   ← 어떤 엔티티를 외부에 공개할지 정의
        ↑
[Projection Layer]     ← ZC_ 프로젝션 뷰 + 프로젝션 BDL (소비 계층)
        ↑
[Behavior Definition]  ← BDL: create/update/delete/action/validation 선언
[Behavior Implementation] ← ABAP 클래스: 실제 로직 구현
        ↑
[Data Model]           ← ZR_ CDS 루트 뷰 엔티티 (+ composition 자식)
        ↑
[Database Table]       ← 영속 테이블, Draft 테이블

핵심은 선언(Declaration)과 구현(Implementation)의 분리다. BDL 파일에서 “이 BO는 생성/수정/삭제가 가능하고, 저장 시점에 고객번호 검증을 한다”고 선언하면, 프레임워크가 그에 맞는 메서드 시그니처를 만들어주고 개발자는 로직만 채운다.

구현 시나리오 3가지

시나리오설명사용 시점
Managed프레임워크가 DB 저장까지 전부 처리신규 개발 (그린필드)
Unmanaged개발자가 저장 로직까지 직접 구현기존 레거시 로직 재사용
Managed with unmanaged save조회/버퍼는 프레임워크, 저장만 직접기존 함수모듈 저장 로직 활용

신규 개발이라면 고민 없이 Managed를 선택하면 된다. 이 글도 Managed 기준으로 진행한다.

2. 사전 준비

  • Eclipse + ADT(ABAP Development Tools) — RAP은 SAP GUI에서 개발할 수 없다. ADT 필수.
  • 시스템: S/4HANA 2020 이상 (온프레미스), 또는 SAP BTP ABAP Environment(Steampunk), ABAP Trial 환경
  • 언어 버전: ABAP for Cloud Development 권장 (릴리즈된 API만 사용 가능)
  • 데모 데이터를 쓰려면 /DMO/FLIGHT_DATA_GENERATOR 프로그램을 먼저 실행해 항공편 샘플 데이터를 생성해 둔다.

예제로는 SAP 공식 데모와 동일한 여행 예약(Travel) 시나리오를 사용한다. 접미사 _JSC는 각자의 이니셜로 바꾸면 된다.

3. Step 1 — 영속 테이블(Persistent Table) 정의

먼저 데이터가 실제로 저장될 DB 테이블을 만든다. RAP에서 중요한 것은 관리 필드(Administrative Fields) 다. abp_ 로 시작하는 전용 데이터 엘리먼트를 써야 프레임워크가 자동으로 값을 채워준다.

abap

@EndUserText.label : 'Travel Data'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #LIMITED
define table ztravel_jsc {
  key client            : abap.clnt not null;
  key travel_uuid       : sysuuid_x16 not null;
  travel_id             : /dmo/travel_id;
  agency_id             : /dmo/agency_id;
  customer_id           : /dmo/customer_id;
  begin_date            : /dmo/begin_date;
  end_date              : /dmo/end_date;
  @Semantics.amount.currencyCode : 'ztravel_jsc.currency_code'
  booking_fee           : /dmo/booking_fee;
  @Semantics.amount.currencyCode : 'ztravel_jsc.currency_code'
  total_price           : /dmo/total_price;
  currency_code         : /dmo/currency_code;
  description           : /dmo/description;
  overall_status        : /dmo/overall_status;
  created_by            : abp_creation_user;
  created_at            : abp_creation_tstmpl;
  last_changed_by       : abp_lastchange_user;
  last_changed_at       : abp_lastchange_tstmpl;
  local_last_changed_at : abp_locinst_lastchange_tstmpl;
}

관리 필드가 두 개인 이유

처음 보면 헷갈리는 부분인데, 변경 타임스탬프가 두 개다.

  • last_changed_at (Total ETag): 루트를 포함해 자식 엔티티까지 어디든 변경되면 갱신. OData 클라이언트의 동시성 제어용.
  • local_last_changed_at (ETag Master / Local ETag): 해당 인스턴스 자체가 변경될 때만 갱신. 낙관적 잠금(Optimistic Locking)용.

자식 엔티티(Booking)에는 local_last_changed_at 만 있으면 된다. Total ETag는 루트에만 존재한다.

4. Step 2 — CDS 루트 뷰 엔티티 정의

DB 테이블 위에 데이터 모델을 얹는다. RAP에서는 define root view entity 를 쓴다. 구식 define view (DDIC 기반 뷰)가 아니라 View Entity 를 쓰는 것이 중요하다.

abap

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Travel - Root View Entity'
define root view entity ZR_TRAVEL_JSC
  as select from ztravel_jsc as Travel

  composition [0..*] of ZR_BOOKING_JSC as _Booking

  association [0..1] to /DMO/I_Agency   as _Agency   on $projection.AgencyID   = _Agency.AgencyID
  association [0..1] to /DMO/I_Customer as _Customer on $projection.CustomerID = _Customer.CustomerID
  association [0..1] to I_Currency      as _Currency on $projection.CurrencyCode = _Currency.Currency
{
  key travel_uuid          as TravelUUID,
      travel_id            as TravelID,
      agency_id            as AgencyID,
      customer_id          as CustomerID,
      begin_date           as BeginDate,
      end_date             as EndDate,

      @Semantics.amount.currencyCode: 'CurrencyCode'
      booking_fee          as BookingFee,
      @Semantics.amount.currencyCode: 'CurrencyCode'
      total_price          as TotalPrice,
      currency_code        as CurrencyCode,

      description          as Description,
      overall_status       as OverallStatus,

      @Semantics.user.createdBy: true
      created_by           as CreatedBy,
      @Semantics.systemDateTime.createdAt: true
      created_at           as CreatedAt,
      @Semantics.user.lastChangedBy: true
      last_changed_by      as LastChangedBy,
      @Semantics.systemDateTime.lastChangedAt: true
      last_changed_at      as LastChangedAt,
      @Semantics.systemDateTime.localInstanceLastChangedAt: true
      local_last_changed_at as LocalLastChangedAt,

      /* Associations */
      _Booking,
      _Agency,
      _Customer,
      _Currency
}

Composition vs Association

RAP BO의 계층 구조는 composition 으로 만든다. Association과의 차이는 명확하다.

  • Composition: 부모-자식 종속 관계. 부모가 삭제되면 자식도 삭제된다. BO의 일부다.
  • Association: 단순 참조. 다른 BO나 마스터 데이터를 가리킬 뿐이다.

자식 뷰(ZR_BOOKING_JSC)에는 반드시 반대 방향의 association to parent 를 선언해야 한다.

abap

define view entity ZR_BOOKING_JSC
  as select from zbooking_jsc as Booking
  association to parent ZR_TRAVEL_JSC as _Travel
    on $projection.TravelUUID = _Travel.TravelUUID
{
  key booking_uuid as BookingUUID,
      travel_uuid  as TravelUUID,
      ...
      _Travel
}

5. Step 3 — Behavior Definition (BDL) 작성

여기가 RAP의 심장이다. 루트 뷰에서 우클릭 → New Behavior Definition 을 선택하면 골격이 생성된다.

abap

managed implementation in class zbp_r_travel_jsc unique;
strict ( 2 );
with draft;

define behavior for ZR_TRAVEL_JSC alias Travel
persistent table ztravel_jsc
draft table ztravel_jsc_d
lock master
total etag LastChangedAt
authorization master ( instance )
etag master LocalLastChangedAt
{
  field ( numbering : managed, readonly ) TravelUUID;
  field ( readonly ) CreatedBy, CreatedAt, LastChangedBy, LastChangedAt,
                     LocalLastChangedAt, TotalPrice, OverallStatus;
  field ( mandatory ) AgencyID, CustomerID, BeginDate, EndDate;

  create;
  update;
  delete;

  association _Booking { create ( features : instance ); with draft; }

  // 결정(Determination)
  determination setInitialStatus on modify { create; }
  determination calculateTotalPrice on modify { field BookingFee, CurrencyCode; }

  // 검증(Validation)
  validation validateCustomer on save { create; field CustomerID; }
  validation validateDates    on save { create; field BeginDate, EndDate; }

  // 액션(Action)
  action ( features : instance ) acceptTravel result [1] $self;
  action ( features : instance ) rejectTravel result [1] $self;

  // Draft 액션
  draft action Edit;
  draft action Activate optimized;
  draft action Discard;
  draft action Resume;
  draft determine action Prepare
  {
    validation validateCustomer;
    validation validateDates;
  }

  mapping for ztravel_jsc
  {
    TravelUUID          = travel_uuid;
    TravelID            = travel_id;
    AgencyID            = agency_id;
    CustomerID          = customer_id;
    BeginDate           = begin_date;
    EndDate             = end_date;
    BookingFee          = booking_fee;
    TotalPrice          = total_price;
    CurrencyCode        = currency_code;
    Description         = description;
    OverallStatus       = overall_status;
    CreatedBy           = created_by;
    CreatedAt           = created_at;
    LastChangedBy       = last_changed_by;
    LastChangedAt       = last_changed_at;
    LocalLastChangedAt  = local_last_changed_at;
  }
}

주요 키워드 해설

키워드의미
strict ( 2 )최신 문법 검사 모드. 신규 개발은 반드시 켠다. 나중에 켜면 수정할 게 많아진다
with draftDraft(임시 저장) 기능 활성화. Fiori에서 편집 중 이탈해도 내용이 보존된다
lock master이 엔티티가 잠금 주체. 자식은 lock dependent by _Travel
numbering : managedUUID 키를 프레임워크가 자동 생성 (키 타입이 RAW16일 때만)
field ( features : instance )인스턴스 상태에 따라 동적으로 활성/비활성 결정
mapping forCDS 필드명 ↔ DB 컬럼명 매핑. 이름이 다르면 필수

Draft 테이블 생성

draft table ztravel_jsc_d 를 적으면 처음엔 에러가 난다. 에러 위에서 Quick Fix (Ctrl+1) → “Create draft table” 을 선택하면 필요한 관리 필드(draftuuid, draftentitycreationdatetime 등)를 포함한 테이블이 자동 생성된다. 손으로 만들지 말 것.

Determination / Validation / Action 구분

세 가지가 헷갈리기 쉬운데, 역할이 명확히 다르다.

  • Determination: 값을 자동으로 채우는 로직. 예) 생성 시 상태를 ‘Open’으로 설정, 예약료 변경 시 총액 재계산
  • Validation: 값을 검사하는 로직. 실패 시 저장을 막고 메시지를 반환. 데이터를 변경하면 안 된다
  • Action: 사용자가 버튼을 눌러 실행하는 로직. Fiori 화면에 버튼으로 표시된다

6. Step 4 — Behavior Implementation 클래스 구현

BDL에서 implementation in class zbp_r_travel_jsc 부분에 커서를 두고 Quick Fix를 실행하면 클래스가 생성된다. 로컬 핸들러 클래스(lhc_Travel)에 메서드 골격까지 자동으로 만들어진다.

6-1. Determination 구현

abap

METHOD setInitialStatus.
  " 1) 현재 값 읽기
  READ ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
    ENTITY Travel
      FIELDS ( OverallStatus )
      WITH CORRESPONDING #( keys )
    RESULT DATA(travels).

  " 2) 이미 상태가 있는 건은 제외
  DELETE travels WHERE OverallStatus IS NOT INITIAL.
  CHECK travels IS NOT INITIAL.

  " 3) 기본값 'O'(Open) 설정
  MODIFY ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
    ENTITY Travel
      UPDATE FIELDS ( OverallStatus )
      WITH VALUE #( FOR travel IN travels
                    ( %tky          = travel-%tky
                      OverallStatus = 'O' ) )
  REPORTED DATA(update_reported).

  reported = CORRESPONDING #( DEEP update_reported ).
ENDMETHOD.

여기서 등장하는 READ ENTITIES / MODIFY ENTITIES 가 바로 EML(Entity Manipulation Language) 이다. RAP에서는 SELECTUPDATE 로 DB에 직접 접근하지 않고, 반드시 EML로 트랜잭션 버퍼를 거친다.

IN LOCAL MODE 를 빼먹지 말 것. 이 구문은 권한 체크와 피처 컨트롤을 건너뛰고 BO 내부에서 직접 접근한다는 뜻이다. 핸들러 클래스 내부에서는 거의 항상 필요하다.

6-2. Validation 구현

abap

METHOD validateCustomer.
  READ ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
    ENTITY Travel
      FIELDS ( CustomerID )
      WITH CORRESPONDING #( keys )
    RESULT DATA(travels).

  DATA customers TYPE SORTED TABLE OF /dmo/customer
       WITH UNIQUE KEY customer_id.

  customers = CORRESPONDING #( travels DISCARDING DUPLICATES
                               MAPPING customer_id = CustomerID EXCEPT * ).
  DELETE customers WHERE customer_id IS INITIAL.

  IF customers IS NOT INITIAL.
    SELECT FROM /dmo/customer FIELDS customer_id
      FOR ALL ENTRIES IN @customers
      WHERE customer_id = @customers-customer_id
      INTO TABLE @DATA(valid_customers).
  ENDIF.

  LOOP AT travels INTO DATA(travel).
    " 상태 초기화 (이전 검증 메시지 제거)
    APPEND VALUE #( %tky = travel-%tky ) TO failed-travel.
    APPEND VALUE #( %tky               = travel-%tky
                    %state_area        = 'VALIDATE_CUSTOMER' )
           TO reported-travel.

    IF travel-CustomerID IS INITIAL.
      APPEND VALUE #( %tky = travel-%tky ) TO failed-travel.
      APPEND VALUE #( %tky         = travel-%tky
                      %state_area  = 'VALIDATE_CUSTOMER'
                      %msg         = NEW /dmo/cm_flight_messages(
                                       textid   = /dmo/cm_flight_messages=>enter_customer_id
                                       severity = if_abap_behv_message=>severity-error )
                      %element-CustomerID = if_abap_behv=>mk-on )
             TO reported-travel.

    ELSEIF NOT line_exists( valid_customers[ customer_id = travel-CustomerID ] ).
      APPEND VALUE #( %tky = travel-%tky ) TO failed-travel.
      APPEND VALUE #( %tky         = travel-%tky
                      %state_area  = 'VALIDATE_CUSTOMER'
                      %msg         = NEW /dmo/cm_flight_messages(
                                       textid      = /dmo/cm_flight_messages=>customer_unkown
                                       customer_id = travel-CustomerID
                                       severity    = if_abap_behv_message=>severity-error )
                      %element-CustomerID = if_abap_behv=>mk-on )
             TO reported-travel.
    ENDIF.
  ENDLOOP.
ENDMETHOD.

핵심 패턴은 이렇다. 검증 실패 시 failed 테이블에 키를 넣어 저장을 막고, reported 테이블에 메시지를 넣어 사용자에게 알린다. %element-CustomerID 를 켜두면 Fiori 화면에서 해당 입력 필드가 빨갛게 표시된다.

6-3. Action 구현

abap

METHOD acceptTravel.
  " 상태를 'A'(Accepted)로 변경
  MODIFY ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
    ENTITY Travel
      UPDATE FIELDS ( OverallStatus )
      WITH VALUE #( FOR key IN keys
                    ( %tky          = key-%tky
                      OverallStatus = 'A' ) ).

  " 변경된 인스턴스를 다시 읽어 result로 반환
  READ ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
    ENTITY Travel
      ALL FIELDS WITH CORRESPONDING #( keys )
    RESULT DATA(travels).

  result = VALUE #( FOR travel IN travels
                    ( %tky   = travel-%tky
                      %param = travel ) ).
ENDMETHOD.

6-4. 인스턴스 피처 컨트롤

이미 승인된 여행에 “승인” 버튼이 활성화되어 있으면 곤란하다. 상태에 따라 버튼을 비활성화한다.

abap

METHOD get_instance_features.
  READ ENTITIES OF ZR_TRAVEL_JSC IN LOCAL MODE
    ENTITY Travel
      FIELDS ( TravelUUID OverallStatus )
      WITH CORRESPONDING #( keys )
    RESULT DATA(travels)
    FAILED failed.

  result = VALUE #( FOR travel IN travels
                    ( %tky = travel-%tky
                      %action-acceptTravel =
                        COND #( WHEN travel-OverallStatus = 'A'
                                THEN if_abap_behv=>fc-o-disabled
                                ELSE if_abap_behv=>fc-o-enabled )
                      %action-rejectTravel =
                        COND #( WHEN travel-OverallStatus = 'X'
                                THEN if_abap_behv=>fc-o-disabled
                                ELSE if_abap_behv=>fc-o-enabled ) ) ).
ENDMETHOD.

7. Step 5 — 프로젝션 레이어 (소비 계층)

여기서 많은 개발자가 “왜 뷰를 두 번 만들지?” 라고 묻는다. 답은 재사용성 이다.

  • ZR_ (Interface/Base 레이어): 비즈니스 로직과 데이터 모델의 단일 원천. 여러 서비스가 공유
  • ZC_ (Consumption/Projection 레이어): 특정 UI나 API를 위한 맞춤 노출. 필요한 필드만 골라내고 UI 어노테이션을 붙임

같은 BO를 관리자용 앱과 조회 전용 앱에 각각 다르게 노출해야 할 때, 프로젝션 레이어만 하나 더 만들면 된다.

프로젝션 뷰

abap

@EndUserText.label: 'Travel - Consumption View'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.allowExtensions: true
@Search.searchable: true
define root view entity ZC_TRAVEL_JSC
  provider contract transactional_query
  as projection on ZR_TRAVEL_JSC
{
  @Search.defaultSearchElement: true
  key TravelUUID,
      TravelID,
      AgencyID,
      CustomerID,
      BeginDate,
      EndDate,
      BookingFee,
      TotalPrice,
      CurrencyCode,
      Description,
      OverallStatus,
      LastChangedAt,
      LocalLastChangedAt,

      /* Associations */
      _Booking : redirected to composition child ZC_BOOKING_JSC,
      _Agency,
      _Customer,
      _Currency
}

provider contract transactional_queryredirected to 가 핵심이다. Composition은 반드시 프로젝션 계층의 자식으로 리디렉션해야 한다.

프로젝션 Behavior Definition

abap

projection;
strict ( 2 );
use draft;

define behavior for ZC_TRAVEL_JSC alias Travel
{
  use create;
  use update;
  use delete;

  use action acceptTravel;
  use action rejectTravel;

  use action Edit;
  use action Activate;
  use action Discard;
  use action Resume;
  use action Prepare;

  use association _Booking { create; with draft; }
}

프로젝션 BDL에서는 기본 계층에 선언된 것 중 노출할 것만 골라서 use 한다. 조회 전용 앱이라면 use create/update/delete 를 빼면 그만이다.

8. Step 6 — Service Definition & Binding

Service Definition

abap

@EndUserText.label: 'Service Definition for Travel'
define service ZUI_TRAVEL_JSC
{
  expose ZC_TRAVEL_JSC   as Travel;
  expose ZC_BOOKING_JSC  as Booking;
  expose /DMO/I_Agency   as Agency;
  expose /DMO/I_Customer as Customer;
  expose I_Currency      as Currency;
}

밸류 헬프(F4)에 쓰이는 마스터 데이터 뷰도 함께 노출해야 화면에서 정상 동작한다. 이걸 빼먹어 F4가 안 뜨는 경우가 많다.

Service Binding

Service Definition에서 우클릭 → New Service Binding.

바인딩 타입용도
OData V4 – UIFiori Elements 앱 (신규 개발 권장)
OData V2 – UI구버전 Fiori 호환 필요 시
OData V4 – Web API시스템 간 연동 API

생성 후 Activate 버튼을 누르면 서비스가 활성화되고, 엔티티를 선택한 뒤 Preview 를 누르면 브라우저에서 바로 Fiori Elements 화면이 뜬다. 별도 UI5 개발 없이 여기까지 온 것이다.


9. Step 7 — 메타데이터 확장으로 화면 다듬기

프리뷰를 처음 열면 컬럼이 하나도 안 보이거나 배치가 엉망일 것이다. UI 어노테이션이 없기 때문이다. 프로젝션 뷰에 직접 쓸 수도 있지만, @Metadata.allowExtensions: true 를 켜뒀으니 Metadata Extension 으로 분리하는 편이 깔끔하다.

abap

@Metadata.layer: #CORE
@UI: {
  headerInfo: {
    typeName: 'Travel',
    typeNamePlural: 'Travels',
    title: { type: #STANDARD, value: 'TravelID' }
  }
}
annotate entity ZC_TRAVEL_JSC with
{
  @UI.facet: [
    { id: 'Travel', purpose: #STANDARD, type: #IDENTIFICATION_REFERENCE,
      label: 'Travel', position: 10 },
    { id: 'Booking', purpose: #STANDARD, type: #LINEITEM_REFERENCE,
      label: 'Booking', position: 20, targetElement: '_Booking' }
  ]

  @UI.hidden: true
  TravelUUID;

  @UI: {
    lineItem:       [ { position: 10 } ],
    identification: [ { position: 10 } ],
    selectionField: [ { position: 10 } ]
  }
  TravelID;

  @UI: {
    lineItem:       [ { position: 20 } ],
    identification: [ { position: 20 } ],
    selectionField: [ { position: 20 } ]
  }
  @Consumption.valueHelpDefinition: [ { entity: { name: '/DMO/I_Agency',
                                                  element: 'AgencyID' } } ]
  AgencyID;

  @UI: {
    lineItem: [ { position: 60, importance: #HIGH },
                { type: #FOR_ACTION, dataAction: 'acceptTravel', label: '승인' },
                { type: #FOR_ACTION, dataAction: 'rejectTravel', label: '반려' } ]
  }
  OverallStatus;
}
  • @UI.facet — 상세 화면의 섹션 구성. #LINEITEM_REFERENCE 로 자식 테이블을 붙인다
  • @UI.lineItem — 목록(List Report) 컬럼
  • @UI.selectionField — 상단 필터 필드
  • @Consumption.valueHelpDefinition — F4 밸류 헬프
  • type: #FOR_ACTION — 액션을 버튼으로 노출

10. 자주 만나는 오류와 해결법

“Entity does not have an ETag field” BDL에 etag master LocalLastChangedAt 선언이 빠졌거나, CDS 뷰에 @Semantics.systemDateTime.localInstanceLastChangedAt: true 어노테이션이 없다.

“Field … is not mapped” CDS 필드명과 DB 컬럼명이 다른데 mapping for 블록에 해당 필드가 없다. Quick Fix로 매핑 전체를 생성할 수 있다.

Determination에서 값을 바꿨는데 저장이 안 됨 MODIFY ENTITIESIN LOCAL MODE 를 빠뜨렸거나, 해당 필드가 field ( readonly ) 로 선언되어 있는 경우다. readonly 필드는 프레임워크가 아니라 determination에서만 변경 가능하도록 별도 처리가 필요하다.

%tky vs %key Draft를 쓴다면 무조건 %tky(transactional key)를 쓴다. %tky 는 키에 더해 Draft 여부(%is_draft)까지 포함한다. %key 만 쓰면 Draft 인스턴스를 못 찾는다.

Fiori 프리뷰에서 “승인” 버튼이 안 보임 프로젝션 BDL에 use action acceptTravel; 을 선언했는지, 메타데이터 확장에 type: #FOR_ACTION 을 넣었는지 확인한다. 둘 다 필요하다.

UUID가 자동 생성되지 않음 numbering : managed 는 키 필드 타입이 sysuuid_x16(RAW16)일 때만 동작한다. CHAR 타입 키라면 early numbering 을 직접 구현해야 한다.

11. 끝으로

RAP으로 BO 하나를 만들면서 얻는 가장 큰 소득은 관심사의 분리 다.

  • 데이터 모델은 CDS가
  • 트랜잭션 동작은 BDL이
  • 비즈니스 로직은 핸들러 클래스가
  • 화면은 어노테이션이

각각 책임진다. 클래식 ABAP에서 다이나믹 프로그램 하나에 화면 로직과 DB 접근과 검증이 뒤엉켜 있던 것과 비교하면, 유지보수 관점에서 차원이 다르다.

처음에는 만들어야 할 오브젝트가 많아 부담스럽게 느껴지지만, 두세 번 반복하면 패턴이 몸에 익는다. ADT의 Quick Fix가 골격을 대부분 만들어주기 때문에 실제로 타이핑하는 양은 생각보다 적다.

다음 단계로 학습을 이어간다면 이 순서를 권한다.

  1. Draft 처리 심화 — Draft 인스턴스와 Active 인스턴스의 라이프사이클
  2. 비즈니스 이벤트(RAP Business Events) — 상태 변경 시 이벤트 발행
  3. 권한 제어 — CDS Access Control(DCL) + authorization master
  4. Unmanaged 시나리오 — 레거시 함수모듈을 RAP으로 래핑하기
  5. Side Effects 어노테이션 — 필드 변경 시 다른 필드를 즉시 갱신

SAP에서 제공하는 openSAP 강의RAP 개발 가이드(공식 문서), 그리고 /DMO/ 데모 패키지의 소스 코드가 가장 좋은 교재다. 특히 /DMO/ 패키지는 Managed / Unmanaged / Draft 시나리오별 완성된 예제가 모두 들어 있으니, 막힐 때마다 열어보길 권한다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤