📖 개요: BAPI는 성공했다는데 테이블엔 데이터가 없다?
ABAP 입문자가 가장 자주 겪는 미스터리 중 하나가 이것입니다. BAPI를 호출했고, RETURN에 에러도 없었고, 디버거로 보면 오더 번호까지 반환됐는데 — 정작 VBAK 테이블을 조회하면 아무것도 없습니다. 원인은 대부분 하나, 커밋을 하지 않아서입니다. 이 글은 SAP LUW(논리적 작업 단위) 관점에서 저장 누락이 발생하는 흐름을 추적하고, BAPI_SALESORDER_CREATEFROMDAT2 실전 예제로 올바른 커밋 패턴을 단계별로 구성합니다.
- BAPI 호출 후 데이터가 사라지는 정확한 원인 이해
- SAP LUW와 업데이트 태스크의 동작 원리 파악
- BAPI_TRANSACTION_COMMIT / ROLLBACK의 올바른 사용 위치
- RETURN 파라미터 검사 → 커밋/롤백 분기 패턴 습득
📚 미리 알아두면 좋은 배경
ABAP 기본 문법(FUNCTION 호출, 내부 테이블, 구조체)과 SE37에서 함수 모듈을 테스트해 본 경험이 있으면 충분합니다. 데이터베이스 트랜잭션(commit/rollback)의 일반 개념을 알고 있다면 SAP LUW와의 차이를 이해하는 데 도움이 됩니다. 판매 오더(VBAK/VBAP) 구조는 몰라도 따라올 수 있습니다.
🔧 환경과 준비물
이 글의 예제는 SAP S/4HANA 2023 (On-Premise) 및 SAP NetWeaver 7.5 이상(ECC 6.0 포함) 환경을 기준으로 작성했습니다. 클래식 BAPI는 두 환경 모두에서 동작하지만, S/4HANA Cloud Public Edition에서는 릴리스된 API만 허용되므로 사용 전 api.sap.com에서 릴리스 여부 확인이 권장됩니다.
- 개발 권한이 있는 시스템 (SE37, SE38 또는 ADT/Eclipse)
- 테스트용 판매 조직/유통 채널/제품부문 마스터 데이터 (예: 1000/10/00)
- 트랜잭션 BAPI(BAPI Explorer)로 대상 BAPI의 문서 확인
- 디버깅용으로 SM13(업데이트 레코드 조회) 접근 권한이 있으면 좋습니다
💡 핵심 개념: 데이터베이스 커밋과 SAP LUW는 다르다
일반 데이터베이스 프로그래밍에서는 INSERT 하면 곧 커밋되고 끝입니다. 하지만 ABAP은 한 겹이 더 있습니다. 바로 SAP LUW(Logical Unit of Work)입니다.
비유하자면 이렇습니다. BAPI 호출은 택배 송장을 작성해서 접수대에 올려놓는 행위입니다. 송장이 완벽하게 작성됐어도(RETURN에 에러 없음), 접수 버튼(COMMIT WORK)을 누르기 전까지는 실제 발송이 일어나지 않습니다. 프로그램이 그냥 종료되거나 롤백되면 접수대 위의 송장은 조용히 버려집니다. 에러 메시지조차 없이요. 이것이 "BAPI는 성공했는데 데이터가 없는" 현상의 정체입니다.
내부 동작을 조금 더 풀어보면:
- 대부분의 표준 BAPI는 데이터베이스에 직접 INSERT하지 않고,
CALL FUNCTION ... IN UPDATE TASK로 업데이트 요청을 등록만 합니다. - 이 요청들은
COMMIT WORK가 실행되는 순간 업데이트 태스크(V1/V2)에서 일괄 처리되어 비로소 테이블에 기록됩니다. - 커밋 없이 세션이 끝나면 등록된 요청은 전부 폐기됩니다. BAPI가 반환한 오더 번호는 번호 범위(Number Range)에서 미리 채번된 것일 뿐, 실제 레코드 존재를 뜻하지 않습니다.
흐름을 도식화하면 다음과 같습니다.
BAPI 호출 → 내부 검증 + 업데이트 요청 등록(메모리) → [분기점] COMMIT WORK 실행 시 → 업데이트 태스크가 DB 기록 → 데이터 저장 완료 / COMMIT 없이 종료 시 → 요청 폐기 → 저장 누락
또 하나 중요한 설계 원칙: BAPI 자신은 커밋하지 않습니다. 여러 BAPI를 하나의 트랜잭션으로 묶을 수 있도록(예: 오더 생성 + 텍스트 변경을 한 번에 확정) 커밋 책임을 호출자에게 넘긴 것입니다. 그래서 호출자는 반드시 BAPI_TRANSACTION_COMMIT(또는 COMMIT WORK)을 명시적으로 호출해야 합니다. RFC로 외부에서 호출할 때도 마찬가지로, 같은 RFC 커넥션에서 커밋 BAPI를 호출해야 합니다.
💻 실전 코드 3단계: 판매 오더 생성으로 배우는 커밋 패턴
1단계 — 기본 예제: 저장이 사라지는 코드 vs 남는 코드
아래는 커밋이 빠져서 데이터가 사라지는 전형적인 코드입니다.
DATA: ls_order_header TYPE bapisdhd1,
ls_header_flag TYPE bapisdhd1x,
lt_order_items TYPE TABLE OF bapisditm,
lt_order_partners TYPE TABLE OF bapiparnr,
lt_return TYPE TABLE OF bapiret2,
lv_salesdoc TYPE bapivbeln-vbeln.
ls_order_header-doc_type = 'TA'. " 표준 오더
ls_order_header-sales_org = '1000'.
ls_order_header-distr_chan = '10'.
ls_order_header-division = '00'.
ls_header_flag-doc_type = 'X'.
ls_header_flag-sales_org = 'X'.
ls_header_flag-distr_chan = 'X'.
ls_header_flag-division = 'X'.
APPEND VALUE #( partn_role = 'AG' partn_numb = '0000001000' )
TO lt_order_partners.
APPEND VALUE #( itm_number = '000010' material = 'MAT-BOLT-01'
target_qty = 5 ) TO lt_order_items.
CALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2'
EXPORTING order_header_in = ls_order_header
order_header_inx = ls_header_flag
IMPORTING salesdocument = lv_salesdoc
TABLES order_items_in = lt_order_items
order_partners = lt_order_partners
return = lt_return.
WRITE: / '생성된 오더:', lv_salesdoc.
" 여기서 프로그램 종료 → 커밋 없음 → 오더 번호는 찍히지만 VBAK에는 없음!
해결책은 단 세 줄입니다. BAPI 호출 직후에 추가하세요.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING wait = 'X'. " 업데이트 태스크 완료까지 대기
wait = 'X'는 COMMIT WORK AND WAIT에 해당합니다. 커밋 직후 SELECT로 오더를 조회해야 한다면 필수입니다. 비동기 커밋(wait 공백)은 업데이트가 끝나기 전에 다음 줄이 실행되므로 조회 시 데이터가 아직 없을 수 있습니다.
2단계 — 실무 시나리오: RETURN 검사와 에러 로깅, 그리고 롤백
실무에서는 "무조건 커밋"도 위험합니다. RETURN에 에러(E/A 타입)가 있는데 커밋해 버리면, 같은 LUW에 등록된 다른 정상 요청까지 반쯤 확정되는 어정쩡한 상태가 생길 수 있습니다. 표준 패턴은 검사 → 성공이면 커밋, 실패면 명시적 롤백입니다.
DATA(lv_has_error) = abap_false.
LOOP AT lt_return INTO DATA(ls_return)
WHERE type = 'E' OR type = 'A' OR type = 'X'.
lv_has_error = abap_true.
" 애플리케이션 로그(BAL)에 남기거나 최소한 메시지 수집
MESSAGE ID ls_return-id TYPE 'S'
NUMBER ls_return-number
WITH ls_return-message_v1 ls_return-message_v2
ls_return-message_v3 ls_return-message_v4
DISPLAY LIKE ls_return-type.
ENDLOOP.
IF lv_has_error = abap_true.
" 등록된 업데이트 요청을 깨끗이 폐기하고 채번/락 상태 정리
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
RETURN.
ENDIF.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING wait = 'X'
TABLES return = lt_return. " 커밋 자체의 에러도 확인 가능
IF lv_salesdoc IS INITIAL.
" 오더 번호가 비어 있으면 커밋 여부와 무관하게 실패로 처리
MESSAGE '오더 생성 실패 — RETURN 로그를 확인하세요' TYPE 'E'.
ENDIF.
포인트 두 가지. 첫째, RETURN이 비어 있어도 성공인 BAPI가 많습니다(성공 메시지를 안 넣는 경우). 따라서 "E/A/X가 없으면 성공"으로 판단하는 것이 일반적입니다. 둘째, 실패 시 롤백을 생략하면 이전 요청 잔재가 세션에 남아 다음 BAPI 호출 결과와 섞일 수 있으므로, 실패 분기에서 BAPI_TRANSACTION_ROLLBACK을 호출하는 습관이 권장됩니다.
3단계 — 프로덕션: 대량 처리 성능, 사전 검증, 권한
수백 건을 반복 생성하는 배치라면 커밋 전략이 성능을 좌우합니다. 매 건마다 wait = 'X' 커밋을 하면 업데이트 태스크 대기 시간이 누적됩니다. 반대로 전체를 한 번에 커밋하면 한 건의 실패가 전체 롤백 판단을 복잡하게 만듭니다. 절충안은 건별 커밋 + 실패 건 롤백 + 결과 집계입니다.
LOOP AT lt_upload_orders INTO DATA(ls_req).
CLEAR: lt_return, lv_salesdoc.
CALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2'
EXPORTING order_header_in = ls_req-header
order_header_inx = ls_req-headerx
IMPORTING salesdocument = lv_salesdoc
TABLES order_items_in = ls_req-items
order_partners = ls_req-partners
return = lt_return.
IF line_exists( lt_return[ type = 'E' ] ).
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
APPEND VALUE #( key = ls_req-ext_key status = 'FAIL'
msgs = lt_return ) TO lt_result_log.
CONTINUE.
ENDIF.
" 대량 처리: 후속 조회가 없다면 wait 생략으로 처리량 확보 가능
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING wait = COND #( WHEN ls_req-need_verify = abap_true
THEN 'X' ELSE space ).
APPEND VALUE #( key = ls_req-ext_key status = 'OK'
vbeln = lv_salesdoc ) TO lt_result_log.
ENDLOOP.
프로덕션 체크리스트:
- 사전 검증: 일부 BAPI는 TESTRUN 파라미터나 시뮬레이션 BAPI(예: BAPI_SALESORDER_SIMULATE)를 제공합니다. 커밋 전에 검증만 돌려 실패 건을 걸러내면 롤백 비용이 줄어듭니다.
- 보안: BAPI는 호출자의 권한으로 실행됩니다. 배치 사용자에게 V_VBAK_VKO 같은 판매 오더 권한 오브젝트가 있는지 확인하고, RFC 시나리오라면 S_RFC 최소 권한 원칙을 적용하세요.
- 테스트: 단위 테스트에서는 BAPI 호출부를 인터페이스로 감싸 테스트 더블로 격리하는 방식이 일반적입니다. 커밋이 들어간 코드를 ABAP Unit에서 직접 실행하면 테스트가 실제 데이터를 남기므로 주의가 필요합니다.
- 모니터링: 커밋했는데도 데이터가 없다면 SM13에서 업데이트 레코드 실패(V1 에러) 여부를 확인하세요. 커밋 성공과 업데이트 태스크 성공은 별개 단계입니다.
⚠️ 흔한 실수와 트러블슈팅 FAQ
Q1. SE37에서 단독 테스트하면 저장이 안 되는데 프로그램에서는 되는 이유는?
SE37 단건 실행은 함수 종료 후 암묵적 롤백이 일어나는 경우가 많습니다. 테스트 시퀀스 기능으로 BAPI → BAPI_TRANSACTION_COMMIT을 연속 실행하면 같은 세션에서 커밋되어 저장을 확인할 수 있습니다.
Q2. RETURN이 완전히 비어 있는데 실패일 수 있나요?
네. RETURN이 비어 있어도 커밋을 안 했다면 저장은 안 됩니다. 반대로 RETURN이 비어 있고 커밋도 했는데 데이터가 없다면 SM13의 업데이트 오류(익스프레스 문서 "업데이트 종료됨" 알림)를 확인하세요. V1 업데이트 실패는 BAPI RETURN에 나타나지 않습니다.
Q3. COMMIT WORK와 BAPI_TRANSACTION_COMMIT 중 뭘 써야 하나요?
로컬 프로그램에서는 사실상 동등하지만, BAPI_TRANSACTION_COMMIT은 RFC로도 호출 가능하고 BUFFER_REFRESH_ALL 등 후처리를 함께 수행하므로 BAPI와 짝지어 쓰는 것이 일반적으로 권장됩니다. 외부 시스템(RFC/레거시 연동)에서는 반드시 같은 커넥션으로 커밋 BAPI를 호출해야 합니다.
Q4. 커밋 전에 SELECT로 방금 만든 오더를 조회하면?
조회되지 않습니다. 업데이트 요청은 아직 메모리에만 있기 때문입니다. 커밋 후 조회하려면 wait = 'X'가 필수입니다. 또한 루프 안에서 다른 BAPI를 커밋 없이 연달아 호출하면 앞 호출의 잔재가 섞이는 부작용이 있으니, 호출 단위마다 커밋 또는 롤백으로 LUW를 닫아 주세요.
🚀 이후에 살펴볼 주제
커밋의 원리를 이해했다면 자연스럽게 이어지는 주제들이 있습니다. 업데이트 태스크 심화(V1/V2 차이, PERFORM ON COMMIT), BAPI 확장(EXTENSIONIN과 구조 확장), enqueue 락과 BAPI의 관계, 그리고 S/4HANA 세대의 대안인 RAP(ABAP RESTful Application Programming Model)에서 프레임워크가 LUW를 대신 관리하는 방식을 비교해 보면, 왜 클래식 BAPI가 커밋을 호출자에게 맡겼는지 더 깊이 이해할 수 있습니다.
📚 더 읽어볼 문서와 링크
댓글 0
아직 댓글이 없습니다.