FI 전표 인터페이스 실전 가이드 BAPI_ACC_DOCUMENT_POST vs OData

외부 시스템에서 SAP로 회계 전표를 밀어 넣는 작업. 요건만 보면 단순하다. “AP 인보이스 데이터를 받아서 전표를 생성한다.” 그런데 실제로 붙여보면 상황이 다르다.

차변과 대변이 0.01원 차이로 안 맞고, 세금 라인을 넣으면 갑자기 dynpro 오류가 뜨고, 로컬에서 되던 게 QAS에서는 Field Status 오류를 내뱉는다. 표준 문서에는 파라미터 목록만 나열되어 있을 뿐, 왜 이 순서로 넣어야 하는지, 왜 이 필드를 비워야 하는지는 어디에도 없다.

이 글은 FI 전표 인터페이스를 실제로 개발하면서 부딪히는 지점들을 정리한 것이다. BAPI 방식과 OData 방식을 모두 다루고, 마지막에 어떤 상황에서 무엇을 선택할지 정리한다.

1. 두 가지 길

FI 전표를 외부에서 생성하는 경로는 크게 둘이다.

구분BAPI (RFC)OData (REST)
호출 방식RFC — JCo, .NET Connector, SAP AdapterHTTP POST + JSON
트랜잭션 제어명시적 COMMIT 필요 (BAPI_TRANSACTION_COMMIT)요청 단위 자동 커밋
방화벽RFC 포트(33xx) 개방 필요HTTPS(443) — 통과 쉬움
대량 처리배열 전송 + 반복 호출, 빠름$batch로 묶어야 함, 상대적으로 느림
에러 응답RETURN 테이블 (BAPIRET2)HTTP 상태코드 + 에러 JSON
Clean Core레거시 취급, S/4HANA에서 유지되나 권장은 아님권장 방식
개발 난이도구조 이해가 어려움, 자료는 많음구조는 쉬움, 커스터마이징이 까다로움

정리하면 이렇다. 레거시 연동이나 대량 배치는 BAPI, 신규 개발과 클라우드 연동은 OData. 다만 현실에서는 세 번째 선택지가 가장 많이 쓰인다. 커스텀 OData 서비스로 BAPI를 감싸는 방식이다. 이건 4장에서 따로 다룬다.

2. BAPI_ACC_DOCUMENT_POST 구조 해부

2-1. 파라미터 전체 구조

BAPI_ACC_DOCUMENT_POST
├─ DOCUMENTHEADER   (BAPIACHE09)  — 전표 헤더, 단일 구조
├─ ACCOUNTGL        (BAPIACGL09)  — G/L 라인 (테이블)
├─ ACCOUNTPAYABLE   (BAPIACAP09)  — 매입처(Vendor) 라인
├─ ACCOUNTRECEIVABLE(BAPIACAR09)  — 매출처(Customer) 라인
├─ ACCOUNTTAX       (BAPIACTX09)  — 세금 라인
├─ CURRENCYAMOUNT   (BAPIACCR09)  — 모든 금액이 여기 들어간다
├─ EXTENSION2       (BAPIPAREX)   — 커스텀 필드 확장
└─ RETURN           (BAPIRET2)    — 결과 메시지

여기서 처음 접하는 사람이 가장 많이 실수하는 부분이 CURRENCYAMOUNT다. G/L 라인 테이블에는 금액 필드가 아예 없다. 계정번호, 원가센터, 텍스트 같은 속성만 들어간다. 금액은 오로지 CURRENCYAMOUNT 테이블에만 존재하고, ITEMNO_ACC로 각 라인과 연결된다.

2-2. ITEMNO_ACC — 이 글에서 가장 중요한 부분

ITEMNO_ACC는 전표 라인의 일련번호다. 규칙은 단순해 보이지만 어기면 반드시 터진다.

  1. 모든 라인 테이블을 통틀어 유일해야 한다. G/L에 1,2를 썼으면 Vendor는 3부터 시작한다. 테이블별로 1부터 다시 매기면 안 된다.
  2. 1부터 연속이어야 한다. 중간에 번호가 비면 오류다.
  3. 순서가 의미를 갖는다.

