왜 Copy Action이 아닌 Factory Action인가
RAP(RESTful ABAP Programming Model)에서 기존 비즈니스 오브젝트(BO)를 복제하는 요구사항은 실무에서 매우 자주 등장합니다. 예를 들어 판매 오더(SalesOrder)를 그대로 복사해 새 오더 초안으로 만들거나, 계약서 템플릿을 기반으로 새 계약을 생성하는 시나리오가 대표적입니다. 초보자는 대개 Copy 액션을 찾다가 벽에 부딪히는데, RAP에는 별도의 built-in Copy Action이 존재하지 않기 때문입니다. Fiori Elements의 "Copy" 버튼은 사실 표준 Create를 열고 기존 필드 값을 프론트엔드에서 복사해 채워 넣는 방식이며, 서버 측에서 트랜잭션 무결성을 보장하는 복제는 아닙니다.
- 이 글을 마치면 확인할 것
- [ ] Factory action과 일반 action의 차이 설명
- [ ]
factory action선언 시[1]cardinality의 의미 이해 - [ ] Behavior Implementation에서
MODIFY ENTITIES로 자식까지 함께 복제 - [ ] draft 활성화된 BO에서의 복제 흐름 파악
미리 익혀두면 좋은 것들
이 글은 advanced 난이도이므로 RAP 관리 시나리오(managed 또는 unmanaged)의 CDS behavior definition 문법, EML(Entity Manipulation Language)의 MODIFY ENTITIES·READ ENTITIES 구문, Fiori Elements Object Page의 액션 바인딩 개념을 사전 이해하고 있어야 합니다. Draft-enabled BO 처리 흐름과 CREATE BY \_ChildEntity 구문에 익숙하면 훨씬 수월합니다.
실습 환경 및 준비 사항
이 글의 코드는 ABAP Cloud (S/4HANA Cloud Public Edition 2402 이상 또는 ABAP Environment on BTP 2402 릴리스)를 기준으로 작성했으며, on-premise S/4HANA 2022 FPS02 이상에서도 동일한 문법이 통용됩니다. 개발 도구는 ADT(ABAP Development Tools for Eclipse) 3.40 이상, 미리보기 확인용 Fiori Elements Preview는 SAP Business Application Studio 또는 ADT의 Service Binding Editor를 사용합니다.
- 대상 BO:
ZR_SalesOrderTP(Root),ZR_SalesOrderItemTP(Child) - Behavior 구현 클래스:
ZBP_R_SalesOrderTP - Service Definition:
ZUI_SALESORDER_O4, Binding type: OData V4 UI - 권한 오브젝트:
S_DEVELOP(개발자),S_RFC(테스트 실행)
Factory Action의 동작 원리와 개념
Factory action은 이름 그대로 "인스턴스를 생산해내는 액션"입니다. 일반 action이 이미 존재하는 인스턴스에 대해 동작하는 반면, factory action은 실행 결과로 새 인스턴스를 만들어냅니다. 이는 CREATE 오퍼레이션의 확장이라고 이해하면 됩니다. RAP 런타임 관점에서 factory action은 다음과 같이 분류됩니다.
- Static factory action: 특정 인스턴스와 무관하게 실행. 예: "빈 템플릿에서 오더 생성"
- Instance-bound factory action: 특정 인스턴스를 기반으로 실행. 복제(Copy) 시나리오의 정석
비유하자면, 일반 action은 이미 조립된 자동차의 사이드미러를 조정하는 버튼이고, factory action은 기존 자동차 사양서를 옆에 두고 같은 옵션으로 새 자동차를 한 대 더 뽑아내는 주문서입니다. 원본은 그대로 유지되며, 새로 태어난 인스턴스는 별도의 키를 갖습니다.
Draft 활성화 BO에서 factory action을 실행하면 자동으로 새 draft 인스턴스가 생성됩니다. 이는 사용자가 저장 전에 복제된 값을 수정할 수 있게 해주므로 UX 관점에서도 매우 유리합니다. 반환 파라미터로 $self를 지정하면 Fiori Elements가 자동으로 새 인스턴스의 Object Page로 네비게이션합니다.
BO 정의 단계: behavior definition에 factory action 선언하기
먼저 Behavior Definition(BDEF)에 factory action을 선언합니다. 아래는 판매 오더 루트 엔티티에 duplicateOrder라는 이름의 factory action을 정의한 예시입니다.
managed implementation in class zbp_r_salesordertp unique;
strict ( 2 );
with draft;
define behavior for ZR_SalesOrderTP alias SalesOrder
persistent table zsalesorder
draft table zsalesorder_d
lock master
total etag LastChangedAt
authorization master ( instance )
etag master LastChangedAt
{
create;
update;
delete;
association \_Item { create; with draft; }
field ( readonly, numbering : managed ) SalesOrderUUID;
field ( readonly ) SalesOrderId, CreatedAt, CreatedBy, LastChangedAt;
" Factory action 선언: 인스턴스 기반, 결과는 $self로 반환
factory action duplicateOrder [1] result [1] $self;
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare;
}
여기서 핵심은 factory action duplicateOrder [1] result [1] $self; 구문입니다. 앞쪽 [1]은 한 번 호출당 하나의 새 인스턴스를 만들겠다는 의미이고, result [1] $self는 반환값이 새로 생성된 오더 자신이라는 뜻입니다.
실전 예제: SalesOrder 복제 구현
Behavior Implementation 클래스에서 factory action의 메서드를 구현합니다. 원본 오더를 읽어와 필요한 필드를 필터링한 뒤, 새 UUID와 새 오더 ID를 부여하고 자식 아이템까지 함께 생성하는 것이 전체 흐름입니다.
CLASS lhc_salesorder IMPLEMENTATION.
METHOD duplicateOrder.
" 1) 원본 오더 헤더 읽기
READ ENTITIES OF zr_salesordertp IN LOCAL MODE
ENTITY SalesOrder
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_source_orders)
FAILED DATA(ls_read_failed)
REPORTED DATA(ls_read_reported).
IF lt_source_orders IS INITIAL.
APPEND VALUE #( %msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = '원본 오더를 찾을 수 없습니다.' )
) TO reported-salesorder.
RETURN.
ENDIF.
" 2) 원본 아이템 함께 읽기 (by association)
READ ENTITIES OF zr_salesordertp IN LOCAL MODE
ENTITY SalesOrder BY \_Item
ALL FIELDS WITH VALUE #( FOR o IN lt_source_orders
( %tky = o-%tky ) )
RESULT DATA(lt_source_items).
" 3) 복제할 헤더 인스턴스 준비 (키/시스템 필드 제외)
DATA lt_create TYPE TABLE FOR CREATE zr_salesordertp\\_SalesOrder.
DATA lt_create_item TYPE TABLE FOR CREATE zr_salesordertp\_Item.
LOOP AT lt_source_orders INTO DATA(ls_src).
DATA(lv_new_uuid) = cl_system_uuid=>create_uuid_x16_static( ).
APPEND VALUE #(
%cid = |CID_{ ls_src-SalesOrderUUID }|
SalesOrderUUID = lv_new_uuid
CustomerId = ls_src-CustomerId
Currency = ls_src-Currency
OrderStatus = 'D'
Description = |Copy of { ls_src-Description }|
) TO lt_create.
" 자식 아이템 매핑
LOOP AT lt_source_items INTO DATA(ls_item)
WHERE SalesOrderUUID = ls_src-SalesOrderUUID.
APPEND VALUE #(
%cid_ref = |CID_{ ls_src-SalesOrderUUID }|
%target = VALUE #( (
%cid = |CID_ITEM_{ ls_item-ItemUUID }|
ItemUUID = cl_system_uuid=>create_uuid_x16_static( )
ProductId = ls_item-ProductId
Quantity = ls_item-Quantity
UnitOfMeasure = ls_item-UnitOfMeasure
NetAmount = ls_item-NetAmount
) )
) TO lt_create_item.
ENDLOOP.
ENDLOOP.
" 4) 새 인스턴스 및 자식 한꺼번에 생성
MODIFY ENTITIES OF zr_salesordertp IN LOCAL MODE
ENTITY SalesOrder
CREATE FIELDS ( SalesOrderUUID CustomerId Currency
OrderStatus Description )
WITH lt_create
CREATE BY \_Item
FIELDS ( ItemUUID ProductId Quantity UnitOfMeasure NetAmount )
WITH lt_create_item
MAPPED DATA(ls_mapped)
FAILED DATA(ls_failed)
REPORTED DATA(ls_reported).
" 5) 결과 반환 ($self)
result = VALUE #( FOR m IN ls_mapped-salesorder
( %cid = m-%cid
%param = CORRESPONDING #( m ) ) ).
failed = ls_failed.
reported = ls_reported.
ENDMETHOD.
ENDCLASS.
복제 대상 필드 필터링과 연관 데이터 처리
실무에서 가장 자주 놓치는 지점이 어떤 필드를 그대로 복사하고, 어떤 필드를 초기화할 것인가입니다. 다음 원칙을 권장합니다.
- 초기화해야 할 필드: 키 필드(UUID), 상태 필드(승인, 확정, 게시 여부), 감사 필드(CreatedAt, CreatedBy 등), 외부 시스템 참조 ID(회계 문서 번호 등)
- 복사해야 할 필드: 마스터 데이터 참조(고객, 통화, 조직), 비즈니스 로직 값(수량, 금액, 조건)
- 변형해야 할 필드: Description(예: "Copy of ..." 접두), 유효기간(오늘 기준 재계산)
연관 엔티티가 여러 계층(SalesOrder → Item → Schedule Line)이면 MODIFY ENTITIES의 CREATE BY \_Association 구문을 중첩해 사용합니다. 이때 %cid와 %cid_ref를 올바르게 연결하지 않으면 자식이 어느 부모에 속하는지 런타임이 판정할 수 없어 PRECONDITION_FAILED 에러가 발생합니다.
운영 배포용으로 상승시킬 때는 트랜잭션 로그와 성능도 함께 고려해야 합니다. 아래는 대량 복제 시나리오에 적용할 만한 보완 코드 스니펫입니다.
DATA(lo_logger) = cl_bali_log_factory=>create_log(
VALUE bal_s_log( object = 'ZSO'
subobject = 'DUPLICATE'
aluser = sy-uname ) ).
TRY.
IF lines( lt_create_item ) > 1000.
DATA(lv_batches) = CEIL( lines( lt_create_item ) / 1000 ).
ENDIF.
CATCH cx_abap_behv INTO DATA(lx_behv).
lo_logger->add_exception( lx_behv ).
APPEND VALUE #( %msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = lx_behv->get_text( ) )
) TO reported-salesorder.
ENDTRY.
테스트 및 디버깅 실전 팁
Factory action 개발 시 가장 유용한 검증 도구는 EML을 직접 호출하는 ABAP Unit 테스트입니다. Behavior implementation을 직접 인스턴스화하는 것보다 MODIFY ENTITIES ... EXECUTE duplicateOrder 구문으로 통합 시나리오를 검증하는 편이 권장됩니다.
CLASS ltc_duplicate DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS test_copy_creates_new_uuid FOR TESTING.
ENDCLASS.
CLASS ltc_duplicate IMPLEMENTATION.
METHOD test_copy_creates_new_uuid.
DATA(lv_src_uuid) = cl_system_uuid=>create_uuid_x16_static( ).
MODIFY ENTITIES OF zr_salesordertp
ENTITY SalesOrder
EXECUTE duplicateOrder
FROM VALUE #( ( %tky-SalesOrderUUID = lv_src_uuid
%cid = 'DUP1' ) )
MAPPED DATA(ls_mapped).
cl_abap_unit_assert=>assert_not_initial( ls_mapped-salesorder ).
ENDMETHOD.
ENDCLASS.
흔한 실수와 트러블슈팅 FAQ
- Q1. duplicateOrder 실행 후 자식 아이템이 딸려오지 않아요.
%cid와%cid_ref가 정확히 일치하는지 확인하세요. 문자열 조립 시 대소문자 또는 공백 하나만 달라도 매핑에 실패합니다. 또한 BDEF의 association 절에{ create; with draft; }가 선언되어 있어야 draft로 아이템 생성이 가능합니다. - Q2. Fiori UI에서 버튼이 안 보이거나 비활성화됩니다.
Metadata Extension(MDE)에@UI.identification이나@UI.lineItem어노테이션으로 액션을 노출했는지 확인하세요. 서비스 재배포(Publish Local) 후 브라우저 캐시를 지우고 다시 접속해야 반영되는 경우가 많습니다. - Q3. Draft 활성 BO인데 저장이 안 되고 즉시 활성화되어 버립니다.
factory action의 result 절과 UI manifest의 navigation intent를 점검하세요.$self가 올바르게 선언되어야 Fiori Elements가 draft 경로로 네비게이션합니다. - Q4. 성능이 매우 느려요.
READ ENTITIES 시ALL FIELDS대신 필요한 필드만 지정하고, 아이템 수가 많은 경우 배치 처리로 분할하세요. SAT 트랜잭션으로 Determinations 과도 트리거 여부를 프로파일링합니다.
여기서 더 나아가고 싶다면
Factory action 패턴을 익혔다면 parameterized factory action으로 확장해 보세요. 복제 시점에 새 고객 ID나 유효 기간을 파라미터로 받을 수 있습니다. 또한 대량 복제가 필요한 백엔드 시나리오에서는 factory action 대신 별도의 class-based service로 로직을 분리하고, RAP는 얇은 오케스트레이션 계층으로만 사용하는 아키텍처도 검토할 만합니다. Side effects 어노테이션과 결합하면 복제 직후 화면 갱신 흐름까지 자연스럽게 이어집니다.
댓글 0
아직 댓글이 없습니다.