BTP

iFlow Dead Letter 없으면 큰일 — 3단계 방어 #shorts #SAP #BTP

1. Dead Letter Channel 패턴이 왜 필요한가

SAP BTP Integration Suite로 S/4HANA와 외부 시스템을 연동하다 보면 반드시 마주치는 질문이 있습니다. "수신 시스템이 죽어 있을 때 그 메시지는 어디로 가는가?" 에러 처리를 설계하지 않은 iFlow에서는 답이 명확합니다. 메시지는 Failed 상태로 남고, 페이로드는 보존 기간이 지나면 사라지며, 아무도 모르는 사이에 SalesOrder 하나가 유실됩니다. 매출 데이터가 걸린 인터페이스라면 이건 장애가 아니라 사고입니다.

Dead Letter Channel은 Enterprise Integration Patterns(Hohpe & Woolf)에서 정의된 고전적인 메시징 패턴입니다. 핵심 아이디어는 단순합니다. 전달에 최종 실패한 메시지를 버리지 않고, 별도의 저장소(Dead Letter Store)로 옮겨 보존한 뒤 나중에 분석하거나 재처리한다는 것입니다. 우체국의 반송 우편물 보관소에 비유할 수 있습니다. 수취인 불명 우편을 소각하지 않고 별도 창고에 보관해 두었다가, 주소가 확인되면 다시 배송하는 구조입니다.

이 글에서는 Integration Suite의 표준 기능(JMS Queue, Exception Subprocess, Data Store)만으로 Dead Letter 패턴을 3단계로 구현하는 방법을, S/4HANA SalesOrder 연동 시나리오를 예제로 삼아 설명합니다. 읽고 나면 다음을 할 수 있게 됩니다.

  • iFlow에서 일시적 오류와 영구적 오류를 구분하여 처리하는 구조 설계
  • JMS 어댑터의 재시도·Dead-Letter Queue 옵션 구성
  • Data Store 기반 Dead Letter Store 구축과 재처리 iFlow 작성
  • 실패 메시지에 대한 알림·모니터링 체계 수립

2. 시작 전에 갖춰야 할 배경과 환경

이 글은 중급자 대상입니다. iFlow의 기본 구성 요소(Sender/Receiver 어댑터, Content Modifier, Router)와 Groovy Script 스텝의 기본 사용법을 알고 있다고 가정합니다. Message Processing Log(MPL)를 조회해 본 경험이 있으면 이해가 훨씬 빠릅니다.

환경 조건은 다음과 같습니다.

  • SAP Integration Suite(Cloud Foundry 환경) — Cloud Integration capability 활성화. 2024년 이후 표준 테넌트 기준으로 설명합니다.
  • JMS 어댑터는 일반적으로 Integration Suite standard/premium 에디션에서 사용 가능하며, basic 에디션에서는 제약이 있을 수 있으니 테넌트의 Enterprise Messaging 리소스(큐 개수, 스토리지 용량)를 먼저 확인하는 것이 권장됩니다.
  • Data Store는 모든 에디션에서 사용 가능하지만 테넌트 전체 용량 제한이 있습니다.
  • 테스트용 Receiver로 S/4HANA Cloud의 Sales Order API(OData V2 API_SALES_ORDER_SRV) 또는 임의의 HTTP 엔드포인트를 준비합니다.

3. Integration Suite의 에러 처리 아키텍처 이해

구현에 들어가기 전에 Cloud Integration이 에러를 다루는 계층을 정리해야 합니다. 크게 세 겹의 방어선이 있습니다.

  • 1차 방어선 — 어댑터 레벨 재시도: JMS Sender 어댑터는 처리 실패 시 트랜잭션을 롤백하고 메시지를 큐에 되돌린 뒤, 설정된 간격(Retry Interval, Exponential Backoff)으로 자동 재시도합니다. 네트워크 순단 같은 일시적(transient) 오류는 대부분 여기서 해소됩니다.
  • 2차 방어선 — Exception Subprocess: iFlow 내부에서 발생한 예외를 잡아내는 전용 서브프로세스입니다. try-catch의 catch 블록에 해당하며, 여기서 에러 정보를 가공해 Dead Letter Store에 기록하거나 알림을 발송합니다.
  • 3차 방어선 — JMS Dead-Letter Queue 옵션: 페이로드 자체가 문제(예: 초대형 메시지로 인한 메모리 부족)여서 워커 노드가 다운되는 상황(poison message)을 막기 위한 안전장치입니다. 이 옵션을 켜면 동일 메시지가 두 번 처리 실패했을 때 큐 안에서 blocked 상태로 격리되어 더 이상 자동 재시도되지 않습니다.

