목차
Early numbering
App을 다시 실행해 보겠습니다. 그리고 Create 버튼을 누르고 저장하지 마시고 뒤로가기 버튼을 누릅니다. 그러면 아래와 같이 Draft 상태인 라인 하나가 추가됩니다. 이 상태에서 다시 Create 버튼을 누르면 어떻게 될까요? 한번 눌러보겠습니다.

The key value is already in use. Please enter a different one. 라는 에러가 발생합니다. 최초 Create 버튼을 눌렀을때 Travel ID가 빈값으로 Draft 테이블에 저장이 된 상태에서 다시 Create 버튼을 누르면 Travel ID를 빈값으로 다시 Draft 테이블에 저장을 시도하기 때문에 Key 중복으로 발생하는 오류입니다. App은 여러사람이 사용하기 때문에 이 문제를 해결해야 합니다. 어떻게 해결해야 할까요? 네 Create 버튼을 눌렀을때 유니크한 Travel ID를 채번해서 사용하면 됩니다. 이때 사용하는 기능이 Early numbering 입니다.

Early numbering vs Late numbering

Early numbering이 Key를 먼저 채번한다고 하면 저장시점에 채번하는 기능도 있지 않을까요? 있습니다. 바로 Late numbering입니다. Late numbering은 이번시간에 배울 대상은 아니지만 Early numbering과 같이 설명하겠습니다.
RAP로 Fiori App을 개발하다 보면 반드시 마주치는 개념이 하나 있습니다. 바로 Key를 언제 생성하는냐의 문제입니다.
Early numbering의 장점은 사용자가 현재 자기가 작업하고 있는 Key정보를 확인하면서 작업할 수 있고 Draft 기능과도 완벽하게 호환됩니다. 하지만 Draft 상태에서 저장하지 않고 Discard draft 해버리면 채번했던 Key는 버려지게 됩니다.
Late numbering은 실제 저장시점에 채번이 되기 때문에 버려지는 Key가 없다는 장점이 있습니다. 하지만 데이터를 입력하는 동안에는 %cid 라는 임시키로 제어해야 하기 때문에 로직이 좀더 복잡해지는 단점이 있습니다. Late numbering은 해당기능 실습할때 좀더 자세히 설명드리겠습니다.
Behavior Definition에 early numbering을 추가해 보겠습니다. 아래와 같이 추가해주면 create 부분에 노란색 밑줄이 생깁니다. early numbering 기능은 ABAP로직으로 구현해야 하는 즉, Method 가 필요한 기능입니다. 이런 기능을 추가하게 되면 관련 부분에 저렇게 노란색 밑줄이 생깁니다. 왜 create 에 밑줄이 생겼을까요? early numbering 기능은 데이터 생성시에 Key값을 만들어 주는 로직이므로 create 에 밑줄이 생기게 됩니다.

Method를 생성해 주겠습니다. create에 마우스를 찍고 Ctrl + 1 Quick Assist 가 나오면 더블클릭을 합니다.

earlynumbering_create 라는 Method가 생성되었습니다. 해당오류는 Behavior Definition에 early numbering 을 추가하고 활성화를 안한 상태에서 Method를 생성하면 발생합니다. Class 와 Behavior Definition을 동시에 활성화 해줍니다.

early numbering Method에 로직을 추가해 보겠습니다. 그전에 Eclipse에서 ABAP 개발을 하면서 구조, 테이블, Method등의 Object의 Type을 확인하는 탭을 추가해 보겠습니다. Window -> Show view -> Other 을 클릭합니다. 아래와 같은 창이 뜨면 element로 검색해서 ABAP Element Info를 선택하고 Open 버튼을 클릭합니다.

자주쓰는 기능이기 때문에 하단에 탭으로 포함시켜놓고 개발하면 편합니다. 그럼 우리가 로직을 추가시켜야할 earlynumbering_create Method에 마우스를 클릭해 보겠습니다. 그러면 방금 추가한 ABAP Element Info 창에 클릭한 Method의 파라미터 정보를 확인 할 수 있습니다. 이 Method 는 entities 파라미터를 입력 받아서 mapped, failed, reported 파라미터를 Return 하는 Method입니다. RAP에서 사용하는 대부분의 Method들이 조금씩은 틀리지만 거의 이와 유사한 형태로 데이터를 주고 받는다고 생각하시면 됩니다. Method가 받는 파라미터인 entities의 타입을 살펴보겠습니다. 우측에 [ derived type..]을 클릭합니다.

