1. CONCATENATE 시대의 종말: 왜 문자열 템플릿인가
ABAP으로 오래 개발해 본 사람이라면 CONCATENATE 구문에 얽힌 크고 작은 상처가 하나쯤 있을 것이다. 변수 5개를 이어 붙이는데 공백이 사라져서 결과가 뭉치거나, SEPARATED BY space를 깜빡해서 로그가 읽을 수 없게 되거나, 숫자 변수를 문자열에 붙이려다 타입 변환 덤프를 만나거나 하는 상황이다. NetWeaver 7.02 이후 도입된 문자열 템플릿(string template) |...| 문법은 이런 반복 노동을 정리해 주는 문법 설탕이다.
이 글에서는 문자열 템플릿의 세 가지 조합 방식을 판매 주문(SalesOrder)·구매 요청(PurchaseRequisition)·고객(Customer) 시나리오로 재구성해 정리한다. 다 읽고 나면 다음 체크리스트를 스스로 확인할 수 있어야 한다.
- 파이프(
|) 두 개로 문자열 리터럴과 변수 자리({ })를 어떻게 조합하는지 설명할 수 있다. CONCATENATE대비 문자열 템플릿이 제공하는 실질적 이점을 세 가지 이상 꼽을 수 있다.- 변수 삽입 / 표현식 삽입 / 포맷 지정자(
ALPHA,DATE,NUMBER) 삽입을 상황에 맞게 골라 쓸 수 있다. - 이스케이프 문자(
\|,\{) 를 정확히 사용하고 성능 함정을 피할 수 있다.
2. 알고 있어야 편한 배경 지식
ABAP 데이터 타입 중 string(가변 길이)과 c(고정 길이)의 차이를 알고 있으면 좋다. 또한 DATA 선언, 인라인 선언(DATA(...)), 그리고 간단한 산술 연산(+, *) 을 이해해야 표현식 삽입 예제를 무리 없이 따라올 수 있다. WRITE / cl_demo_output=>display( ) 같은 출력 수단도 알고 있으면 실습 결과를 바로 확인하기 편하다.
3. 실습 환경과 버전 체크
문자열 템플릿은 NetWeaver AS ABAP 7.02(SAP_BASIS 702, EhP2 시점)부터 지원된다. 다만 인라인 DATA(...) 선언, 표현식 삽입 안에서의 함수 호출 등은 7.40 SP08 이상에서 완전하게 동작한다. 아래 예제는 다음 환경 기준으로 검증하기를 권장한다.
- SAP S/4HANA 2022 이상 (온프레미스) 또는 SAP BTP ABAP Environment (Steampunk)
- SAP_BASIS 7.55 이상 (7.40 SP08 이하는 일부 구문 제약)
- ABAP Development Tools (ADT, Eclipse 기반) 최신 버전
- 테스트용
Z네임스페이스 프로그램 또는 클래스 (예:ZCL_DEMO_STRING_TEMPLATE)
클래식 SAP GUI SE38에서도 실행되지만, 문자열 템플릿의 진가는 인라인 선언과 함께 쓸 때 나오기 때문에 ADT 환경을 권장한다.
4. 파이프 문법의 동작 원리를 한 그림으로
문자열 템플릿은 시각적으로 이렇게 이해하면 편하다.
|... 문자 리터럴 ...{ 변수/식 [WIDTH=..] [ALIGN=..] [형식지정자] }... 문자 리터럴 ...|
왼쪽 파이프에서 오른쪽 파이프까지가 하나의 string 값이다. 파이프 안쪽 텍스트는 그대로 문자열이 되고, 중괄호 { } 로 감싼 부분만 런타임에 값으로 치환된다. 개념적으로는 자바스크립트의 백틱 템플릿 리터럴(`Hello ${name}`)이나 파이썬 f-string(f"Hello {name}")과 매우 유사하다.
내부 동작은 크게 세 단계다. 첫째, ABAP 컴파일러가 |...|를 만나면 이를 임시 문자열 결합 트리로 파싱한다. 둘째, 중괄호 안의 식은 암묵적 문자열 변환이 일어난다. i, p, d, t 등 어떤 표준 타입이든 string으로 자동 변환되기 때문에 CONCATENATE에서 자주 발생하던 "타입 안 맞음" 덤프가 사라진다. 셋째, 옵션(ALPHA, DATE, NUMBER, WIDTH, ALIGN, PAD) 을 만나면 대응하는 커널 변환기(예: CONVERSION_EXIT_ALPHA_OUTPUT과 유사한 로직)를 통과시킨 뒤 삽입한다.
비유하자면 CONCATENATE가 "레고 블록을 손으로 하나씩 딸깍딸깍 끼우는 방식"이라면, 문자열 템플릿은 "설계도(|...|)를 한 장 그려 두면 나머지 조립은 컴파일러가 자동으로 해주는 방식"이다. 그래서 코드가 짧아지고, 특히 로그 문장이나 메시지처럼 "문자 + 변수 + 문자 + 변수 …"가 반복되는 코드에서 가독성이 크게 좋아진다.
5. 방법 1 · 단순 변수 삽입: 로그 한 줄로 보는 힘
가장 단순한 형태는 변수 이름을 그대로 중괄호 안에 넣는 것이다. 판매 주문 처리 로그를 예로 들어 보자.
REPORT zdemo_stmpl_v1.
DATA(lv_so_id) = |4500001234|.
DATA(lv_customer) = |ACME Corp.|.
DATA(lv_net_value) = CONV p( '1250.75' ).
" 1) CONCATENATE 방식 (레거시)
DATA lv_msg_old TYPE string.
CONCATENATE 'SalesOrder' lv_so_id 'for customer'
lv_customer 'created with net value'
lv_net_value
INTO lv_msg_old SEPARATED BY space.
" 2) 문자열 템플릿 방식
DATA(lv_msg_new) =
|SalesOrder { lv_so_id } for customer { lv_customer } | &&
|created with net value { lv_net_value }|.
WRITE: / lv_msg_old,
/ lv_msg_new.
두 결과 문자열은 동일하다. 하지만 유지보수 관점에서 차이가 크다. CONCATENATE 버전은 필드 하나가 추가될 때마다 INTO와 SEPARATED BY를 다시 확인해야 하고 공백 처리가 번거롭다. 템플릿 버전은 문장 안에 자연스러운 공백을 그대로 남기면 된다. 또한 lv_net_value가 p(packed) 타입인데도 별도 변환 없이 그대로 삽입된다. 여러 줄을 이어 붙일 때는 && 문자열 연산자를 쓰면 되는데, 여기서도 파이프가 각 조각의 경계를 명확하게 만들어 준다.
6. 방법 2 · 표현식 삽입: 계산과 메서드 호출을 그대로
템플릿의 진짜 무기는 중괄호 안에 식(expression)을 통째로 넣을 수 있다는 점이다. 산술 연산, 문자열 함수, 심지어 인스턴스 메서드까지 그대로 호출할 수 있다.
CLASS zcl_demo_stmpl_v2 DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS run.
PRIVATE SECTION.
METHODS get_customer_name
IMPORTING iv_id TYPE string
RETURNING VALUE(rv_name) TYPE string.
ENDCLASS.
CLASS zcl_demo_stmpl_v2 IMPLEMENTATION.
METHOD run.
DATA(lt_items) = VALUE string_table(
( |Item-A| ) ( |Item-B| ) ( |Item-C| ) ).
DATA(lv_unit_price) = CONV p( '99.90' ).
DATA(lv_qty) = 3.
" 표현식 삽입: 총액 계산 + 아이템 개수 + 메서드 호출
DATA(lv_summary) =
|PurchaseRequisition contains { lines( lt_items ) } items, | &&
|total = { lv_unit_price * lv_qty }, | &&
|raised by { to_upper( get_customer_name( |C-0001| ) ) }.|.
cl_demo_output=>display( lv_summary ).
ENDMETHOD.
METHOD get_customer_name.
rv_name = COND string(
WHEN iv_id = |C-0001| THEN |globex trading|
ELSE |unknown| ).
ENDMETHOD.
ENDCLASS.
결과 문자열은 대략 PurchaseRequisition contains 3 items, total = 299.70, raised by GLOBEX TRADING. 이 된다. lines( ) 내장 함수, 산술식 lv_unit_price * lv_qty, 그리고 to_upper( get_customer_name( ... ) ) 라는 이중 함수 호출이 모두 중괄호 하나에 들어간다. 이 정도 조합을 CONCATENATE로 옮기려면 임시 변수 두세 개가 추가로 필요하다는 걸 상상하면 차이가 실감난다.
7. 방법 3 · 포맷 지정자와 조건부 삽입: 실무 로그의 마무리
세 번째 형태는 값의 표현 방식을 제어하는 것이다. 자주 쓰는 지정자는 다음과 같다.
ALPHA = IN/OUT— 앞자리 0 채움/제거 (문서 번호, 고객 번호)DATE = USER/ISO/ENVIRONMENT— 날짜 포맷TIME = USER/ISO— 시간 포맷NUMBER = USER/ENVIRONMENT— 천 단위 구분자와 소수점WIDTH = n,ALIGN = LEFT|RIGHT|CENTER,PAD = '.'— 폭·정렬·채움 문자
METHOD build_audit_line.
DATA(lv_cust_id_raw) = |1000000042|.
DATA(lv_posted_on) = sy-datum.
DATA(lv_posted_at) = sy-uzeit.
DATA(lv_amount) = CONV p( '1234567.89' ).
DATA(lv_is_urgent) = abap_true.
DATA(lv_audit) =
|Customer { lv_cust_id_raw ALPHA = OUT WIDTH = 10 ALIGN = RIGHT } | &&
|on { lv_posted_on DATE = USER } { lv_posted_at TIME = USER } | &&
|amount = { lv_amount NUMBER = USER } | &&
|priority = { COND string( WHEN lv_is_urgent = abap_true
THEN |URGENT|
ELSE |NORMAL| ) }|.
rv_line = lv_audit.
ENDMETHOD.
여기서 두 가지를 눈여겨보면 된다. 첫째, ALPHA = OUT이 0000042 같은 앞자리 0을 제거해 사람이 읽기 좋은 형태로 만들어 준다. 반대로 DB 조회 키를 만들 때는 ALPHA = IN을 써서 42 → 0000000042로 만든다. 둘째, COND string( ... )을 그대로 중괄호 안에 넣어서 조건부 문자열을 삽입한다. IF ... ELSE ... ENDIF 세 줄이 한 줄로 정리되는 순간이다.
8. 실무 시나리오: 판매 주문 확인 이메일 본문 생성
세 방법을 조합해 실무 코드로 마무리한다. 판매 주문 처리 후 담당자에게 보낼 이메일 본문을 조립하는 유스케이스다. 에러 처리, 트레이스 로깅, 그리고 프로덕션 관점의 성능 팁까지 포함했다.
METHOD compose_so_confirmation.
TRY.
IF is_header-so_id IS INITIAL.
RAISE EXCEPTION TYPE zcx_stmpl_error
EXPORTING textid = zcx_stmpl_error=>empty_so.
ENDIF.
DATA(lv_total) = REDUCE p( INIT sum = 0
FOR ls IN it_items
NEXT sum = sum + ls-net_amount ).
DATA(lv_header_line) =
|Dear { is_header-customer_name },| && cl_abap_char_utilities=>cr_lf &&
|your order { is_header-so_id ALPHA = OUT } dated | &&
|{ is_header-created_on DATE = USER } has been accepted.|.
DATA lv_item_block TYPE string.
LOOP AT it_items INTO DATA(ls_item).
lv_item_block = lv_item_block &&
| { sy-tabix WIDTH = 3 ALIGN = RIGHT }. | &&
|{ ls_item-material WIDTH = 18 ALIGN = LEFT PAD = '.' } | &&
|qty { ls_item-quantity NUMBER = USER } x | &&
|{ ls_item-net_amount NUMBER = USER } | &&
|{ ls_item-currency }| &&
cl_abap_char_utilities=>cr_lf.
ENDLOOP.
DATA(lv_footer) =
|Total: { lv_total NUMBER = USER } { is_header-currency }.| &&
cl_abap_char_utilities=>cr_lf &&
|Delivery: { COND string(
WHEN is_header-express = abap_true
THEN |Express (within 24h)|
ELSE |Standard (3-5 business days)| ) }|.
rv_body = lv_header_line
&& cl_abap_char_utilities=>cr_lf
&& cl_abap_char_utilities=>cr_lf
&& lv_item_block
&& cl_abap_char_utilities=>cr_lf
&& lv_footer.
CATCH cx_sy_arithmetic_error INTO DATA(lx_arith).
RAISE EXCEPTION TYPE zcx_stmpl_error
EXPORTING previous = lx_arith
textid = zcx_stmpl_error=>calc_failed.
ENDTRY.
ENDMETHOD.
실전 관점에서 짚어둘 포인트가 세 가지 있다. (1) 루프 안에서 문자열을 이어 붙일 때는 && 대신 내부 string_table에 APPEND하고 CONCATENATE LINES OF ... INTO ... SEPARATED BY로 마지막에 한 번만 결합하는 편이 대량 데이터에서 훨씬 빠르다. (2) 예제 코드의 lv_item_block = lv_item_block && ... 패턴은 수백 라인 이하에서는 편리하지만, 수만 건 처리에서는 O(n²) 복사가 발생한다. (3) ALPHA = OUT, DATE = USER는 사용자 로케일을 따르므로, 배치 잡에서 특정 형식을 강제하고 싶다면 ENVIRONMENT가 아닌 ISO를 명시하는 편이 안전하다.
9. 자주 밟는 지뢰와 대처 FAQ
Q1. 파이프 문자 자체를 문자열 안에 넣고 싶은데 신택스 에러가 납니다. 파이프와 중괄호는 이스케이프해야 한다. \|, \{, \}를 쓴다. 예: DATA(lv_x) = |A \| B \{ C \}|. 결과는 A | B { C }가 된다.
Q2. WRITE lv_amount와 결과가 다르게 나옵니다. 문자열 템플릿의 기본 변환은 WRITE와 다르다. WRITE는 리스트 처리기 기준으로 부호를 오른쪽에 붙이지만, 템플릿은 내부 표현(C 스타일)을 따른다. 화면과 동일하게 맞추고 싶다면 NUMBER = USER, 부호 위치가 중요하면 SIGN = LEFT 같은 지정자를 명시한다.
Q3. 대용량 로그를 생성하는 배치에서 문자열 템플릿을 썼더니 오히려 느려졌습니다. 앞서 언급한 O(n²) 이어 붙이기가 원인일 가능성이 높다. string_table에 각 줄을 APPEND하고 마지막에 CONCATENATE LINES OF lt INTO lv SEPARATED BY cl_abap_char_utilities=>newline으로 한 번에 합치자. 문자열 템플릿은 한 줄 구성에만 쓰고 줄 결합은 다른 수단으로 분리하는 것이 일반적으로 안전하다.
Q4. 상수를 정의할 때 |abc|를 쓸 수 있나요? 아니다. CONSTANTS 선언에는 리터럴 'abc'만 허용된다. 문자열 템플릿은 런타임 식이므로 상수의 값으로 쓸 수 없다. 상수는 작은따옴표, 동적 조립은 파이프로 역할을 나누자.
Q5. 유닛 테스트에서 로케일에 따라 결과가 바뀝니다. DATE = USER, NUMBER = USER는 사용자 파라미터에 종속된다. 테스트 코드에서는 DATE = ISO, NUMBER = RAW처럼 로케일 독립 지정자를 사용하고, 예상값 문자열도 그 기준으로 하드코딩하는 것을 권장한다.
10. 더 깊이 파고들 주제
문자열 템플릿이 손에 익으면 자연스럽게 함께 살펴볼 주제가 몇 개 있다. 첫째, REDUCE, FOR, COND, SWITCH 같은 표현식형 ABAP 문법이다. 이들과 결합하면 로그·메시지 조립이 선언적으로 표현된다. 둘째, 메시지 클래스(MESSAGE ... INTO)와 cl_message_helper다. 국제화가 필요한 텍스트는 파이프 대신 T100 메시지를 쓰는 편이 유지보수에 유리하다. 셋째, RAP(RESTful ABAP Programming Model)의 MESSAGE 반환 패턴이다. 서비스에서 응답 메시지를 돌려줄 때 문자열 템플릿과 T100을 조합해 쓰는 사례가 많다. 넷째, ABAP Cloud 환경에서는 XCO 라이브러리가 별도의 문자열 빌더 API를 제공하는데, 짧은 조립은 여전히 파이프가 편하고, 복잡한 URL/JSON 조립은 XCO 쪽이 안전하다.
11. 관련 공식 문서
댓글 0
아직 댓글이 없습니다.