1. Content Modifier와 Script의 본질적 차이
SAP Integration Suite(구 Cloud Integration, CPI)로 iFlow를 설계하다 보면 "이 데이터 변환을 Content Modifier로 처리해야 하나, Script로 처리해야 하나?"라는 판단을 매일같이 만나게 됩니다. 두 단계 모두 메시지의 Header, Property, Body를 조작할 수 있기 때문에 초심자에게는 기능이 겹쳐 보이지만, 사실 두 컴포넌트는 완전히 다른 실행 모델 위에서 동작합니다.
이 글에서 다룰 학습 포인트는 다음과 같습니다.
- Content Modifier의 선언형(declarative) 처리 방식과 내부 동작 순서
- Script Step(Groovy/JavaScript)의 명령형(imperative) 실행 컨텍스트
- Simple Expression, XPath, JsonPath로 표현 가능한 범위와 한계
- 실전에서 "어느 쪽을 골라야 유지보수·성능에서 유리한가"에 대한 판단 기준
- 두 요소를 조합해 최소 코드로 안정적인 매핑을 완성하는 패턴
결론부터 정리하면, Content Modifier는 런타임이 이미 지원하는 표현식과 규칙으로 처리할 수 있는 단순 변환에 최적화된 도구이고, Script Step은 런타임 표현식으로는 불가능한 복잡한 로직·조건·외부 라이브러리 활용이 필요할 때 사용하는 도구입니다.
2. 이 예제를 이해하기 위해 필요한 배경
본격적으로 들어가기 전에 다음 개념 정도는 이미 알고 있다고 가정합니다. Camel 기반 메시지 모델(Exchange, Message, Header, Property, Body), iFlow의 기본 단계(Sender, Receiver, Adapter, Processing Step), Groovy 문법 초급 수준, 그리고 Integration Suite의 Cloud Integration 캡슐에서 iFlow를 Deploy/Undeploy 해 본 경험입니다. XPath/JsonPath는 값 추출 수준만 알아도 충분합니다.
3. 실습 환경과 준비물
이 글의 코드는 다음 환경을 기준으로 검증했습니다. 버전에 따라 UI 라벨이 조금씩 다를 수 있으니 이름이 다르면 유사 항목을 찾아 사용하시면 됩니다.
- SAP Integration Suite — Cloud Integration capability (2026년 상반기 릴리스 기준)
- Runtime: Cloud Integration Runtime (based on Apache Camel)
- Script 언어: Groovy 2.4 계열 (JSR223 엔진), JavaScript(Nashorn 대체)도 선택 가능하지만 Groovy 사용이 일반적으로 권장
- 테스트 도구: Postman 또는 Integration Suite 내장 Test Console
- 권한:
WebIDE_CPIDeveloper,PI_Integration_Developer등 iFlow 편집/배포 롤 - 디버깅: Trace 레벨로 iFlow를 Deploy 후 Monitor 화면 - Manage Integration Content에서 MPL 확인
Groovy Script Step을 처음 추가하면 IDE가 Message processData(Message message) 시그니처를 가진 템플릿을 자동으로 생성합니다. 이 시그니처는 반드시 유지해야 런타임이 스크립트를 호출할 수 있습니다.
4. Content Modifier 내부에서 벌어지는 일
Content Modifier는 이름 그대로 "메시지 내용을 수정하는" 선언형 단계입니다. UI에서 Message Header, Exchange Property, Message Body 탭을 열고 이름과 값(그리고 데이터 타입)을 입력하기만 하면 런타임이 알아서 순서대로 값을 설정합니다. 이 "알아서"의 실제 동작은 다음과 같은 파이프라인으로 이해할 수 있습니다.
입력 Exchange → (1) Header 계산·설정 → (2) Property 계산·설정 → (3) Body 계산·설정 → 출력 Exchange
각 값은 Type 컬럼에 따라 다르게 계산됩니다. Constant는 문자열 리터럴, Expression은 Simple Expression 파서를 통과하며, XPath·External Parameter는 별도 엔진이 값을 뽑아냅니다. 예를 들어 Header 값에 ${property.customerRegion}이라고 쓰면 이는 이미 Property로 존재하는 customerRegion을 문자열로 치환합니다.
여기서 초보자가 가장 자주 혼동하는 것은 Header vs Property의 차이입니다. Header는 어댑터의 아웃바운드 호출로 전달되어 HTTP 헤더나 SFTP 파일명 등으로 사용될 수 있지만, Property는 Exchange 내부에서만 유효한 "작업용 변수"입니다. 민감 정보나 중간 계산 결과는 Property로 두고, 실제로 외부에 나가야 하는 값만 Header로 승격시키는 것이 일반적으로 권장됩니다.
비유하자면 Content Modifier는 엑셀의 수식 셀과 같습니다. 셀에 값을 채우거나 다른 셀을 참조하는 수식을 넣을 수는 있지만, 반복문을 돌리거나 외부 API를 부를 수는 없습니다. 반대로 Script Step은 매크로(VBA)에 해당합니다. 무엇이든 할 수 있지만 그만큼 관리 비용이 발생합니다.
5. Script Step — Groovy가 얹히는 실행 컨텍스트
Script Step은 JSR223 스크립트 엔진 위에서 Groovy 코드를 실행합니다. 진입점 메서드는 Message processData(Message message)이며, 매개변수 message는 현재 Exchange의 In-Message를 감싼 래퍼입니다. 여기서 접근 가능한 주요 API는 다음과 같습니다.
message.getBody(Class)— Body를 원하는 타입으로 변환해 반환 (String, byte[], Reader 등)message.setBody(Object)— Body 치환message.getHeaders()·setHeader(name, value)— Camel Header 조작message.getProperties()·setProperty(name, value)— Exchange Property 조작com.sap.it.api.ITApiFactory— Secure Store, Partner Directory 등 SAP 확장 API
Script는 Content Modifier로 표현 불가능한 세 부류의 요구를 처리합니다. (a) 조건 분기가 많은 값 계산, (b) 반복문으로 컬렉션을 재구성하는 매핑, (c) 외부 유틸리티 클래스(예: java.time, groovy.json.JsonSlurper)를 활용한 파싱·포맷 변환입니다.
실전 예제 1 — Content Modifier만으로 처리하기
가장 흔한 사례입니다. 상류 시스템에서 받은 구매요청(PurchaseRequisition) JSON에 대해 아래 세 가지 사전 작업이 필요합니다.
- Body를 그대로 두되, 요청 ID를 별도 Property로 저장해 이후 라우팅에 활용
- Receiver로 나갈 HTTP 요청에 상관 관계용 X-Correlation-Id 헤더 추가
- 내부 감사용으로 처리 시각 Property 기록
이 정도는 Groovy 없이 Content Modifier만으로 충분합니다. UI에 다음처럼 채워 넣으면 됩니다.
<!-- Content Modifier: Enrich PR Message -->
<!-- Exchange Property 탭 -->
Name: requisitionId Type: XPath Value: //PurchaseRequisition/Id/text()
Name: processedAt Type: Expression Value: ${date:now:yyyy-MM-dd'T'HH:mm:ssZ}
<!-- Message Header 탭 -->
Name: X-Correlation-Id Type: Expression Value: ${property.requisitionId}
Name: Content-Type Type: Constant Value: application/xml
여기서 눈여겨볼 부분은 ${date:now:...} 같은 Simple Expression 함수와, Property 값을 Header가 다시 참조하는 순서(Property가 먼저 계산되기 때문에 가능)입니다. 한 줄의 코드도 없이 배포·롤백·버전 관리가 가능하기 때문에 유지보수 부담이 매우 낮습니다.
실전 예제 2 — Script로만 가능한 변환
이제 다음 요구를 봅시다. 상류에서 넘어온 JSON 배열 items[] 중에서 수량이 0보다 크고, 카테고리가 'MRO' 또는 'CAPEX'인 항목만 남긴 뒤, 총액을 계산해 Body에 다시 넣고 Property로도 노출해야 합니다. JsonPath만으로는 필터·집계를 동시에 할 수 없기 때문에 이 시점부터는 Groovy가 필요합니다.
import com.sap.gateway.ip.core.customdev.util.Message
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
Message processData(Message message) {
def rawBody = message.getBody(String) ?: '{"items":[]}'
def parsed = new JsonSlurper().parseText(rawBody)
def allowedCategories = ['MRO', 'CAPEX'] as Set
def filtered = parsed.items.findAll { line ->
(line?.quantity ?: 0) > 0 && allowedCategories.contains(line?.category)
}
def totalAmount = filtered.sum { (it.unitPrice ?: 0) * (it.quantity ?: 0) } ?: 0
def result = [
requisitionId : parsed.requisitionId,
lineCount : filtered.size(),
totalAmount : totalAmount,
lines : filtered
]
// Body 및 후속 라우팅용 Property 세팅
message.setBody(JsonOutput.toJson(result))
message.setProperty('requisitionTotal', totalAmount)
message.setProperty('requisitionLineCount', filtered.size())
// 감사 로깅 (MPL Attachment)
def messageLog = messageLogFactory.getMessageLog(message)
messageLog?.addAttachmentAsString(
'FilteredRequisition',
JsonOutput.prettyPrint(JsonOutput.toJson(result)),
'application/json'
)
messageLog?.setStringProperty('LineCountAfterFilter', filtered.size().toString())
return message
}
포인트는 세 가지입니다. 첫째, getBody(String)으로 안전하게 문자열을 얻고 널 방어를 함께 처리했습니다. 둘째, 결과를 Body와 Property 양쪽에 노출해 이후 Router에서 Simple Expression으로 분기할 수 있게 했습니다. 셋째, messageLogFactory로 MPL(Message Processing Log)에 첨부와 커스텀 프로퍼티를 남겨 운영자가 Web UI에서 즉시 원인을 파악할 수 있게 했습니다.
실전 예제 3 — 프로덕션 품질로 다듬기
실제 운영에 올릴 때는 예외 처리, 시크릿 접근, 방어적 로깅을 추가합니다. 아래 예제는 Secure Parameter에 저장한 API Key를 사용하고, 파싱 오류를 명시적 예외로 승격시키며, Trace 레벨 이상에서만 대용량 페이로드를 로깅합니다.
import com.sap.gateway.ip.core.customdev.util.Message
import com.sap.it.api.ITApiFactory
import com.sap.it.api.securestore.SecureStoreService
import com.sap.it.api.securestore.UserCredential
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
Message processData(Message message) {
def log = messageLogFactory.getMessageLog(message)
try {
def secureStore = ITApiFactory.getService(SecureStoreService.class, null)
UserCredential cred = secureStore.getUserCredential('PARTNER_API_KEY')
message.setHeader('X-Api-Key', cred.getPassword().toString())
def raw = message.getBody(String)
if (!raw?.trim()) {
throw new IllegalStateException('Empty payload received from upstream')
}
def payload = new JsonSlurper().parseText(raw)
def enriched = [
source : 'PR-Enricher',
receivedAt : new Date().format("yyyy-MM-dd'T'HH:mm:ssXXX"),
original : payload
]
message.setBody(JsonOutput.toJson(enriched))
message.setProperty('enrichmentStatus', 'OK')
if (log?.getLogLevel()?.toString() == 'TRACE') {
log.addAttachmentAsString(
'EnrichedPayload',
JsonOutput.prettyPrint(JsonOutput.toJson(enriched)),
'application/json')
}
return message
} catch (Exception ex) {
log?.setStringProperty('enrichmentStatus', 'FAILED')
log?.setStringProperty('errorMessage', ex.message ?: 'unknown')
// 상위 Exception Subprocess로 전파
throw ex
}
}
운영 관점에서 좋은 습관 몇 가지를 함께 기억해두세요. Secure Store에 등록하지 않은 자격증명을 스크립트에 하드코딩하지 말 것, 대용량 Body 로깅은 반드시 Trace·Debug 레벨 게이트 뒤에 둘 것, 그리고 throw는 Exception Subprocess로 흐름을 넘겨 재시도·알림 로직을 재사용할 수 있게 할 것.
6. 자주 겪는 함정과 해결 아이디어
Q1. Content Modifier에서 ${property.xxx}가 빈 값으로 나옵니다. Property가 앞선 단계에서 실제로 세팅되었는지 MPL에서 확인하세요. 흔한 원인은 (1) 대소문자 오타, (2) 앞 단계가 Content Modifier가 아니라 Router 뒤 다른 브랜치에서 세팅되었을 때, (3) Body가 stream이라 한 번 읽히고 소진된 경우입니다. 스트림 소진은 Content Modifier 앞단에서 Body 타입을 Expression · ${bodyAs(java.lang.String)}로 한번 치환해두면 해결됩니다.
Q2. Groovy 스크립트가 로컬에서는 되는데 배포하면 ClassNotFoundException이 납니다. Cloud Integration Runtime은 화이트리스트된 클래스만 허용합니다. java.time, groovy.json.*, Camel·SAP 기본 API는 대부분 사용 가능하지만 외부 JAR은 원칙적으로 올릴 수 없습니다. 대체 API를 찾거나 로직을 단순화해야 합니다.
Q3. Script가 잘 도는데 성능이 느립니다. 세 가지를 점검하세요. (1) 매 실행마다 큰 정규식을 컴파일하고 있지는 않은지 — @groovy.transform.Field로 정적 캐싱 가능, (2) getBody(Document) 같이 무거운 파서를 부르는지 — 가능하면 String으로 받아 필요한 부분만 파싱, (3) 반복문 안에서 로깅·Attachment를 남기는지 — 루프 밖으로 이동.
Q4. Property에 담긴 값이 예상과 타입이 다릅니다. Content Modifier의 Data Type 컬럼을 비워두면 기본 java.lang.String이 됩니다. 숫자·불리언 비교가 필요한 Property는 반드시 타입을 명시하거나, Script에서 as Integer·Boolean.parseBoolean으로 캐스팅해야 Router 조건이 의도대로 동작합니다.
7. 이 다음에 다뤄볼 만한 주제
Content Modifier와 Script의 경계를 익혔다면, 그 위에 얹히는 다음 도구들로 시야를 넓혀보세요. Message Mapping(그래픽 매핑)과 XSLT Mapping은 대량 필드 매핑에 적합하고, Filter/Splitter/Gather 패턴은 컬렉션 처리를 Script 없이 선언형으로 풀어냅니다. Externalized Parameter와 Value Mapping을 활용하면 환경별 상수(개발·QA·운영)를 iFlow 밖으로 뽑아 관리 부담을 크게 줄일 수 있습니다. 관측성 측면에서는 Trace·Debug·Info 로그 레벨 전략과 Cloud ALM 연계, 그리고 Custom Header Propagation 정책도 함께 익혀두면 좋습니다.
8. 더 깊이 파고들 때 도움되는 자료
- SAP Help Portal — Integration Suite 공식 문서 허브
- help.sap.com — Cloud Integration: Content Modifier 단계 설명
- help.sap.com — Cloud Integration: Script 단계와 지원 언어
- help.sap.com — Simple Expression 함수 레퍼런스
- help.sap.com — Script API Reference (Message, MessageLog)
- SAP Community — Integration Suite 토픽 (실전 Q&A와 블로그)
- Apache Groovy 공식 문서 — 언어 문법 및 표준 라이브러리
댓글 0
아직 댓글이 없습니다.