Travel 정보를 Create 시 호출되는 Method 이기 때문에 전달받는 정보를 보면 Travel 테이블의 필드인걸 확인할 수 있습니다. 그런데 %로 시작하는 필드들이 보입니다. 그리고 파마리터를 선언한 타입도 평소에 보지 못한 타입입니다. RAP내부에서 자주 사용되는 derived type에 대해 먼저 알아보겠습니다.

Derived type
RAP 모델에서 사용하는 데이터 타입을 보면 TYPE TABLE FOR READ IMPORTING.. 또는 TYPE RESPONSE FOR... 등과 같은 처음보는 문법들이 나옵니다. 바로 Derived type(파생타입)입니다. CDS View와 Behavior Definition의 구조를 기반으로 SAP 컴파일러가 자동으로 파생시켜주는 특수 ABAP 데이터 타입입니다.
Derived type 으로 생성된 구조체를 보면 CDS View에서 정의한 필드 외에 퍼센트(%) 기호가 붙은 특이한 필드들이 자동으로 붙어 있습니다. 이런 변수들은 백엔드 트랜젝션과 화면(UI)을 제어하기 위한 특수 제어 필드입니다.
자주쓰는 특수 제어필드에 대해 간략히 알아보겠습니다.
%tky (Transactional Key): Key값 + 임시저장(Draft) 데이터인지 실제(Active) 데이터인지 상태 플래그가 합쳐진 복합키 입니다. 실제 개발할때 가장 많이 쓰입니다.
%key (Business Key): 상태 플래그를 제외하고 순수한 Key값들만 모아놓은 구조체입니다.
%control (Control Structure): 데이터 변경(MODIFY)시 어떤 필드가 수정되었고 어떤 필드가 그대로인지 컴파일러에게 플래그로 알려주는 역할을 합니다.
%cid (Content ID): 데이터 생성시 실제 Key값이 생성되기 전까지 임시로 화면과 백엔드를 묶어주는 임시 Key 역할을 합니다.
특수제어필드의 사용법은 실습하면서 다시 설명드리겠습니다.
다시 Class로 돌아와서 earlynumbering_create 메소드에 아래와 같이 소스를 추가해 줍니다. Importing 파라미터인 entities가 테이블 타입이기 때문에 LOOP 구분을 추가하고 데이터가 어떻게 들어오는지 확인을 합니다. 앞으로도 기능이 추가면세 추가된 메소드에 로직을 구현해야 할때 메소드에 파라미터가 어떻게 들어오는지 먼저 확인해보고 로직을 구현하시길 추천드립니다. LOOP 구문이 시작되는 부분에 디버깅을 겁니다. 디버깅을 거는 방법은 아래 화면에 보이는 좌측 공간에 마우스로 더블클릭하면 디버깅 포인트가 생깁니다. 그 상태에서 다시한번 더블클릭하면 디버깅 포인트가 사라집니다. 디버깅을 걸고 App을 실행해서 Create 버튼을 눌러보겠습니다.

디버깅이 걸렸을때의 화면입니다. 디버깅 포인트를 찍은곳을 잘 보시면 화살표가 위치해 있습니다. 디버깅 단축키(F5, F6, F7, F8) 는 SAP GUI에서와 동일합니다. Create이기 때문에 빈 Row 한줄을 entities 파라미터로 보내주고 있습니다. entities는 derived type이기 때문에 %가 붙은 제어필드도 보입니다. %CID에는 값이 들어 있습니다. Create이기 때문에 임시 Key역할은 하는 값이 들어 있습니다. %IS_DRAFT 에는 '01' 값이 들어있습니다. '01'은 True, '00'은 'False' 입니다. 즉 Draft 상태라는걸 알 수 있습니다. 여기서 Travel ID를 새로 채번에서 UI로 값을 리턴해서 화면에 채번된 번호가 보이게 해보겠습니다.