세 번째가 핵심이다. AP 인보이스를 예로 들면, 안정적으로 동작하는 순서는 이렇다.

1. G/L 라인    (비용/자산 계정, 차변)
2. TAX 라인    (매입세액, 차변)
3. VENDOR 라인 (매입처, 대변)

G/L → TAX → VENDOR 순서다. VENDOR 라인을 1번으로 올리거나 TAX를 마지막에 두면, 되는 시스템도 있고 안 되는 시스템도 있다. 세금 자동 계산 로직과 Posting Key 결정 순서가 맞물려 있어서 그렇다. 표준 문서에는 이 순서 규칙이 명시되어 있지 않지만, 실무에서는 이 순서를 지키는 것이 안전하다.

2-3. 차변·대변 부호 규칙

CURRENCYAMOUNT-AMT_DOCCUR의 부호가 차대변을 결정한다.

  • 양수(+) = 차변(Debit)
  • 음수(−) = 대변(Credit)

AP 인보이스 110,000원(공급가 100,000 + 세액 10,000)의 경우:

ITEMNO_ACC구분계정AMT_DOCCUR
1G/L비용계정+100,000
2TAX부가세대급금+10,000
3VENDOR매입처−110,000
합계0

합계가 반드시 0이어야 한다. 이게 안 맞으면 F5 702 (Balance in transaction currency) 오류가 난다. 실무에서 가장 자주 만나는 오류이고, 원인은 대부분 세액 계산 방식의 불일치다. 이건 5장에서 자세히 다룬다.

2-4. 실제 호출 코드

abap

DATA: ls_header    TYPE bapiache09,
      lt_gl        TYPE TABLE OF bapiacgl09,
      lt_ap        TYPE TABLE OF bapiacap09,
      lt_tax       TYPE TABLE OF bapiactx09,
      lt_amount    TYPE TABLE OF bapiaccr09,
      lt_return    TYPE TABLE OF bapiret2,
      lv_obj_key   TYPE bapiache09-obj_key.

"=== 1. 헤더 ===
ls_header-obj_type   = 'BKPFF'.        " 참조 트랜잭션
ls_header-obj_key    = lv_if_key.      " 멱등성 키 (6장 참조)
ls_header-obj_sys    = 'ITGCLNT100'.   " 논리 시스템
ls_header-bus_act    = 'RFBU'.         " FI 전기
ls_header-username   = sy-uname.
ls_header-header_txt = '외부 인터페이스 전표'.
ls_header-comp_code  = '1000'.
ls_header-doc_date   = '20260801'.    " 증빙일 (BLDAT)
ls_header-pstng_date = '20260801'.    " 전기일 (BUDAT)
ls_header-doc_type   = 'KR'.          " 전표유형(매입처인보이스)
ls_header-ref_doc_no = lv_invoice_no. " 참조번호 (XBLNR)

"=== 2. G/L 라인 (ITEMNO_ACC = 1) ===
APPEND VALUE #( itemno_acc = '0000000001'
                gl_account = '0051010100'    " 10자리 우측정렬 필수
                item_text  = '사무용품 구입'
                costcenter = '0000100010'
                tax_code   = 'V1'
                alloc_nmbr = lv_alloc ) TO lt_gl.

APPEND VALUE #( itemno_acc   = '0000000001'
                currency     = 'KRW'
                amt_doccur   = '100000'      " 차변 = 양수
              ) TO lt_amount.

"=== 3. TAX 라인 (ITEMNO_ACC = 2) ===
APPEND VALUE #( itemno_acc = '0000000002'
                gl_account = '0013500100'    " 부가세대급금
                tax_code   = 'V1'
                acct_key   = 'VST'           " 계정결정키
                cond_key   = 'MWVS'          " 조건유형
              ) TO lt_tax.

APPEND VALUE #( itemno_acc   = '0000000002'
                currency     = 'KRW'
                amt_doccur   = '10000'       " 세액
                amt_base     = '100000'      " 과세표준 반드시 세팅
              ) TO lt_amount.

