에러 핸들링 없이 iFlow를 운영하면 생기는 일
새벽 3시, 운영팀에 긴급 호출이 옵니다. "어제 들어온 구매발주 17건이 S/4HANA에 안 들어왔어요." 로그를 뒤져보니 SAP BTP Integration Suite의 iFlow는 분명 메시지를 수신했는데, 중간 매핑 단계에서 NullPointerException이 터지면서 메시지가 그대로 사라졌습니다. 재처리할 페이로드도 남아있지 않고, 송신 시스템은 이미 "성공" 응답을 받은 상태였습니다. 이런 상황은 "큰일"이라는 말로도 부족합니다.
이 글에서 다룰 핵심은 단순합니다. iFlow는 기본적으로 예외가 발생하면 메시지 처리를 중단합니다. Exception Subprocess를 명시적으로 설계하지 않으면 페이로드, 헤더, 에러 컨텍스트가 모두 휘발됩니다. 아래 항목을 모두 다룹니다.
- Exception Subprocess의 동작 원리와 BPMN 구조
- 구매발주(PO) 처리 시나리오 기반 Groovy 에러 핸들러
- JMS Dead Letter Queue를 활용한 메시지 손실 방지
- ${exception.message} 등 시스템 헤더로 컨텍스트 보존
- Mail/HTTP Adapter 기반 에러 알림 채널 연동
- 운영 검증 체크리스트 7가지
SAP Integration Suite iFlow Exception 처리 구조
SAP BTP Integration Suite(Cloud Integration, CPI 후속 서비스, 2026년 5월 기준 최신 에디션)에서 iFlow는 Camel 기반 런타임 위에서 동작합니다. 메시지가 Sender Adapter로 진입한 후 각 Step을 순차적으로 통과하는 흐름을 "정상 흐름(Main Process)"이라 부르고, 어떤 Step에서든 Exception이 throw되면 즉시 "예외 흐름(Exception Subprocess)"으로 제어가 전환됩니다.
비유하자면 정상 흐름은 고속도로 본선이고, Exception Subprocess는 사고 발생 시 진입하는 갓길과 견인 트럭 역할입니다. 갓길을 만들어 두지 않으면 사고 차량(에러 메시지)은 본선에서 그대로 폐기됩니다. CPI Worker Node가 Exception을 잡지 못하면 MPL(Message Processing Log) 상태는 FAILED로 남지만, 페이로드 자체는 일정 기간 후 자동 삭제되며 재시도도 일어나지 않는 것이 일반적입니다.
중요한 점은 Exception Subprocess가 iFlow당 여러 개 정의될 수 있고, 각 Subprocess는 자체적인 Local Process 스코프를 가진다는 것입니다. 메인 프로세스에서 사용하던 Exchange Property는 그대로 상속되지만, 일부 헤더는 예외 발생 시점에서 갱신되므로 캡처 순서를 주의해야 합니다.
Exception Subprocess 설계: 기본 BPMN 구성
Exception Subprocess의 최소 구성은 세 가지 요소입니다. Error Start Event, 처리 Step(스크립트/Send/매핑 등), Error End Event 또는 Escalation End Event입니다. End Event 종류에 따라 메인 프로세스에 에러를 다시 throw할지, 조용히 종료할지가 결정됩니다.
<!-- iFlow 내부 BPMN 표현 (개념 도식) -->
<subProcess id="ExceptionSubprocess_PO" triggeredByEvent="true">
<startEvent id="ErrorStart_PO">
<errorEventDefinition/>
</startEvent>
<sequenceFlow sourceRef="ErrorStart_PO" targetRef="CaptureContext_Script"/>
<scriptTask id="CaptureContext_Script" scriptFormat="groovy"/>
<sequenceFlow sourceRef="CaptureContext_Script" targetRef="SendToDLQ"/>
<serviceTask id="SendToDLQ" name="JMS Producer - DLQ"/>
<sequenceFlow sourceRef="SendToDLQ" targetRef="ErrorEnd_PO"/>
<endEvent id="ErrorEnd_PO">
<errorEventDefinition/>
</endEvent>
</subProcess>
Error End Event를 사용하면 MPL 상태가 FAILED로 표시되어 운영 모니터링에서 즉시 식별 가능합니다. 반대로 일반 End Event로 닫으면 COMPLETED로 표시되어 실제 비즈니스 에러가 묻혀버리는 안티패턴이 됩니다.
실전 예제: 구매발주 처리 iFlow의 에러 핸들러
실제 운영 환경에서 자주 마주치는 시나리오를 가정해봅니다. 외부 SRM 시스템이 REST API로 구매발주(PurchaseOrder) JSON을 전송하면 iFlow가 이를 IDoc으로 매핑하여 S/4HANA에 전달합니다. 매핑 단계에서 필수 필드 누락, S/4HANA 응답 타임아웃, 인증 실패 등 다양한 예외가 발생할 수 있습니다.
// Exception Subprocess 내부 Script Step
// 파일명: captureErrorContext.groovy
import com.sap.gateway.ip.core.customdev.util.Message
import groovy.json.JsonBuilder
import java.text.SimpleDateFormat
def Message processData(Message message) {
def body = message.getBody(java.lang.String) ?: ""
def headers = message.getHeaders()
def properties = message.getProperties()
// 예외 정보 추출 (CPI 시스템 헤더)
def exceptionObj = properties.get("CamelExceptionCaught")
def errorMsg = exceptionObj?.getMessage() ?: "Unknown error"
def errorClass = exceptionObj?.getClass()?.getName() ?: "N/A"
def rootCause = exceptionObj?.getCause()?.getMessage() ?: errorMsg
// 비즈니스 식별자 보존
def poNumber = headers.get("SAP_PO_Number") ?: "UNRESOLVED"
def msgId = headers.get("SAP_MessageProcessingLogID")
def timestamp = new SimpleDateFormat("yyyy-MM-ddTHH:mm:ssZ").format(new Date())
def errorContext = [
messageId : msgId,
poNumber : poNumber,
occurredAt : timestamp,
errorClass : errorClass,
errorMsg : errorMsg,
rootCause : rootCause,
originalPayload: body.take(4000)
]
message.setProperty("ERROR_CONTEXT_JSON", new JsonBuilder(errorContext).toPrettyString())
message.setHeader("X-Error-Severity", "HIGH")
message.setHeader("X-Retry-Count", (headers.get("X-Retry-Count") ?: 0) as Integer)
return message
}
이 스크립트는 Exception Subprocess 시작 직후 실행되어 예외 객체에서 클래스명, 메시지, 근본 원인을 추출하고, 원본 페이로드와 함께 JSON으로 직렬화합니다. 이후 Step에서 이 Property를 활용해 알림과 DLQ 적재가 가능해집니다.
Message Loss 방지: Dead Letter 패턴 구현
SAP Integration Suite Enterprise Edition에서는 JMS Queue를 활용할 수 있습니다. Exception Subprocess의 마지막 단계에서 JMS Producer로 실패 메시지를 별도 큐(예: PO_ERROR_DLQ)에 적재하면, 별도의 재처리용 iFlow가 주기적으로 폴링하여 자동 재시도 또는 수동 재처리 UI를 제공할 수 있습니다.
<!-- JMS Receiver Adapter 설정 (Dead Letter Queue) -->
<jms:queue name="PO_ERROR_DLQ">
<property name="Retry Interval">5 minutes</property>
<property name="Exponential Backoff">true</property>
<property name="Maximum Retry Interval">60 minutes</property>
<property name="Dead-Letter Queue Enabled">true</property>
<property name="Concurrent Processes">5</property>
</jms:queue>
// 재시도 카운터 증가 + 한계 체크
def Message handleRetry(Message message) {
def retryCount = (message.getHeader("X-Retry-Count", Integer) ?: 0) + 1
def MAX_RETRY = 5
if (retryCount > MAX_RETRY) {
message.setHeader("X-Final-Status", "PERMANENT_FAILURE")
message.setProperty("TARGET_QUEUE", "PO_PERMANENT_FAILURE")
} else {
message.setHeader("X-Retry-Count", retryCount)
message.setProperty("TARGET_QUEUE", "PO_ERROR_DLQ")
}
return message
}
주의할 점은 JMS Queue 사용 시 Worker Node당 할당된 큐 용량 제한이 있다는 것입니다. 무한 재시도 루프를 방지하기 위해 반드시 최대 재시도 횟수와 영구 실패 큐를 분리 설계해야 합니다.
에러 컨텍스트 보존: 페이로드와 헤더 캡처
CPI는 예외 발생 시 활용 가능한 시스템 헤더와 Property를 제공합니다. 이를 적절히 조합하면 디버깅 시간을 크게 줄일 수 있습니다.
| 식별자 | 설명 |
|---|---|
| ${property.CamelExceptionCaught} | 실제 예외 객체(Throwable) |
| ${property.CamelFailureRouteId} | 예외 발생 Step ID |
| ${header.SAP_MessageProcessingLogID} | MPL ID, 모니터링 추적용 |
| ${header.SAP_ApplicationID} | 비즈니스 식별자(설정 시) |
| ${header.SAP_Sender} | 송신자 시스템 정보 |
// 페이로드를 Data Store에 안전하게 보관
import com.sap.it.api.ITApiFactory
import com.sap.it.api.msglog.MessageLogFactory
def Message persistFailedPayload(Message message) {
def msgLog = MessageLogFactory.getMessageLog(message)
def errorJson = message.getProperty("ERROR_CONTEXT_JSON")
def poNumber = message.getHeader("SAP_PO_Number", String) ?: "UNKNOWN"
msgLog?.addAttachmentAsString(
"ErrorContext_${poNumber}",
errorJson,
"application/json"
)
msgLog?.setStringProperty("PO_Number", poNumber)
msgLog?.setStringProperty("Error_Class",
message.getProperty("CamelExceptionCaught")?.getClass()?.getSimpleName() ?: "N/A")
return message
}
MPL Attachment로 첨부된 페이로드는 운영자가 Monitoring UI에서 즉시 다운로드할 수 있어 사후 분석에 매우 유용합니다. 다만 첨부 크기에는 제한이 있으므로 대용량 페이로드는 외부 Object Store나 Data Store에 분리 저장하는 것이 권장됩니다.
알림 채널 연동: 이메일과 Slack 에러 통보
에러가 발생했다는 사실은 사람이 빠르게 알아야 의미가 있습니다. Exception Subprocess 내부에 Mail Adapter와 HTTP Adapter를 병렬로 배치하여 다중 채널 알림을 구현하는 것이 일반적입니다.
// Slack Webhook 전송용 페이로드 빌더
def Message buildSlackAlert(Message message) {
def ctx = new groovy.json.JsonSlurper().parseText(
message.getProperty("ERROR_CONTEXT_JSON"))
def slackPayload = [
text: ":rotating_light: iFlow Error - PO Integration",
attachments: [[
color: "danger",
fields: [
[title: "PO Number", value: ctx.poNumber, short: true],
[title: "Error Class", value: ctx.errorClass, short: true],
[title: "MPL ID", value: ctx.messageId, short: false],
[title: "Root Cause", value: ctx.rootCause?.take(500), short: false],
[title: "Occurred At", value: ctx.occurredAt, short: true]
],
footer: "BTP Integration Suite - PROD"
]]
]
message.setBody(new groovy.json.JsonBuilder(slackPayload).toString())
message.setHeader("Content-Type", "application/json")
return message
}
운영 노하우 하나를 덧붙이면, 알림 자체가 또 다른 실패 지점이 될 수 있다는 것입니다. Slack 통보용 HTTP Adapter가 타임아웃되면 Exception Subprocess 내부에서 다시 예외가 발생할 수 있는데, 이는 무한 루프를 유발할 수 있습니다. 따라서 알림 Step 자체에는 별도의 try-catch를 두거나 ignoreException 설정을 활용하는 것이 권장됩니다.
운영 환경에서 검증된 체크리스트와 안티패턴
실제 프로덕션에서 검증된 7가지 점검 항목을 정리합니다.
- 모든 iFlow에 Exception Subprocess가 정의되어 있는가 — 기본 템플릿화하여 신규 iFlow 생성 시 자동 포함되도록 표준화
- Error End Event를 사용하는가 — 일반 End Event로 닫아 MPL이 COMPLETED로 표시되는 안티패턴 회피
- 비즈니스 식별자(PO번호, 주문번호 등)가 헤더에 보존되는가 — 매핑 직후 SAP_ApplicationID 또는 커스텀 헤더에 즉시 캡처
- 원본 페이로드가 MPL Attachment 또는 Data Store에 저장되는가 — 재처리 가능성 확보
- JMS DLQ와 최대 재시도 한도가 설정되었는가 — 무한 재시도 방지, 영구 실패 큐 분리
- 알림 채널이 이중화되어 있는가 — 메일과 Slack/Teams 등 최소 2개 채널, 알림 Step 자체 예외 격리
- 에러 코드 분류 체계가 있는가 — Retryable(네트워크 타임아웃) vs Non-Retryable(데이터 검증 실패) 구분 처리
대표적인 안티패턴은 다음과 같습니다. 첫째, Exception Subprocess 안에서 또 다른 외부 호출(예: REST API)을 동기로 수행하면서 타임아웃을 길게 설정하는 경우입니다. 이는 Worker Node의 메모리와 스레드를 점유하여 다른 메시지 처리에까지 영향을 줍니다. 둘째, 모든 예외를 동일하게 처리하는 경우입니다. HTTP 503(일시적 서비스 불가)과 HTTP 400(잘못된 요청)은 재시도 전략이 완전히 달라야 합니다. 셋째, 페이로드를 로그에 그대로 출력하는 경우입니다. 개인정보나 결제 정보가 포함되면 컴플라이언스 이슈가 됩니다. 민감 필드는 마스킹 후 로깅하는 것이 일반적으로 권장됩니다.
댓글 0
아직 댓글이 없습니다.