비유하자면 1차는 "다시 초인종 누르기", 2차는 "부재중 쪽지 남기고 경비실에 맡기기", 3차는 "위험물로 분류해 격리 보관"입니다. Dead Letter 패턴의 완성은 이 세 겹을 조합하되, 최종 실패 메시지가 반드시 조회·재처리 가능한 형태로 남도록 만드는 데 있습니다.

또 하나 중요한 구분은 Error End Event와 Escalation End Event입니다. Exception Subprocess를 Error End Event로 끝내면 MPL이 Failed로 남고 JMS 트랜잭션이 롤백되어 재시도가 이어집니다. 반면 Escalation End Event로 끝내면 MPL은 Escalated 상태가 되고 트랜잭션은 커밋되어 재시도가 멈춥니다. 즉 "우리가 Dead Letter Store에 안전하게 옮겨 담았으니 큐에서의 재시도는 그만"이라는 신호를 Escalation으로 표현하는 것이 일반적인 설계입니다.

4. 3단계 구현 개요: Receiver 분리 → Error Handler → Dead Letter Store

전체 구조는 두 개의 iFlow와 하나의 저장소로 구성됩니다.

  1. 1단계 — 수신과 처리의 분리(비동기 디커플링): 외부에서 SalesOrder를 받는 Inbound iFlow는 검증만 하고 즉시 JMS Queue(SALESORDER_INBOUND)에 적재 후 202 Accepted를 반환합니다. 실제 S/4HANA 호출은 JMS Sender로 시작하는 Processing iFlow가 담당합니다. 이렇게 하면 Receiver 장애가 송신자에게 전파되지 않습니다.
  2. 2단계 — Error Handler: Processing iFlow에 Exception Subprocess를 두고, 재시도 횟수(SAPJMSRetries 헤더)를 확인하여 한도 초과 시 Dead Letter 경로로 분기합니다.
  3. 3단계 — Dead Letter Store: 최종 실패 메시지를 페이로드·에러 원인·타임스탬프와 함께 Data Store(DLQ_SALESORDER)에 Write하고 Escalation으로 종료합니다. 별도의 재처리 iFlow가 이 저장소를 읽어 재전송합니다.

첫 번째 실전 코드로, Exception Subprocess 안에서 에러 컨텍스트를 수집하는 기본 Groovy 스크립트를 보겠습니다.

import com.sap.gateway.ip.core.customdev.util.Message

