개요 및 이 글에서 다루는 것
오래된 ABAP 코드는 대부분 7.02 이전 문법으로 작성되어 있어, 변수 선언이 상단에 몰려 있거나 내부 테이블 처리에 LOOP...MODIFY 패턴이 반복됩니다. ABAP 7.4부터 도입된 New Syntax는 이러한 코드를 절반 이하 라인으로 줄이면서 가독성과 성능을 개선합니다. 이 글은 판매오더(SalesOrder) 처리 코드를 소재로, 실전에서 검증된 3단계 점진적 현대화 접근을 다룹니다.
- 인라인 선언(
DATA(...),FIELD-SYMBOL(...))으로 변수 블록 제거 - String Template과
CONV로 문자열 조립·형변환 정리 VALUE,FOR,FILTER,REDUCE로 내부 테이블 처리 재작성- ABAP 7.4 / 7.5 / 7.54 / ABAP Cloud 각 릴리스에서의 지원 범위 확인
- 리팩토링 시 발생하는 성능·타입 추론 관련 주의점
알고 있으면 좋은 배경
이 글의 코드 예제를 이해하려면 기본 ABAP OO 문법과 내부 테이블(STANDARD TABLE, SORTED TABLE, HASHED TABLE) 구조, Open SQL의 SELECT ... INTO TABLE 패턴에 익숙해야 합니다. ADT(ABAP Development Tools in Eclipse) 사용 경험이 있으면 Quick Fix 기능으로 리팩토링 속도를 크게 높일 수 있습니다.
환경, 릴리스, 사전 준비
New Syntax는 릴리스별로 지원 범위가 다르므로 대상 시스템 확인이 우선입니다. 일반적으로 다음 매트릭스를 기준으로 판단합니다.
| 릴리스 | 주요 지원 기능 |
|---|---|
| ABAP 7.02 이하 | 구문법(Classic Syntax)만 사용 |
| ABAP 7.40 SP02+ | Inline Declaration, Constructor Expressions(VALUE, NEW, CONV), String Templates |
| ABAP 7.40 SP05+ | FOR, REDUCE, COND, SWITCH |
| ABAP 7.50+ | FILTER, CORRESPONDING ... MAPPING, Open SQL 확장 |
| ABAP 7.54 / Cloud | RAP, ABAP SQL, Released API 제한적 접근 |
준비물: SAP GUI 대신 ADT(Eclipse + ABAP plugin), 대상 시스템의 SY-SAPRL이나 SAP 노트 확인, 리팩토링 대상 프로그램의 최신 백업. ABAP Cloud(Steampunk, Embedded Steampunk)에서는 Released 오브젝트 외에는 접근이 제한되므로, On-Premise 리팩토링과 다른 규칙이 적용됩니다.
핵심 개념: 왜 New Syntax인가
Classic ABAP는 "먼저 선언, 나중에 사용"이라는 절차적 스타일을 강제합니다. 그 결과 500라인 프로그램에서 상단 100라인이 DATA 블록으로 채워지고, 하나의 변수가 어디에서 처음 사용되는지 추적하기 어렵습니다. New Syntax는 표현식 지향(expression-oriented) 스타일을 도입해 "필요한 순간에 선언·계산·할당"을 한 줄로 처리합니다.
비유하자면 Classic 방식은 요리 재료를 전부 조리대에 늘어놓고 시작하는 것이고, New Syntax는 필요한 순간마다 냉장고에서 꺼내 바로 사용하는 방식입니다. 재료가 어디서 왔는지 명확하고, 조리대(스택)가 지저분해지지 않습니다.
표현식 지향의 핵심은 세 가지 축입니다. Inline Declaration(사용처에서 선언), Constructor Expression(
VALUE #( ),NEW #( ),CONV #( )로 즉시 인스턴스화), Iteration Expression(FOR,REDUCE,FILTER로 루프를 표현식으로 축약).
중요한 점은 New Syntax가 단순한 "문법 설탕"이 아니라는 것입니다. 컴파일러가 타입을 추론하고, 임시 변수의 생명주기를 좁혀 메모리 사용을 최적화할 수 있습니다. 다만 무분별한 사용은 오히려 가독성을 해치므로, 3단계로 점진 적용하는 것이 권장됩니다.
1단계: 인라인 선언으로 변수 블록 정리
가장 먼저 손대야 할 것은 상단의 DATA 블록입니다. 판매오더 헤더를 조회해 특정 고객의 오더만 필터링하는 전형적인 코드를 살펴봅니다.
* Before: 7.02 스타일
DATA: lt_orders TYPE STANDARD TABLE OF vbak,
ls_order TYPE vbak,
lv_customer TYPE kunnr,
lv_count TYPE i.
lv_customer = '0000012345'.
SELECT * FROM vbak
INTO TABLE lt_orders
WHERE kunnr = lv_customer
AND erdat GE sy-datum - 30.
LOOP AT lt_orders INTO ls_order.
lv_count = lv_count + 1.
ENDLOOP.
WRITE: / 'Order count:', lv_count.
1단계 리팩토링에서는 사용처에서 선언하고, WRITE를 걷어냅니다.
* After 1단계: Inline Declaration
CONSTANTS lc_customer TYPE kunnr VALUE '0000012345'.
SELECT * FROM vbak
INTO TABLE @DATA(lt_orders)
WHERE kunnr = @lc_customer
AND erdat GE @( sy-datum - 30 ).
DATA(lv_count) = lines( lt_orders ).
cl_demo_output=>display( |Order count: { lv_count }| ).
@DATA(lt_orders)는 Open SQL의 이스케이프 문자 @와 결합된 인라인 선언입니다. lines( ) 빌트인 함수는 LOOP + COUNT 패턴을 완전히 대체합니다. 이 단계만 적용해도 라인 수가 30% 이상 줄어드는 경우가 흔합니다.
2단계: 문자열 처리와 형변환 현대화
2단계는 CONCATENATE, WRITE ... TO, MOVE 같은 문자열/형변환 명령을 String Template과 CONV로 교체합니다. 실무에서는 로그 메시지·이메일 본문 조립에서 특히 효과가 큽니다.
* Before: 문자열 조립과 형변환
DATA: lv_msg TYPE string,
lv_amount_c TYPE char20,
lv_date_c TYPE char10.
WRITE ls_order-netwr TO lv_amount_c CURRENCY ls_order-waerk.
WRITE ls_order-erdat TO lv_date_c.
CONCATENATE 'Order' ls_order-vbeln
'created on' lv_date_c
'amount' lv_amount_c ls_order-waerk
INTO lv_msg SEPARATED BY space.
* 에러 로깅
IF sy-subrc <> 0.
MESSAGE e001(zsd) WITH ls_order-vbeln INTO lv_msg.
CALL FUNCTION 'BAL_LOG_MSG_ADD_FREE_TEXT'
EXPORTING
i_s_msg = VALUE bal_s_msg( msgty = 'E' msgid = 'ZSD'
msgno = '001' msgv1 = ls_order-vbeln ).
ENDIF.
New Syntax로 재작성하면 다음과 같습니다. String Template의 포맷 지시자(CURRENCY, DATE)를 활용하는 것이 핵심입니다.
* After 2단계: String Template + CONV + VALUE
LOOP AT lt_orders ASSIGNING FIELD-SYMBOL(<fs_order>).
DATA(lv_msg) = |Order { <fs_order>-vbeln } | &&
|created on { <fs_order>-erdat DATE = USER } | &&
|amount { <fs_order>-netwr CURRENCY = <fs_order>-waerk } | &&
|{ <fs_order>-waerk }|.
TRY.
process_order( iv_vbeln = <fs_order>-vbeln
iv_amount = CONV dmbtr( <fs_order>-netwr ) ).
CATCH cx_sy_conversion_error INTO DATA(lx_conv).
MESSAGE ID 'ZSD' TYPE 'E' NUMBER '001'
WITH <fs_order>-vbeln lx_conv->get_text( )
INTO DATA(lv_err).
APPEND VALUE #( msgty = 'E' msgid = 'ZSD' msgno = '001'
msgv1 = <fs_order>-vbeln
msgv2 = CONV symsgv( lx_conv->get_text( ) ) )
TO mt_log.
ENDTRY.
ENDLOOP.
ASSIGNING FIELD-SYMBOL(<fs>)는 LOOP AT ... INTO보다 성능상 유리한데, 구조체 복사를 피하기 때문입니다. VALUE #( )는 로그 구조체를 즉석에서 생성해 APPEND합니다. 예외 객체를 INTO DATA(lx_conv)로 받는 패턴은 7.4 SP08 이후 지원됩니다.
3단계: 내부 테이블 처리 전면 재작성
3단계는 가장 큰 효과가 나는 구간이자, 가장 신중해야 하는 구간입니다. LOOP 안에서 조건 판단과 변형을 하는 코드를 FILTER, FOR, REDUCE로 재작성합니다. 실전 시나리오: 판매오더 목록에서 특정 판매조직만 필터링하고, 금액을 EUR로 환산한 결과 테이블을 만들며, 총 합계를 계산합니다.
* Before: LOOP 기반 처리 (레거시)
DATA: lt_result TYPE STANDARD TABLE OF ty_order_summary,
ls_result TYPE ty_order_summary,
lv_total TYPE dmbtr,
lv_rate TYPE kurst.
LOOP AT lt_orders INTO ls_order.
IF ls_order-vkorg = '1000' AND ls_order-netwr > 0.
CLEAR ls_result.
ls_result-vbeln = ls_order-vbeln.
ls_result-kunnr = ls_order-kunnr.
CALL FUNCTION 'CONVERT_TO_LOCAL_CURRENCY'
EXPORTING
date = ls_order-erdat
foreign_amount = ls_order-netwr
foreign_currency = ls_order-waerk
local_currency = 'EUR'
IMPORTING
local_amount = ls_result-amount_eur.
APPEND ls_result TO lt_result.
lv_total = lv_total + ls_result-amount_eur.
ENDIF.
ENDLOOP.
SORT lt_result BY amount_eur DESCENDING.
New Syntax로 재작성한 프로덕션 수준 코드입니다. FILTER는 소스 테이블이 SORTED/HASHED이거나 별도 키 지정 시 사용 가능하므로, 일반 STANDARD TABLE에는 FOR ... WHERE가 실용적입니다.
* After 3단계: 표현식 기반 처리
CLASS-METHODS convert_to_eur
IMPORTING is_order TYPE vbak
RETURNING VALUE(rv_amount) TYPE dmbtr.
* Method Body
DATA(lt_result) = VALUE tt_order_summary(
FOR <order> IN lt_orders
WHERE ( vkorg = '1000' AND netwr > 0 )
( vbeln = <order>-vbeln
kunnr = <order>-kunnr
amount_eur = convert_to_eur( <order> ) ) ).
SORT lt_result BY amount_eur DESCENDING.
DATA(lv_total) = REDUCE dmbtr(
INIT sum = 0
FOR <row> IN lt_result
NEXT sum = sum + <row>-amount_eur ).
" 성능 로깅
DATA(lv_runtime) = cl_abap_runtime=>create_hr_timer( ).
cl_demo_output=>display(
|Processed { lines( lt_result ) } orders, | &&
|total EUR { lv_total NUMBER = USER }| ).
이 코드는 세 가지 프로덕션 관점을 반영합니다. 첫째, 환율 변환을 별도 메서드로 캡슐화해 단위 테스트(ABAP Unit)에서 목업 가능하도록 했습니다. 둘째, REDUCE는 초기값과 누산 로직을 명시해 병렬 처리 안전성을 확보합니다. 셋째, 성능이 중요한 대량 처리에서는 여전히 LOOP + ASSIGNING이 유리한 경우가 있으므로, SAT(Runtime Analysis)로 실측 후 판단해야 합니다.
흔한 실수와 트러블슈팅
Q1. VALUE #( )에서 #은 무엇인가요?
#은 "컨텍스트에서 타입을 추론하라"는 지시자입니다. 대상 변수의 타입이 명확할 때만 사용 가능하며, 모호한 경우 VALUE ty_order_summary( ... )처럼 명시적 타입이 필요합니다. @DATA로 받은 인라인 변수 안에서는 추론이 불가능하므로 반드시 타입을 명시해야 합니다.
Q2. FOR ... IN itab WHERE ( ... )가 성능상 LOOP + WHERE보다 느린가요?
일반적으로 컴파일러가 유사한 수준으로 최적화하지만, WHERE 조건이 복잡한 표현식이면 오히려 느려질 수 있습니다. 100만 건 이상 처리에서는 SORTED TABLE의 키 접근을 활용한 LOOP가 유리한 경우가 많으니, SAT로 벤치마크한 뒤 결정하는 것이 권장됩니다.
Q3. ABAP Cloud에서 인라인 선언은 되는데 특정 함수 모듈이 컴파일 오류입니다.
ABAP Cloud(Steampunk)는 Released API만 허용합니다. CONVERT_TO_LOCAL_CURRENCY 같은 클래식 함수 모듈은 대부분 Released 상태가 아니므로, CL_EXCHANGE_RATE나 CDS 기반 Analytical Query로 대체해야 합니다. New Syntax 자체는 완벽히 지원됩니다.
Q4. DATA(lv_x) = ... 이후 같은 이름을 다시 인라인 선언하면?
컴파일 오류가 납니다. 인라인 선언은 스코프 내에서 유일해야 하며, 재할당 시에는 lv_x = ...만 씁니다. IF/LOOP 블록 안에서 선언하면 블록 종료 후에도 접근 가능한 점(스코프가 서브루틴/메서드 전체)에 주의합니다.
Q5. 리팩토링 후 코드 리뷰에서 "읽기 어렵다"는 피드백이 나옵니다.
표현식 중첩이 3단계를 넘으면 대부분 가독성이 급락합니다. REDUCE 안에 FILTER 안에 VALUE를 넣는 대신, 중간 변수 하나를 DATA(lt_tmp) = ...로 분리하는 편이 유지보수에 유리합니다.
이후 확장할 만한 주제
3단계 리팩토링을 완료했다면 다음 확장을 고려할 수 있습니다. RAP(RESTful ABAP Programming Model)로 트랜잭션 로직을 Behavior Definition으로 이전, CDS View + Access Control로 Open SQL 대체, ABAP Unit + Test Doubles로 리팩토링 안전망 구축, Code Inspector와 ATC(ABAP Test Cockpit)의 Modern Syntax 체크 프로파일 활성화 등이 자연스러운 후속 작업입니다. 또한 abapGit으로 리팩토링 이력을 관리하면 롤백과 리뷰가 훨씬 수월해집니다.
더 읽어볼 문서
- SAP Help Portal - ABAP Keyword Documentation (Latest)
- help.sap.com - ABAP Expressions Overview (7.52)
- help.sap.com - Inline Declarations
- help.sap.com - ABAP Platform Cloud / Released API Concept
- SAP Community - ABAP New Syntax 태그 아티클
- SAP Developers - ABAP Environment Trial 시작하기
댓글 0
아직 댓글이 없습니다.