Note.
사실 App에서 Create 버튼으로 값이 들어올때는 무조껀 한줄이 들어올수 밖에 없습니다. 하지만 entities 파라미터 타입이 테이블 타입이기 때문에 여러건이 들어올수 있다고 생각하고 로직을 구현해야 합니다.
read table entities into data(ls_entities) index 1. 이런식으로 하면 안됩니다. RAP BO를 생성하게 되면 외부에서도 그 기능을 호출할 수 있습니다. 그때 여러건을 동시에 파라미터에 담아서 호출할 수도 있기 때문입니다. "App UI로는 절대 이런 데이터는 들어올수 없어" 라고 생가하지 마시고 외부에서도 호출하여 사용할 수 있다고 생각하고 개발해야 합니다.
F8번을 눌러 디버깅 모드에서 빠져나가 보겠습니다. 디버깅이 완료된후 다시 개발화면으로 돌아가려면 우측 상단에 ABAP버튼을 누르면 됩니다. App화면에 Travel ID가 빈값으로 Draft 데이터가 생성되면 그다음 생성시 덤프가 나기때문에 생성이 되면 discard draft 로 삭제를 해주면서 실습하면 됩니다.

로직을 구현해 보겠습니다. 번호범위 오브젝트는 표준 오브젝트 /DMO/TRV_M을 사용하겠습니다. 번호가 정상적으로 채번되면 Mapped를 통해서 값을 UI로 전달합니다. 만약 실패시 에러메세지를 화면에 보여줘야 하기때문에 Failed와 Reported에 값을 넣고 리턴하면 됩니다.
METHOD earlynumbering_create.
LOOP AT entities INTO DATA(ls_entities).
TRY.
cl_numberrange_runtime=>number_get(
EXPORTING
nr_range_nr = '01'
object = '/DMO/TRV_M'
IMPORTING
number = DATA(lv_number)
).
CATCH cx_nr_object_not_found INTO DATA(lo_error).
failed-zever_m_travel_i = VALUE #( base failed-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft ) ).
reported-zever_m_travel_i = VALUE #( base reported-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft
%msg = lo_error ) ).
CONTINUE.
CATCH cx_number_ranges INTO DATA(lo_error2).
failed-zever_m_travel_i = VALUE #( base failed-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft ) ).
reported-zever_m_travel_i = VALUE #( base reported-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft
%msg = lo_error2 ) ).
CONTINUE.
ENDTRY.
mapped-zever_m_travel_i = VALUE #( base mapped-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft
TravelId = lv_number ) ).
ENDLOOP.
ENDMETHOD.
Note.
RAP에서 비즈니스 로직을 구현할때 가장 많이 만나는 삼총사 출력 파라미터에 대해 알아보겠습니다.
mapped: 비즈니스 로직이 성공적으로 완료되어 데이터가 새로 생성되거나 변경 되었을때 그 결과물(Key값)을 담습니다.
mapped에는 Behavior Definition에 연결되어있는 모든 entity가 파라미터로 존재합니다. 위의 예제같은 경우는 mapped-zever_m_travel_i 와 mapped-zever_m_booking_i 두개가 존재합니다. 위 실습에서는 Travel에 대한 create이기 때문에 mapped-zever_m_travel_i 쪽에 값을 넘겨주면 됩니다.
failed: 로직수행중 오류가 발생하여 더이상 진행 못하도록 프레임워크에 알릴때 에러난 대상을 failed로 넘겨줍니다. 구체적인 에러 메시지는 담지 않고 에러가 발생한 데이터의 식별자 (%cid 또는 %tky)만 담습니다. 여기에 데이터가 담기면 시스템은 자동으로 SAVE단계를 중단 시킵니다.
reported: 사용자 화면(UI)에 보여줄 구체적인 에러/경고/성공 메세지를 담습니다. 보통 failed와 세트로 움직입니다. failed에 에러난 대상을 지정했다면 reported에는 메시지 정보를 담아서 화면에 에러 정보를 띄워줍니다.
위 소스를 보면 %is_draft 라는 필드가 있습니다. 현재 상태가 Draft 상태(01) 인지 Active 상태(00) 인지 값을 전달해 줘야 합니다. 입력받은 entities 에 있는 %is_draft 값을 그대로 전달해 주면 됩니다. 값을 전달해 주지 않으면 오류가 발생합니다. 만약 Draft 기능을 활성화 하지 않으면 시스템에 %is_draft 라는 변수 자체가 존재하지 않기 때문에 그때는 따로 신경쓸 필요는 없습니다.
여기까지 하고 App을 실행시켜서 create버튼을 눌러보겠습니다. 아래와 같이 채번된 번호가 Travel ID로 보여집니다. 나머지 필드에도 값을 입력하고 하단에 Create 버튼을 클릭합니다.