"=== 4. VENDOR 라인 (ITEMNO_ACC = 3) ===
APPEND VALUE #( itemno_acc = '0000000003'
                vendor_no  = '0000100234'
                pmnttrms   = 'ZB01'          " 지급조건
                bline_date = '20260801'      " 기산일
                pymt_meth  = 'T'
                item_text  = '8월 사무용품'
              ) TO lt_ap.

APPEND VALUE #( itemno_acc   = '0000000003'
                currency     = 'KRW'
                amt_doccur   = '110000-'     " 대변 = 음수
              ) TO lt_amount.

"=== 5. CHECK 먼저 호출 (권장) ===
CALL FUNCTION 'BAPI_ACC_DOCUMENT_CHECK'
  EXPORTING  documentheader = ls_header
  TABLES     accountgl      = lt_gl
             accountpayable = lt_ap
             accounttax     = lt_tax
             currencyamount = lt_amount
             return         = lt_return.

IF line_exists( lt_return[ type = 'E' ] ) OR
   line_exists( lt_return[ type = 'A' ] ).
  " 검증 실패 → POST 호출하지 않고 종료
  RETURN.
ENDIF.

CLEAR lt_return.

"=== 6. POST ===
CALL FUNCTION 'BAPI_ACC_DOCUMENT_POST'
  EXPORTING  documentheader = ls_header
  IMPORTING  obj_key        = lv_obj_key
  TABLES     accountgl      = lt_gl
             accountpayable = lt_ap
             accounttax     = lt_tax
             currencyamount = lt_amount
             return         = lt_return.

"=== 7. 커밋 또는 롤백 ===
IF line_exists( lt_return[ type = 'E' ] ) OR
   line_exists( lt_return[ type = 'A' ] ).
  CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ELSE.
  CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
    EXPORTING wait = 'X'.                    "동기 커밋
  " OBJ_KEY = 전표번호(10) + 회사코드(4) + 회계연도(4)
  DATA(lv_belnr) = lv_obj_key(10).
  DATA(lv_bukrs) = lv_obj_key+10(4).
  DATA(lv_gjahr) = lv_obj_key+14(4).
ENDIF.

2-5. 놓치기 쉬운 세 가지

BAPI는 자동으로 커밋하지 않는다. BAPI_TRANSACTION_COMMIT을 호출하지 않으면 전표번호는 받았는데 DB에는 없는 상황이 벌어진다. WAIT = 'X'를 주면 update task 완료까지 기다리므로, 커밋 직후 조회해야 하는 경우 필수다.

CHECK를 먼저 부르는 습관을 들이자. BAPI_ACC_DOCUMENT_CHECK는 실제 전기 없이 검증만 수행한다. POST에서 실패하면 롤백해야 하지만, CHECK로 걸러내면 애초에 트랜잭션을 열지 않는다. 성능 부담도 크지 않다.

계정번호는 10자리 우측정렬. 51010100이 아니라 0051010100이다. 매입처/매출처 코드도 마찬가지다. ABAP 내부에서 호출하면 CONVERSION_EXIT_ALPHA_INPUT으로 변환하고, 외부 시스템에서 넘길 때는 아예 앞자리를 채워서 보내는 편이 안전하다.

3. OData 방식 — 표준 API 활용

S/4HANA에서는 표준 OData 서비스로 전표를 전기할 수 있다. 대표적인 것이 Journal Entry – Post 서비스다.

3-1. 기본 구조

표준 서비스는 Deep Insert 구조를 쓴다. 헤더 엔티티 하나에 라인 아이템들을 중첩 배열로 담아 한 번에 POST한다.

POST /sap/opu/odata/sap/API_JOURNALENTRY_SRV/A_JournalEntryCreateRequest

json