def Message captureErrorContext(Message message) {
    def map = message.getProperties()
    // CamelExceptionCaught: 프레임워크가 잡은 마지막 예외 객체
    def ex = map.get("CamelExceptionCaught")

    def orderId   = message.getHeaders().get("SalesOrderNumber") ?: "UNKNOWN"
    def retryCnt  = (message.getHeaders().get("SAPJMSRetries") ?: 0) as Integer

    message.setProperty("dlq_orderId", orderId)
    message.setProperty("dlq_retryCount", retryCnt.toString())
    message.setProperty("dlq_errorType", ex != null ? ex.getClass().getCanonicalName() : "Unknown")
    message.setProperty("dlq_errorText", ex != null ? ex.getMessage() : "No exception detail")
    message.setProperty("dlq_failedAt", new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"))
    return message
}

이 프로퍼티들은 이후 Router 분기 조건과 Data Store 엔트리 ID, 알림 본문에 재사용됩니다.

5. JMS Queue로 영속성 보장하기

Dead Letter 패턴의 전제는 "메시지가 처리 전에 이미 안전한 곳에 있다"는 것입니다. JMS Queue는 테넌트 재시작이나 워커 노드 장애에도 메시지를 보존하므로 이 역할에 적합합니다. Processing iFlow의 JMS Sender 어댑터에서 핵심 파라미터는 다음과 같습니다.

설정 항목권장 값(예시)의미
Retry Interval (min)1실패 후 첫 재시도까지 대기 시간
Exponential Backoff활성화재시도마다 간격 2배 증가 (1→2→4→8분)
Maximum Retry Interval (min)60백오프 간격의 상한
Dead-Letter Queue활성화2회 실패 후 poison message 격리(blocked)

여기서 흔한 오해 하나를 짚고 갑니다. JMS 어댑터의 "Dead-Letter Queue" 체크박스는 별도의 큐를 만들어 주는 기능이 아닙니다. 노드 크래시를 유발하는 메시지를 같은 큐 안에서 blocked 상태로 묶어두는 보호 장치입니다. 비즈니스 관점의 Dead Letter Store(조회·재처리 가능한 보관소)는 다음 섹션의 Data Store 또는 전용 JMS 큐(SALESORDER_DLQ)로 직접 설계해야 합니다. 두 방식의 차이는 이렇습니다.

  • Data Store 방식: Monitor 화면에서 엔트리 조회·다운로드·삭제가 쉬워 운영자 친화적. 보존 기간(Expiration Period) 지정 가능. 대용량 페이로드에는 용량 제한 주의.
  • 전용 DLQ 큐 방식: 재처리 iFlow를 JMS Sender로 붙이면 자동 재시도 파이프라인 재활용 가능. 다만 큐 개수 쿼터를 소모.

중간 규모 인터페이스라면 Data Store 방식이 운영 편의성 면에서 일반적으로 무난합니다.

6. 재시도 전략과 최대 횟수 제어 — 실무 시나리오 코드

무한 재시도는 Dead Letter 패턴의 적입니다. S/4HANA가 반환하는 오류 중 400 Bad Request(데이터 오류)는 천 번을 재시도해도 성공할 수 없는 영구적 오류이고, 503 Service Unavailable은 기다리면 풀리는 일시적 오류입니다. 이를 구분해 재시도 예산을 다르게 배정해야 합니다. Exception Subprocess 내 Router 앞단에 배치할 판별 스크립트입니다. 에러 로깅과 MPL 커스텀 헤더 기록까지 포함한 실무형 예제입니다.

import com.sap.gateway.ip.core.customdev.util.Message

def Message decideDeadLetter(Message message) {
    def props    = message.getProperties()
    def ex       = props.get("CamelExceptionCaught")
    def retryCnt = (message.getHeaders().get("SAPJMSRetries") ?: 0) as Integer
    def httpCode = (props.get("CamelHttpResponseCode") ?: 0) as Integer

    final int MAX_RETRY_TRANSIENT = 5   // 일시적 오류 재시도 예산
    boolean permanent = (httpCode >= 400 && httpCode < 500)  // 4xx = 재시도 무의미
    boolean exhausted = (retryCnt >= MAX_RETRY_TRANSIENT)

    // Router 분기용 플래그: true면 Dead Letter 경로로
    message.setProperty("dlq_route", (permanent || exhausted).toString())

    // MPL 커스텀 헤더 — 모니터링 화면에서 주문번호로 검색 가능하게
    def mpl = messageLogFactory.getMessageLog(message)
    if (mpl != null) {
        mpl.addCustomHeaderProperty("SalesOrderNumber",
            (message.getHeaders().get("SalesOrderNumber") ?: "UNKNOWN") as String)
        mpl.addCustomHeaderProperty("DLQ_Reason",
            permanent ? "PERMANENT_HTTP_" + httpCode : "RETRY_EXHAUSTED_" + retryCnt)
    }
    return message
}

Router는 ${property.dlq_route} = 'true'이면 Data Store Write(Operation: Write, Data Store Name: DLQ_SALESORDER, Entry ID: ${property.dlq_orderId}_${date:now:yyyyMMddHHmmss}, Retention: 90일, Encrypt 활성화) 후 Escalation End로 향하게 하고, false이면 Error End로 보내 JMS 롤백·재시도를 계속하게 합니다. 이 분기 하나가 패턴의 심장입니다.

7. 실전 예제: SalesOrder Dead Letter 재처리 iFlow (프로덕션 관점)

보관만 하고 재처리하지 못하면 반쪽짜리입니다. 재처리 iFlow는 Timer(예: 30분 주기) → Data Store Select(최대 N건) → Splitter → 원본 Receiver 재호출 → 성공 시 Data Store Delete 순으로 구성합니다. 프로덕션에서는 다음 세 가지를 반드시 고려해야 합니다.

  • 멱등성(Idempotency): 재전송 시 S/4HANA에 주문이 중복 생성되지 않도록 멱등 키를 함께 보냅니다. 재처리 페이로드를 복원할 때 원본의 주문번호+실패시각 조합을 키로 사용합니다.
  • 성능: Select 건수를 소량(예: 20건)으로 제한해 재처리 폭주가 정상 트래픽을 밀어내지 않게 합니다.
  • 보안: Data Store Encrypt 옵션을 켜고, 페이로드에 개인정보가 있다면 보존 기간을 짧게 잡습니다.

Data Store Write 직전, 에러 메타데이터를 페이로드에 감싸 넣는 래핑 스크립트입니다. 재처리 시 원본과 이력을 함께 복원할 수 있습니다.

import com.sap.gateway.ip.core.customdev.util.Message
import groovy.json.JsonOutput

def Message wrapForDeadLetterStore(Message message) {
    def originalBody = message.getBody(String)
    def envelope = [
        meta: [
            salesOrderId : message.getProperty("dlq_orderId"),
            errorType    : message.getProperty("dlq_errorType"),
            errorText    : message.getProperty("dlq_errorText"),
            retryCount   : message.getProperty("dlq_retryCount"),
            failedAt     : message.getProperty("dlq_failedAt"),
            sourceIFlow  : "IF_SalesOrder_S4_Replication_v2"
        ],
        payload: originalBody.bytes.encodeBase64().toString()
    ]
    message.setBody(JsonOutput.prettyPrint(JsonOutput.toJson(envelope)))
    message.setHeader("Content-Type", "application/json")
    return message
}

테스트 시에는 Receiver를 의도적으로 잘못된 엔드포인트로 바꿔 5회 재시도 → Data Store 적재 → 엔드포인트 복구 → 재처리 성공 → 엔트리 삭제까지의 전체 사이클을 검증하는 것이 권장됩니다. 이 사이클이 한 번이라도 수동 개입 없이 돌아야 프로덕션 준비가 된 것입니다.

8. 흔한 실수, 모니터링 체계, 그리고 더 파볼 주제들

마지막으로 운영 단계에서 자주 나오는 질문을 정리합니다.

  • Q1. Escalation으로 끝냈는데 메시지가 계속 재시도됩니다. Exception Subprocess 밖의 메인 프로세스가 Error End로 끝나거나, 스크립트에서 예외를 다시 던지고 있는지 확인하세요. Dead Letter 경로의 끝은 반드시 Escalation(또는 정상 End)이어야 JMS 트랜잭션이 커밋됩니다.
  • Q2. SAPJMSRetries 헤더가 항상 비어 있습니다. 이 헤더는 JMS Sender로 시작하는 iFlow에서만 채워집니다. HTTPS Sender 기반 동기 iFlow에는 존재하지 않으므로, 그 경우 재시도 카운트를 프로퍼티로 직접 관리해야 합니다.
  • Q3. Data Store가 가득 차서 Write가 실패합니다. 테넌트 전체 용량 제한 때문입니다. Retention을 현실적으로(30~90일) 잡고, 재처리 성공 시 Delete를 빠뜨리지 않았는지 점검하세요. 대용량 페이로드는 참조(문서 ID)만 저장하는 Claim Check 패턴과의 결합이 일반적으로 권장됩니다.
  • Q4. blocked 메시지는 어떻게 풀어주나요? Monitor → Manage Message Queues에서 해당 큐의 blocked 엔트리를 선택해 Retry 하거나 삭제합니다. 근본 원인(주로 페이로드 크기) 해결이 우선입니다.

알림 체계는 두 축으로 구성합니다. 첫째, Exception Subprocess의 Dead Letter 경로에 Mail/HTTP Receiver를 추가해 실패 즉시 팀 채널로 통보. 둘째, Cloud Integration의 OData Monitoring API로 Escalated MPL과 Data Store 엔트리 수를 주기 조회하여 SAP Alert Notification Service나 대시보드에 연결하는 방식이 일반적입니다. MPL 커스텀 헤더에 주문번호를 넣어 두었으므로 장애 티켓에서 특정 주문의 처리 이력을 바로 추적할 수 있습니다.

이 글의 구조가 손에 익었다면 다음 주제로 확장해 보세요. XI 어댑터의 재전송 메커니즘과의 비교, JMS 대신 SAP Event Mesh를 큐잉 계층으로 쓰는 이벤트 기반 설계, Splitter-Gather 조합에서의 부분 실패 처리, 그리고 트랜잭션 핸들링(Required for JMS) 세부 옵션이 자연스러운 다음 걸음입니다.

더 깊이 볼 만한 자료:

댓글 0

아직 댓글이 없습니다.