위에서 디버깅 포인트를 삭제 안하신 분들은 확인 되시겠지만 최종 Create 버튼을 누를때도 earlynumbering_create 메소드가 또 실행됩니다. entities정보를 보면 %is_draft가 이번에는 Active 상태(00)인걸 확인할수 있고 TRAVELID 필드에는 이전에 채번한 번호 4248이 들어있습니다. 여기서 다시 채번하게 되면 4249번이 mapped로 전달되게 됩니다. 이러면 어떻게 될까요?

테이블을 확인해 보겠습니다. Max Rows를 5000으로 바꾸고 전체를 검색해 보겠습니다. 4248, 4249 모두 저장이 안되었습니다. Draft 당시 채번했던 번호를 실제 저장시점에도 동일하게 넣어줘야 합니다. 소스를 수정해 보겠습니다.

entities-TravelId에 값이 없으면 채번하고 entities-TravelId에 값이 있으면 그 번호 그대로 사용하도록 로직을 추가했습니다. 다시 App을 실행해 보겠습니다.
METHOD earlynumbering_create.
LOOP AT entities INTO DATA(ls_entities).
IF ls_entities-TravelId IS INITIAL.
TRY.
cl_numberrange_runtime=>number_get(
EXPORTING
nr_range_nr = '01'
object = '/DMO/TRV_M'
IMPORTING
number = DATA(lv_number)
).
CATCH cx_nr_object_not_found INTO DATA(lo_error).
failed-zever_m_travel_i = VALUE #( BASE failed-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft ) ).
reported-zever_m_travel_i = VALUE #( BASE reported-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft
%msg = lo_error ) ).
CONTINUE.
CATCH cx_number_ranges INTO DATA(lo_error2).
failed-zever_m_travel_i = VALUE #( BASE failed-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft ) ).
reported-zever_m_travel_i = VALUE #( BASE reported-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft
%msg = lo_error2 ) ).
CONTINUE.
ENDTRY.
ELSE.
lv_number = ls_entities-TravelId.
ENDIF.
mapped-zever_m_travel_i = VALUE #( BASE mapped-zever_m_travel_i ( %cid = ls_entities-%cid
%is_draft = ls_entities-%is_draft
TravelId = lv_number ) ).
ENDLOOP.
ENDMETHOD.
4256번이 채번이 되었고 최종 Create 버튼을 클릭합니다.

아마 또 덤프오류가 발생할 것입니다. Eclipse의 Feed Reader에 가서 확인해 보겠습니다.

/DMO/TRAVEL 테이블에 00000000 이란 Key값이 존재해서 Key 중복 에러가 났습니다. 그러면 실제 그런 데이터가 있는지 확인해 보겠습니다.

실제로 00000000 이란 Key값이 존재합니다. 그러면 왜 우리가 채번한 4256번으로 저장을 시도하지 않고 00000000 값으로 저장을 시도했을까요? 원인은 Behavior Definition에서 사용하는 Entity의 필드명과 실제 테이블의 필드명이 불일치 하기 때문입니다.

아래와 같이 Entity에서 사용하는 필드명은 TravelId 인 반면 실제 테이블 필드명은 travel_id 입니다. 이렇게 Entity와 실제 테이블의 필드명이 틀린경우는 조회할때는 아무 문제가 없지만 테이블을 생성, 수정, 삭제 할 때는 문제가 됩니다. 이 문제를 해결해 보겠습니다. Interface Behavior Definition으로 가보겠습니다.

아래와 같이 Entity 필드명과 테이블 필드명을 서로 매핑을 시켜줘야 합니다. 왼쪽이 Entity 필드명이고 오른쪽이 테이블 필드명입니다. 매핑을 완료 했으면 이제 다시 App을 실행해 보겠습니다.

이번엔 4257번이 채번되었습니다. Create 버튼을 눌러보겠습니다.

드디어 생성되었다는 메세지가 떴습니다. 테이블도 확인해 보겠습니다.

테이블에도 정상적으로 생성되었습니다.

잘 돌아가는 예제로 실습하기 보다는 지금처럼 많은 오류를 내보고 확인하고 해결해 가는 과정을 계속 경험을 해봐야 합니다.
이번 시간에는 Travel정보에 대해 Key값을 채번하여 저장하는 것까지 해봤습니다. 아직은 많이 미흡한 App입니다. 다음시간에 이어서 App을 조금더 발전시켜 보겠습니다.