{
  "CompanyCode": "1000",
  "DocumentReferenceID": "INV-20260801-001",
  "CreatedByUser": "ITGUSER",
  "BusinessTransactionType": "RFBU",
  "AccountingDocumentType": "KR",
  "DocumentHeaderText": "외부 인터페이스 전표",
  "PostingDate": "/Date(1785196800000)/",
  "DocumentDate": "/Date(1785196800000)/",

  "_GLItems": [
    {
      "ReferenceDocumentItem": "1",
      "GLAccount": "0051010100",
      "AmountInTransactionCurrency": {
        "value": "100000.00",
        "currency": "KRW"
      },
      "DebitCreditCode": "S",
      "TaxCode": "V1",
      "CostCenter": "0000100010",
      "DocumentItemText": "사무용품 구입"
    }
  ],

  "_APItems": [
    {
      "ReferenceDocumentItem": "3",
      "Supplier": "0000100234",
      "AmountInTransactionCurrency": {
        "value": "-110000.00",
        "currency": "KRW"
      },
      "DebitCreditCode": "H",
      "PaymentTerms": "ZB01",
      "DocumentItemText": "8월 사무용품"
    }
  ],

  "_TaxItems": [
    {
      "ReferenceDocumentItem": "2",
      "TaxCode": "V1",
      "TaxAmount": { "value": "10000.00", "currency": "KRW" },
      "TaxBaseAmount": { "value": "100000.00", "currency": "KRW" }
    }
  ]
}

주의: 엔티티셋과 네비게이션 프로퍼티 이름은 S/4HANA 릴리즈에 따라 다르다. 반드시 해당 시스템의 $metadata를 직접 조회해 확인하자. GET /sap/opu/odata/sap/API_JOURNALENTRY_SRV/$metadata

3-2. CSRF 토큰 처리

OData 쓰기 요청에는 CSRF 토큰이 필요하다. 외부 시스템에서 연동할 때 첫 번째 관문이다.

① 토큰 발급
GET /sap/opu/odata/sap/API_JOURNALENTRY_SRV/
Headers: x-csrf-token: Fetch

→ 응답 헤더에서 x-csrf-token 값과 Set-Cookie 획득

② 실제 요청
POST /sap/opu/odata/sap/API_JOURNALENTRY_SRV/A_JournalEntryCreateRequest
Headers:
  x-csrf-token: <①에서 받은 값>
  Cookie: <①에서 받은 세션 쿠키>
  Content-Type: application/json

쿠키를 같이 보내지 않으면 토큰이 있어도 403이 난다. 세션과 토큰이 쌍으로 묶여 있기 때문이다. Java에서 RestTemplate이나 HttpClient를 쓴다면 쿠키 스토어를 유지하는 설정이 필요하다.

3-3. 대량 처리는 $batch

건당 HTTP 왕복은 느리다. 100건이면 100번의 TCP 핸드셰이크와 인증이 발생한다. $batch로 묶으면 한 번의 요청으로 처리된다.

POST /sap/opu/odata/sap/API_JOURNALENTRY_SRV/$batch
Content-Type: multipart/mixed; boundary=batch_001

changeset으로 묶으면 그 안의 요청들이 하나의 LUW로 처리된다. 즉 하나가 실패하면 전부 롤백된다. 전표 단위로 원자성이 필요하면 changeset, 건별 독립 처리가 필요하면 개별 요청으로 나눈다. 이 설계 판단은 업무 요건에 따라 갈린다.

4. 현실적인 선택 — 커스텀 OData로 BAPI 감싸기

표준 API는 깔끔하지만 한계가 있다. 커스텀 필드가 있거나, 전표 생성 전후에 자체 검증·매핑 로직이 필요하거나, 표준 API가 지원하지 않는 시나리오(특별 G/L, 특정 전표유형 조합)를 다뤄야 하면 답이 안 나온다.

그래서 SI 현장에서 가장 흔한 구조는 이렇다.

[외부 시스템]
     │ HTTPS + JSON
     ▼
[커스텀 OData 서비스]  ← SEGW 또는 RAP으로 개발
     │  ① 페이로드 파싱
     │  ② 마스터 검증 / 코드 매핑
     │  ③ ITEMNO_ACC 채번
     │  ④ 차대변 검증
     ▼
[BAPI_ACC_DOCUMENT_POST]
     │
     ▼
[전표 생성 + IF 로그 테이블 기록]

