소개: DATA만 쓰던 시절을 넘어서
ABAP을 처음 배울 때 변수 선언은 거의 자동 반사처럼 DATA로 시작합니다. 필드 하나가 필요하면 DATA lv_amount TYPE p DECIMALS 2., 내부 테이블이 필요하면 DATA lt_items TYPE TABLE OF ..., 모두 DATA였죠. 그러나 ABAP 7.40 SP08 이후 인라인 선언이 보편화되고, 7.54 무렵부터 FINAL 키워드가 정식으로 도입되면서 상황이 달라졌습니다. 이제는 "이 변수는 한 번 값을 정하면 더는 바뀌어선 안 된다"는 의도를 컴파일러 수준에서 강제할 수 있게 되었습니다.
이 글에서는 신입 ABAP 개발자가 가장 흔하게 저지르는 실수 — 모든 것을 DATA로 선언해서 의도치 않게 값이 바뀌어버리는 버그 — 를 중심으로 FINAL과 DATA의 사용처를 구분합니다. 단순한 키워드 차이를 넘어, 코드 리뷰에서 동료에게 "왜 여기는 FINAL을 안 썼나요?"라는 질문이 나오지 않도록 의도 표현(intent expression)의 관점에서 정리합니다.
난이도는 입문자 수준이지만, S/4HANA Cloud 또는 On-Premise 2022 이상 환경에서 RAP(RESTful ABAP Programming Model)을 다루는 분이라면 이 글에서 다루는 패턴이 곧바로 실무 코드 품질에 반영됩니다.
이 글에서 다루는 내용
- FINAL 키워드의 도입 배경과 불변성(immutability) 개념 이해
- DATA, FINAL, CONSTANTS 세 키워드의 명확한 구분
- 실수로 DATA를 쓰는 전형적인 패턴 3가지
- FINAL이 적합한 실전 시나리오 (설정값, 루프 헤더, 계산 결과)
- 인라인 선언과 결합한 모던 ABAP 스타일
- 코드 리뷰에서 활용할 수 있는 판단 기준
이 글을 읽기 전에
이 글은 ABAP 기본 문법(DATA, TYPES, METHODS)과 LOOP, IF 같은 제어문을 한 번이라도 작성해 본 분을 대상으로 합니다. 추가로 ABAP 7.40 이후의 인라인 선언(DATA(lv_x) = ...) 문법에 익숙하면 이해가 빠릅니다. 객체지향(클래스/메서드 구조)을 살짝이라도 만져봤다면 "메서드 내 지역 변수의 생명주기"라는 표현이 더 자연스럽게 와닿을 것입니다. CDS View나 RAP 비즈니스 객체를 다뤄본 경험이 있다면 후반부의 실전 예제가 익숙하게 느껴질 수 있지만, 필수는 아닙니다.
환경 및 버전 정보
FINAL 키워드는 ABAP 7.54(SAP NetWeaver AS ABAP 7.54, S/4HANA 2020) 부터 인라인 선언 형태로 정식 지원됩니다. 따라서 다음 환경 중 하나를 권장합니다.
- SAP BTP, ABAP Environment (Steampunk) — 항상 최신 ABAP 릴리즈가 제공되므로 FINAL이 기본 지원됩니다.
- S/4HANA On-Premise 2020 이상 — 일반적으로 FINAL 인라인 선언 사용 가능.
- S/4HANA Cloud (Public/Private Edition) — 클린 ABAP 가이드라인이 강하게 권장되며 FINAL 활용도가 높습니다.
개발 도구는 ABAP Development Tools(ADT) for Eclipse 또는 Business Application Studio(BAS)의 ABAP Environment 플러그인을 사용하면 인라인 선언 자동완성과 코드 검사기(ATC) 경고가 모두 활성화되어 학습 효과가 큽니다. SAP GUI의 SE38/SE80에서는 일부 자동완성이 제한적일 수 있어 ADT 환경을 우선 권장합니다.
버전이 7.54 미만이라면 FINAL 키워드 자체가 인식되지 않아 컴파일 오류가 발생합니다. 이 경우 READ-ONLY 메서드 파라미터나 CONSTANTS로 우회하는 패턴을 사용해야 합니다.
핵심 개념: 가변, 불변, 그리고 컴파일 타임 상수
ABAP에서 "값을 담는 그릇"은 크게 세 종류가 있습니다. 그릇의 성격을 음식 보관 용기에 비유하면 이해가 쉽습니다.
- DATA — 매일 메뉴가 바뀌는 도시락통입니다. 아침엔 김밥, 점심엔 비빔밥, 저녁엔 파스타. 언제든 비우고 다시 채울 수 있습니다.
- FINAL — 한 번 음식을 담으면 봉인되는 진공포장입니다. 처음 담을 때까지는 비어 있어도 되지만, 일단 채워지면 그 안의 내용물은 절대 바뀌지 않습니다. 그러나 봉지 자체는 메서드를 실행할 때마다 새로 만들어집니다.
- CONSTANTS — 공장에서 미리 라벨까지 인쇄해 출고되는 통조림입니다. 컴파일 시점에 이미 내용물이 확정되어 있고, 프로그램 전체에서 동일한 값을 공유합니다.
DATA는 가변(mutable), FINAL은 일회 할당 불변(write-once immutable), CONSTANTS는 컴파일 타임 상수(compile-time constant)로 정리할 수 있습니다. 셋 다 "변하지 않는 값"을 표현할 때 후보에 오르지만, 의도하는 바가 다릅니다.
예를 들어 부가가치세율 10%처럼 시스템 전체에서 절대 변하지 않고 컴파일 시 이미 알려진 값은 CONSTANTS가 적합합니다. 반면 사용자가 입력한 환율을 한 번 읽어와 메서드 내에서 여러 곳에 활용하되 그 사이에 누가 실수로 덮어쓰면 안 되는 경우라면 FINAL이 정답입니다. 누적 합계를 계산하면서 LOOP 안에서 값을 계속 더해야 한다면 당연히 DATA를 써야 합니다.
FINAL의 진짜 가치는 코드를 읽는 사람에게 보내는 신호에 있습니다. FINAL(lv_total_amount) = calculate_total( ... ).라고 적힌 한 줄은 "이 값은 이 줄 이후로는 절대 변하지 않습니다. 안심하고 참조하세요"라는 명시적 약속입니다. 컴파일러는 이 약속을 강제로 검증해주고, 누군가 실수로 재할당을 시도하면 즉시 오류를 발생시킵니다.
도식으로 정리하면 다음과 같습니다.
" | 선언 시점 | 재할당 | 적용 범위 | 시점
" DATA | 런타임 | 가능 | 블록/메서드 | 매번 새로 생성
" FINAL | 런타임 | 불가 | 블록/메서드 | 매번 새로 생성, 1회 고정
" CONSTANTS | 컴파일타임 | 불가 | 클래스/프로그램 | 컴파일 시 확정
기본 동작 비교 실전 예제
가장 단순한 형태로 세 키워드의 동작 차이를 확인해봅니다. 사내 결재 시스템에서 최대 결재 한도를 조회하는 시나리오입니다.
CLASS zcl_approval_demo DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS run.
PRIVATE SECTION.
CONSTANTS co_currency TYPE waers VALUE 'KRW'.
ENDCLASS.
CLASS zcl_approval_demo IMPLEMENTATION.
METHOD run.
" 1) DATA: 자유롭게 변경 가능
DATA lv_running_total TYPE p LENGTH 13 DECIMALS 2 VALUE 0.
lv_running_total = lv_running_total + 1000.
lv_running_total = lv_running_total + 2500. " OK, 누적 합산 의도
" 2) FINAL: 최초 1회 할당, 이후 변경 불가
FINAL(lv_approval_limit) = COND p( WHEN sy-uname = 'MANAGER01'
THEN 5000000
ELSE 1000000 ).
" lv_approval_limit = 9999999. <-- 컴파일 오류 발생
" 3) CONSTANTS: 컴파일 타임 고정값
WRITE: / 'Currency :', co_currency,
/ 'Total :', lv_running_total,
/ 'Limit :', lv_approval_limit.
ENDMETHOD.
ENDCLASS.
주석 처리된 lv_approval_limit = 9999999. 줄의 주석을 풀면 ABAP 컴파일러가 즉시 "FINAL 변수는 재할당할 수 없습니다"라는 취지의 오류를 표시합니다. 이것이 바로 의도된 안전장치입니다. lv_running_total은 명시적으로 누적이 필요하므로 DATA가 맞고, lv_approval_limit은 한 번 계산된 후 그대로 사용해야 하므로 FINAL이 적절합니다.
실무 시나리오 — 영업주문 가격 계산
실무에서 가장 자주 마주치는 패턴은 "한 번 조회한 마스터 데이터를 메서드 내내 참조하는 경우"입니다. 영업주문 라인별 부가세 포함 금액을 계산하는 예제로 보겠습니다.
METHOD calculate_order_totals.
" 입력: it_order_items (영업주문 아이템 내부 테이블)
" 출력: rs_summary (요약 구조)
" 환율은 메서드 시작 시 한 번만 조회. 이후 절대 바뀌면 안 됨.
FINAL(lv_exchange_rate) = zcl_fx_service=>get_daily_rate(
iv_from = 'USD'
iv_to = 'KRW'
iv_date = sy-datum ).
" 세율 테이블도 메서드 실행 동안 고정
FINAL(lt_tax_rates) = zcl_tax_repo=>read_active_rates( sy-datum ).
" 누적 합계는 LOOP에서 계속 더해야 하므로 DATA
DATA lv_subtotal TYPE p LENGTH 13 DECIMALS 2 VALUE 0.
DATA lv_tax_sum TYPE p LENGTH 13 DECIMALS 2 VALUE 0.
LOOP AT it_order_items INTO DATA(ls_item).
" 각 라인의 환산 금액 — 라인 단위 1회 계산이므로 FINAL
FINAL(lv_line_krw) = ls_item-amount_usd * lv_exchange_rate.
" 세율 조회 결과도 라인별 1회 — FINAL
FINAL(lv_rate) = VALUE #( lt_tax_rates[ country = ls_item-country ]-rate
DEFAULT 0 ).
lv_subtotal = lv_subtotal + lv_line_krw.
lv_tax_sum = lv_tax_sum + lv_line_krw * lv_rate / 100.
" 로깅 (BAL 또는 단순 메시지)
MESSAGE i001(zorder) WITH ls_item-item_no lv_line_krw INTO DATA(lv_dummy).
ENDLOOP.
rs_summary-subtotal = lv_subtotal.
rs_summary-tax = lv_tax_sum.
rs_summary-total = lv_subtotal + lv_tax_sum.
" 예외 처리: 환율 조회 실패 시
IF lv_exchange_rate IS INITIAL.
RAISE EXCEPTION TYPE zcx_order_calc
EXPORTING textid = zcx_order_calc=>fx_rate_missing.
ENDIF.
ENDMETHOD.
이 예제에서 주목할 점은 LOOP 안에서도 FINAL이 자연스럽게 활용된다는 것입니다. 매 루프 반복마다 lv_line_krw와 lv_rate는 새로 선언되고 즉시 값이 고정됩니다. 누군가 LOOP 본문에서 실수로 lv_line_krw = 0.이라고 적어 디버깅을 시도해도 컴파일러가 막아줍니다. 반면 lv_subtotal과 lv_tax_sum은 루프 외부에서 선언되고 반복마다 누적되어야 하므로 DATA가 올바른 선택입니다.
RAP Behavior Implementation 실전 적용
RAP(RESTful ABAP Programming Model)에서 Behavior Implementation을 작성할 때 FINAL의 가치는 더욱 빛납니다. 동시성 이슈와 부수효과를 줄이는 데 도움이 됩니다.
CLASS lhc_invoice IMPLEMENTATION.
METHOD validate_amount.
" 임계값은 커스터마이징 테이블에서 1회 조회
FINAL(lv_threshold) = zcl_inv_config=>get_max_amount( ).
" 입력된 인보이스 키 집합 — 변경 금지
FINAL(lt_keys) = keys.
READ ENTITIES OF zi_invoice IN LOCAL MODE
ENTITY Invoice
FIELDS ( InvoiceID GrossAmount Currency )
WITH CORRESPONDING #( lt_keys )
RESULT FINAL(lt_invoices).
LOOP AT lt_invoices INTO DATA(ls_inv).
IF ls_inv-GrossAmount > lv_threshold.
APPEND VALUE #( %tky = ls_inv-%tky
%msg = new_message(
id = 'ZINV'
number = '010'
severity = if_abap_behv_message=>severity-error
v1 = ls_inv-InvoiceID )
%element-GrossAmount = if_abap_behv=>mk-on
) TO reported-invoice.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD calculate_discount.
" 할인율 정책은 클래스 상수로
CONSTANTS co_vip_discount TYPE p LENGTH 3 DECIMALS 2 VALUE '0.15'.
CONSTANTS co_std_discount TYPE p LENGTH 3 DECIMALS 2 VALUE '0.05'.
LOOP AT keys INTO DATA(ls_key).
FINAL(lv_customer_class) = read_customer_class( ls_key-CustomerID ).
FINAL(lv_applied_rate) = SWITCH p( lv_customer_class
WHEN 'VIP' THEN co_vip_discount
ELSE co_std_discount ).
MODIFY ENTITIES OF zi_invoice IN LOCAL MODE
ENTITY Invoice
UPDATE FIELDS ( DiscountRate )
WITH VALUE #( ( %tky = ls_key-%tky
DiscountRate = lv_applied_rate ) ).
ENDLOOP.
ENDMETHOD.
ENDCLASS.
이 패턴에서는 세 종류 키워드가 각각의 자리를 정확히 차지합니다. 할인율 자체(0.15, 0.05)는 비즈니스 규칙이므로 CONSTANTS, 라인별로 결정되는 적용 할인율은 FINAL, 그리고 RAP 프레임워크가 요구하는 reported 같은 변경 가능 누적 구조는 DATA로 처리됩니다. 단위 테스트(ABAP Unit) 관점에서도 FINAL로 선언된 변수가 많을수록 메서드의 부수효과가 줄어 테스트 케이스가 단순해집니다.
현장에서 자주 마주치는 실수와 대처법
실수 1: 조회한 마스터 데이터를 DATA로 받고 중간에 덮어쓰는 경우
고객 마스터를 SELECT SINGLE로 가져온 뒤 검증 로직 중간에 임시 계산용으로 같은 변수를 재활용하는 사례가 흔합니다. 이런 코드는 200줄짜리 메서드의 100번째 줄에서 원본 데이터가 사라져 있어 디버깅 지옥을 만듭니다. 처음부터 SELECT SINGLE ... INTO @FINAL(ls_customer) 형태로 받으면 컴파일러가 보호해줍니다.
실수 2: CONSTANTS로 충분한 값을 FINAL로 선언
"부가세율 10%"처럼 변하지 않는 값을 메서드 안에서 FINAL(lv_vat) = '0.10'.이라고 적는 경우입니다. 동작은 하지만 매번 메서드 호출 시 변수가 생성됩니다. 클래스 또는 인터페이스 수준의 CONSTANTS로 끌어올리는 것이 일반적으로 권장됩니다.
실수 3: 누적용 변수를 FINAL로 선언
LOOP에서 합계를 더해야 하는 변수를 FINAL로 선언하고 컴파일 오류가 발생하자 "FINAL은 못 쓰는 키워드"라고 결론짓는 사례입니다. 누적은 본질적으로 가변이므로 DATA가 맞습니다. FINAL은 만능이 아니라 "쓸 자리"가 있을 뿐입니다.
자주 묻는 질문
- Q. FINAL은 ABAP 어떤 버전부터 사용 가능한가요? A. 인라인 선언 형태의 FINAL은 ABAP 7.54부터 일반적으로 사용 가능합니다. 이전 버전에서는 컴파일 오류가 발생하므로 시스템 정보(SE38에서
SYST-SAPRL확인)로 먼저 확인하세요. - Q. FINAL과 READ-ONLY 메서드 파라미터는 같은가요? A. 비슷하지만 다릅니다. READ-ONLY는 IMPORTING 파라미터를 메서드 내부에서 수정 못 하게 막는 용도이고, FINAL은 메서드 안의 지역 변수에 적용됩니다.
- Q. 성능 차이가 있나요? A. 일반적으로 런타임 성능 차이는 미미합니다. 다만 컴파일러 최적화 힌트로 작용해 일부 케이스에서 더 효율적인 바이트코드가 생성될 여지가 있습니다.
더 깊이 살펴볼 만한 주제
FINAL을 익혔다면 자연스럽게 클린 ABAP(Clean ABAP) 가이드라인 전반으로 시야를 넓혀보길 권합니다. 특히 인라인 선언과 함께 자주 등장하는 VALUE #( ), COND, SWITCH, REDUCE 같은 표현식 기반 ABAP은 FINAL과 결합할 때 효과가 극대화됩니다. 또한 메서드 시그니처에서 IMPORTING ... TYPE ... READ-ONLY를 활용해 호출자 측 데이터 무결성도 함께 보호하는 패턴을 익히면 좋습니다.
한 단계 더 나아가면 RAP의 unmanaged/managed Behavior Implementation에서 트랜잭션 일관성을 유지하기 위해 FINAL을 어떻게 활용하는지, 그리고 ABAP Unit 테스트에서 불변 입력값을 어떻게 표현하는지로 학습을 확장할 수 있습니다.
댓글 0
아직 댓글이 없습니다.