외부 시스템에서 SAP로 회계 전표를 밀어 넣는 작업. 요건만 보면 단순하다. “AP 인보이스 데이터를 받아서 전표를 생성한다.” 그런데 실제로 붙여보면 상황이 다르다.
차변과 대변이 0.01원 차이로 안 맞고, 세금 라인을 넣으면 갑자기 dynpro 오류가 뜨고, 로컬에서 되던 게 QAS에서는 Field Status 오류를 내뱉는다. 표준 문서에는 파라미터 목록만 나열되어 있을 뿐, 왜 이 순서로 넣어야 하는지, 왜 이 필드를 비워야 하는지는 어디에도 없다.
이 글은 FI 전표 인터페이스를 실제로 개발하면서 부딪히는 지점들을 정리한 것이다. BAPI 방식과 OData 방식을 모두 다루고, 마지막에 어떤 상황에서 무엇을 선택할지 정리한다.
1. 두 가지 길
FI 전표를 외부에서 생성하는 경로는 크게 둘이다.
| 구분 | BAPI (RFC) | OData (REST) |
|---|---|---|
| 호출 방식 | RFC — JCo, .NET Connector, SAP Adapter | HTTP 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는 전표 라인의 일련번호다. 규칙은 단순해 보이지만 어기면 반드시 터진다.
- 모든 라인 테이블을 통틀어 유일해야 한다. G/L에 1,2를 썼으면 Vendor는 3부터 시작한다. 테이블별로 1부터 다시 매기면 안 된다.
- 1부터 연속이어야 한다. 중간에 번호가 비면 오류다.
- 순서가 의미를 갖는다.
세 번째가 핵심이다. 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 |
|---|---|---|---|
| 1 | G/L | 비용계정 | +100,000 |
| 2 | TAX | 부가세대급금 | +10,000 |
| 3 | VENDOR | 매입처 | −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 연동 개발의 통과의례 같은 영역이다. 처음에는 파라미터 구조를 이해하는 데 시간을 쓰고, 그다음에는 오류 메시지를 해석하는 데 시간을 쓰고, 마지막에는 운영 안정성을 확보하는 데 시간을 쓴다.
돌아보면 가장 중요한 것은 세 가지다.
- ITEMNO_ACC의 순서와 유일성 — 여기서 대부분의 미스터리한 오류가 시작된다
- 보내기 전에 검증한다 — 차대변, 마스터 존재 여부, 기간 상태는 SAP에 가기 전에 확인할 수 있다
- 멱등성을 설계에 넣는다 — 이중 전기는 복구 비용이 가장 큰 사고다
BAPI든 OData든, 이 셋을 지키면 나머지는 설정 확인과 디버깅의 영역이다.
참고 자료
- SE37에서
BAPI_ACC_DOCUMENT_POST문서 — 파라미터별 설명이 생각보다 상세하다 - BAPI 트랜잭션(BAPI) — 비즈니스 오브젝트 단위로 사용 가능한 BAPI 탐색
- SAP API Business Hub — 표준 OData 서비스의 스펙과 샘플 페이로드
/$metadata조회 — 결국 해당 시스템의 실제 스펙이 정답이다