이 구조의 장점은 명확하다.

  • 외부 시스템은 HTTPS/JSON만 알면 된다. RFC 포트 개방이 불필요하다
  • 검증 로직과 코드 매핑을 SAP 안에 둘 수 있다
  • BAPI의 모든 기능(특별 G/L, EXTENSION2 등)을 그대로 쓸 수 있다
  • IF 로그를 남겨 재처리 기반을 만들 수 있다

송신 측 페이로드 생성 예시

송신 시스템에서 JSON을 만들 때는 DB 단에서 조립하는 방식이 관리하기 편하다. PostgreSQL 기준 예시다.

sql

CREATE OR REPLACE FUNCTION fn_build_fi_payload(p_if_key VARCHAR)
RETURNS JSONB AS $$
DECLARE
    v_result JSONB;
BEGIN
    SELECT jsonb_build_object(
        'IF_KEY',      h.if_key,
        'COMP_CODE',   h.comp_code,
        'DOC_TYPE',    h.doc_type,
        'DOC_DATE',    to_char(h.doc_date,  'YYYYMMDD'),
        'PSTNG_DATE',  to_char(h.pstng_date,'YYYYMMDD'),
        'REF_DOC_NO',  h.ref_doc_no,
        'ITEMS', (
            SELECT jsonb_agg(
                jsonb_build_object(
                    'ITEMNO_ACC', lpad(d.itemno_acc::text, 10, '0'),
                    'LINE_TYPE',  d.line_type,      -- GL / TAX / VENDOR
                    'GL_ACCOUNT', lpad(d.gl_account, 10, '0'),
                    'VENDOR_NO',  lpad(coalesce(d.vendor_no,''), 10, '0'),
                    'SP_GL_IND',  coalesce(d.sp_gl_ind, ''),
                    'TAX_CODE',   coalesce(d.tax_code, ''),
                    'AMT_DOCCUR', to_char(d.amt_doccur, 'FM9999999999990.00'),
                    'AMT_BASE',   to_char(coalesce(d.amt_base,0), 'FM9999999999990.00'),
                    'CURRENCY',   d.currency,
                    'ITEM_TEXT',  coalesce(d.item_text, ''),
                    'COSTCENTER', lpad(coalesce(d.costcenter,''), 10, '0')
                )
                ORDER BY d.itemno_acc          -- ★ 순서 보장
            )
            FROM if_fi_doc_item d
            WHERE d.if_key = h.if_key
        )
    )
    INTO v_result
    FROM if_fi_doc_head h
    WHERE h.if_key = p_if_key;

    RETURN v_result;
END;
$$ LANGUAGE plpgsql;

핵심은 ORDER BY d.itemno_acc다. jsonb_agg는 정렬을 보장하지 않으므로 반드시 명시해야 한다. 라인 순서가 뒤섞이면 2-2에서 설명한 순서 규칙이 깨진다.

그리고 itemno_acc 자체를 채번하는 로직은 별도로 관리한다.

sql

-- 라인 타입별 정렬 우선순위를 부여해 채번
UPDATE if_fi_doc_item d
SET itemno_acc = s.seq
FROM (
    SELECT ctid,
           row_number() OVER (
               PARTITION BY if_key
               ORDER BY CASE line_type
                          WHEN 'GL'     THEN 1
                          WHEN 'TAX'    THEN 2
                          WHEN 'VENDOR' THEN 3
                          ELSE 9
                        END,
                        line_no
           ) AS seq
    FROM if_fi_doc_item
    WHERE if_key = p_if_key
) s
WHERE d.ctid = s.ctid;

5. 실무에서 반드시 만나는 오류 10선

① 차대변 불일치 — F5 702 Balance in transaction currency

가장 흔하다. 원인은 대부분 셋 중 하나다.

  • 세액 계산 방식 불일치: 송신 시스템은 반올림, SAP는 절사 — 1원 차이가 난다
  • 원화 소수점 처리: KRW는 소수점이 없는 통화다. BAPI에 1000.00을 넘기면 내부적으로 100000으로 해석될 수 있다. 통화별 소수 자릿수(TCURX) 확인이 필요하다
  • 부호 누락: 대변 라인에 마이너스를 안 붙였다

대응: 페이로드를 만드는 시점에 DB에서 먼저 검증한다.

sql

-- 전송 전 차대변 검증
SELECT if_key, SUM(amt_doccur) AS diff
FROM if_fi_doc_item
WHERE if_key = p_if_key
GROUP BY if_key
HAVING SUM(amt_doccur) <> 0;

SAP까지 갔다가 오류를 받는 것보다, 보내기 전에 잡는 편이 디버깅이 훨씬 쉽다.

② 세금 자동 계산 vs 직접 입력

BAPI에서 세금 처리 방식은 두 가지다.

(A) 직접 입력 — ACCOUNTTAX에 세액을 명시하고 AMT_BASE(과세표준)까지 채운다. 송신 시스템의 세액을 그대로 쓴다. 원천 데이터가 이미 확정된 인터페이스에는 이 방식이 맞다.

(B) 자동 계산 — ACCOUNTTAX를 아예 넘기지 않고, G/L 라인에 세금코드만 준다. SAP가 세율을 적용해 세금 라인을 자동 생성한다.

둘을 섞으면 안 된다. 세금 라인을 넣으면서 자동 계산도 기대하면 세액이 두 번 계산되어 잔액이 안 맞는다. 인터페이스 설계 초기에 방식을 확정하고 문서화해두자.

③ Dynpro 오류 — SAPLF040

메시지 자체는 불친절하다. “화면 SAPLF040 …에 필드가 없습니다” 정도로 나온다. 실제 원인은 거의 항상 필드 상태(Field Status) 충돌이다.

  • 계정의 Field Status Group(FSG)에서 필수(Required)로 지정된 필드를 안 보냈다
  • 반대로 **숨김(Suppress)**으로 지정된 필드에 값을 보냈다
  • Posting Key의 필드 상태와 FSG가 충돌한다

진단 방법: OBC4에서 해당 계정의 FSG를 열어 필수/선택/숨김 상태를 확인한다. 특히 원가센터, 수주번호, 배정번호(ALLOC_NMBR), 텍스트 필드를 중점적으로 본다. 값이 없는 필드는 빈 문자열이 아니라 아예 넘기지 않는 것이 안전하다.

④ 특별 G/L (선급금·보증금 등)

선급금 계정으로 전기하려면 매입처 라인에 SP_GL_IND를 설정한다.

abap

APPEND VALUE #( itemno_acc = '0000000002'
                vendor_no  = '0000100234'
                sp_gl_ind  = 'A'          " 선급금
              ) TO lt_ap.

여기서 중요한 점이 있다. 특별 G/L 지시자를 쓰면 대체계정으로 전기되므로, G/L 계정번호를 직접 넣으면 안 된다. 계정은 특별 G/L 설정(OBYR)에서 결정된다. 계정을 같이 넘기면 충돌이 발생한다.

또한 특별 G/L 라인은 세금 처리와 궁합이 까다롭다. 선급금에 세금코드를 붙여야 하는 요건이라면, 표준 설정으로 되는지 먼저 검증하고 안 되면 별도 전표로 분리하는 설계를 검토하자.

⑤ 회계기간 마감 — F5 201

전기일이 속한 기간이 닫혀 있으면 전기가 안 된다. 배치가 자정을 넘어 돌면서 전기일이 다음 달로 넘어가 터지는 사고가 실제로 자주 발생한다.

대응: 전기일을 시스템 날짜로 잡지 말고 업무 기준일을 명시적으로 받는다. 그리고 전송 전에 기간 상태를 확인하는 검증 로직을 둔다.

⑥ 참조번호 중복 — 이중 전기

REF_DOC_NO(XBLNR)만으로는 중복이 막히지 않는다. 회사코드별 중복 체크 설정을 켜야 하는데, 그마저도 조합 조건이 제한적이다.

대응: 6장의 멱등성 설계를 참고하자. 인터페이스 레벨에서 막는 것이 확실하다.

⑦ 통화·환율

외화 전표는 AMT_DOCCUR(전표통화)와 AMT_LOCCUR(현지통화)를 함께 넘기거나, EXCH_RATE를 주고 SAP가 환산하게 한다. 둘 다 안 주면 당일 환율(TCURR)로 자동 환산되는데, 송신 시스템의 환율과 다르면 잔액이 틀어진다.

원천 시스템에서 이미 환산을 마쳤다면 현지통화 금액까지 명시적으로 넘기는 편이 안전하다.

⑧ 매입처 마스터 미생성

매입처가 SAP에 없으면 당연히 실패한다. 문제는 마스터 인터페이스와 전표 인터페이스의 순서다. 야간 배치에서 전표가 마스터보다 먼저 돌면 매번 실패한다.

대응: 배치 스케줄에 선후 관계를 명시하고, 전표 인터페이스에 마스터 존재 여부 사전 검증을 넣는다. 실패 시 오류가 아니라 “대기” 상태로 두고 다음 회차에 재시도하는 설계가 유용하다.

⑨ 원가객체 필수 여부

비용 계정(P&L)은 대부분 원가센터나 내부오더 같은 원가객체(CO Object)가 필수다. 안 넣으면 KI 235 계열 오류가 난다. 반대로 재무상태표 계정에 원가센터를 넣으면 그것도 오류다.

대응: 계정 마스터의 원가요소 여부에 따라 원가객체 필수/금지가 갈린다. 매핑 테이블에 이 정보를 미리 담아두고 페이로드 생성 시 분기한다.

⑩ 텍스트 필드 길이 초과

ITEM_TEXT는 50자, HEADER_TXT는 25자다. 한글은 인코딩에 따라 바이트가 달라진다. 초과분은 잘려서 들어가거나 오류가 난다.

대응: 송신 측에서 미리 자른다. PostgreSQL이면 left(item_text, 25) 같은 처리를 페이로드 생성 함수에 넣어두자.

6. 인터페이스 아키텍처 설계

기능이 동작하는 것과 운영에서 버티는 것은 다른 문제다. 설계 단계에서 챙겨야 할 것들이다.

6-1. 스테이징 테이블

외부 데이터를 바로 BAPI에 밀어 넣지 말고, 중간 테이블을 둔다.

sql

-- 헤더
CREATE TABLE if_fi_doc_head (
    if_key        VARCHAR(30)  PRIMARY KEY,  -- 인터페이스 고유키
    comp_code     VARCHAR(4)   NOT NULL,
    doc_type      VARCHAR(2)   NOT NULL,
    doc_date      DATE         NOT NULL,
    pstng_date    DATE         NOT NULL,
    ref_doc_no    VARCHAR(16),
    -- 처리 상태
    if_status     VARCHAR(1)   DEFAULT 'R',  -- R:준비 S:전송 C:완료 E:오류
    retry_cnt     INTEGER      DEFAULT 0,
    -- SAP 결과
    sap_belnr     VARCHAR(10),
    sap_bukrs     VARCHAR(4),
    sap_gjahr     VARCHAR(4),
    err_msg       TEXT,
    send_dt       TIMESTAMP,
    create_dt     TIMESTAMP    DEFAULT now()
);

CREATE INDEX ix_if_fi_head_status
    ON if_fi_doc_head (if_status, create_dt);

상태 코드를 두는 이유는 재처리 때문이다. 오류 건만 골라 다시 돌릴 수 있어야 운영이 가능하다.

6-2. 멱등성 — 이중 전기 방지

인터페이스에서 가장 무서운 사고는 같은 전표가 두 번 생기는 것이다. 네트워크 타임아웃으로 응답을 못 받았는데 SAP에서는 이미 전기가 끝난 경우가 대표적이다.

해결책은 OBJ_TYPE / OBJ_KEY / OBJ_SYS를 활용하는 것이다. 이 세 값은 BKPF의 AWTYP / AWKEY / AWSYS에 그대로 저장된다.

abap

" 전기 전 중복 확인
SELECT SINGLE belnr, bukrs, gjahr
  FROM bkpf
  INTO @DATA(ls_exist)
  WHERE awtyp = 'BKPFF'
    AND awkey = @lv_if_key
    AND awsys = 'ITGCLNT100'.

IF sy-subrc = 0.
  " 이미 전기됨 → 전표번호만 반환하고 종료
  rv_belnr = ls_exist-belnr.
  RETURN.
ENDIF.

OBJ_KEY에 인터페이스 고유키를 넣어두면, 재전송이 와도 안전하게 막을 수 있다. 이 설계 하나로 운영 중 사고의 상당 부분이 예방된다.

6-3. 역분개 설계

전표를 잘못 보냈을 때 되돌리는 경로도 처음부터 설계해야 한다. 역분개는 BAPI_ACC_DOCUMENT_REV_POST를 쓴다.

abap

CALL FUNCTION 'BAPI_ACC_DOCUMENT_REV_POST'
  EXPORTING  reversal = ls_reversal   " 원전표번호, 회사코드, 연도, 역분개사유
             bus_act  = 'RFBU'
  IMPORTING  obj_key  = lv_rev_key
  TABLES     return   = lt_return.

주의할 점이 있다. 이미 지급이 이루어졌거나 청산(Clearing)된 전표는 역분개가 안 된다. 청산 취소를 먼저 해야 한다. 따라서 역분개 인터페이스는 사전 상태 검증을 반드시 포함해야 하고, 실패 시 사용자에게 명확한 사유를 돌려줘야 한다.

역분개 이력도 스테이징 테이블에 남긴다. 원전표와 역분개전표의 연결 관계를 추적할 수 없으면 나중에 정산이 불가능하다.

6-4. 로그와 모니터링

운영에서 실제로 필요한 것은 이런 질문에 답할 수 있는 로그다.

  • 어제 몇 건 보냈고 몇 건 실패했나
  • 실패한 건의 사유가 무엇인가
  • 같은 사유로 반복 실패하는 건이 있나
  • 며칠째 처리 안 된 건이 있나

요청 페이로드와 응답 메시지를 원문 그대로 남기자. 저장 공간은 저렴하지만, 로그 없이 하는 장애 분석은 매우 비싸다.

7. 그래서 뭘 쓸 것인가

상황권장
온프레미스 ECC/S4, 대량 배치BAPI 직접 호출
S/4HANA Public Cloud표준 OData (BAPI 사용 불가)
클라우드/외부 시스템 연동, 방화벽 제약OData
커스텀 필드·특수 시나리오 포함커스텀 OData → BAPI 래핑
신규 개발, Clean Core 준수 필요OData (표준 우선, 부족하면 RAP 기반 커스텀)

현장에서 가장 무난한 답은 커스텀 OData로 BAPI를 감싸는 구조다. 외부에는 표준적인 HTTP 인터페이스를 제공하면서, 내부에서는 BAPI의 유연성을 온전히 쓸 수 있다. Clean Core 관점에서도 커스텀 로직이 확장 레이어에 격리되므로 나쁘지 않다.

끝으로

FI 전표 인터페이스는 SAP 연동 개발의 통과의례 같은 영역이다. 처음에는 파라미터 구조를 이해하는 데 시간을 쓰고, 그다음에는 오류 메시지를 해석하는 데 시간을 쓰고, 마지막에는 운영 안정성을 확보하는 데 시간을 쓴다.

돌아보면 가장 중요한 것은 세 가지다.

  1. ITEMNO_ACC의 순서와 유일성 — 여기서 대부분의 미스터리한 오류가 시작된다
  2. 보내기 전에 검증한다 — 차대변, 마스터 존재 여부, 기간 상태는 SAP에 가기 전에 확인할 수 있다
  3. 멱등성을 설계에 넣는다 — 이중 전기는 복구 비용이 가장 큰 사고다

BAPI든 OData든, 이 셋을 지키면 나머지는 설정 확인과 디버깅의 영역이다.

참고 자료

  • SE37에서 BAPI_ACC_DOCUMENT_POST 문서 — 파라미터별 설명이 생각보다 상세하다
  • BAPI 트랜잭션(BAPI) — 비즈니스 오브젝트 단위로 사용 가능한 BAPI 탐색
  • SAP API Business Hub — 표준 OData 서비스의 스펙과 샘플 페이로드
  • /$metadata 조회 — 결국 해당 시스템의 실제 스펙이 정답이다

댓글 달기

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

위로 스크롤