ABAP

FINAL vs DATA — 아직도 DATA만 쓰나요? #shorts #SAP #ABAP

▶ YouTube에서 보기

개요와 이 글에서 얻어갈 것

ABAP 7.40 이후 변수 선언 방식이 인라인 선언(DATA(...), FINAL(...))으로 확장되면서, 많은 개발자가 습관적으로 DATA만 사용하는 패턴을 유지하고 있습니다. 그러나 ABAP 7.57(SAP S/4HANA 2022 / Cloud Edition)부터 정식 지원되는 FINAL 키워드는 단순한 "또 하나의 선언 방식"이 아니라, 변수의 불변성(immutability)을 컴파일 타임에 강제하는 강력한 도구입니다. 이 글에서는 DATAFINAL의 동작 차이, 실수로 DATA를 쓰는 전형적 사례, 그리고 FINAL이 코드 품질을 어떻게 끌어올리는지 실전 예제로 풀어봅니다.

  • 인라인 DATAFINAL의 의미·범위 차이 이해
  • 재할당 가능성을 컴파일러가 어떻게 검사하는지 파악
  • SalesOrder, InvoiceAmount 등 실무 시나리오에서 적용 기준 수립
  • 리팩토링 시 DATAFINAL 전환 판단 기준
  • ATC(ABAP Test Cockpit) 점검 항목과 연계

읽기 전 알아두면 좋은 것

ABAP의 기본 변수 선언 문법(DATA lv_x TYPE i)과 인라인 선언(DATA(lv_x) = ...) 개념을 익히고 있어야 합니다. CONSTANTS 선언, LOOP AT ... INTO/REFERENCE INTO, READ TABLE 패턴, 그리고 메서드 내 로컬 변수 스코프에 대한 기초가 있으면 이해가 빠릅니다. 객체지향 ABAP에서 메서드 시그니처와 IMPORTING/RETURNING 의미도 알고 있으면 좋습니다.

실습 환경과 버전 요건

FINAL은 일반적으로 ABAP Release 7.57 이상에서 동작합니다. 사용 가능한 환경은 다음과 같이 정리할 수 있습니다.

  • SAP S/4HANA On-Premise 2022 이상 (ABAP Platform 2022, Kernel 7.89+)
  • SAP BTP ABAP Environment (Steampunk) — 클라우드 릴리스에서 기본 지원
  • ABAP Cloud Development Model — RAP(RESTful ABAP Programming Model) 환경 권장
  • ADT (ABAP Development Tools) for Eclipse 3.30+ — 신택스 하이라이팅·Quick Fix 지원
  • ATC 체크 변형: FINAL 권장 룰을 포함한 최신 Code Inspector 변형 사용

S/4HANA 2021 이하 또는 NetWeaver 7.55 이하 시스템에서는 컴파일러가 FINAL을 인식하지 못하므로, 본 글의 코드는 동작하지 않을 수 있습니다. 시스템 메뉴 → 상태에서 커널 릴리스를 확인해 두는 것이 좋습니다.

핵심 개념: 변경 가능성에 대한 컴파일러 약속

DATAFINAL의 본질적 차이는 한 줄로 요약할 수 있습니다. DATA는 "이 변수의 값은 앞으로 바뀔 수 있다"는 가능성을 열어두고, FINAL은 "선언 시점 이후 단 한 번도 바뀌지 않는다"는 약속을 컴파일러에게 명시적으로 전달합니다.

비유하자면, DATA는 화이트보드 위 마커펜 메모와 같습니다. 언제든 지우고 다시 쓸 수 있습니다. 반면 FINAL은 잉크로 새긴 명패에 가깝습니다. 한 번 새기면 같은 명패에 다른 글자를 덧쓰는 행위가 컴파일 단계에서 차단됩니다.

중요한 점은 FINAL이 만들어내는 것은 "읽기 전용 변수"이지 "상수(constant)"가 아니라는 사실입니다. 상수는 CONSTANTS 키워드로 선언하며, 값이 컴파일 타임에 결정되어야 합니다. FINAL은 런타임에 한 번 값이 정해지고, 그 이후 재할당이 금지됩니다.

세 가지 선언 방식의 의미 차이를 표로 정리하면 다음과 같습니다.

키워드값 결정 시점재할당주요 용도
CONSTANTS컴파일 타임불가리터럴 상수 (예: 통화 코드 'KRW')
FINAL(x)런타임 첫 할당불가계산 결과·조회 결과 보관
DATA(x)런타임 첫 할당가능누적·반복 갱신되는 값

컴파일러는 FINAL로 선언된 변수를 다시 할당하려는 모든 시도를 신택스 에러로 처리합니다. 또한 CHANGING 파라미터로 전달하거나, FIELD-SYMBOL에 변경 가능 형태로 바인딩하거나, REFERENCE INTO로 쓰기 참조를 만드는 시도 역시 차단합니다. 즉 "이 값이 절대 안 바뀐다"는 신호가 코드를 읽는 사람뿐 아니라 정적 분석기에도 명확히 전달됩니다.

1단계 — 기본 예제: 단일 메서드 안의 조회 결과

가장 흔한 패턴은 데이터베이스 조회 결과를 받아 후속 로직에서 읽기만 하는 경우입니다. 영업 주문(SalesOrder) 헤더를 조회하는 메서드를 보면, 결과를 한 번 받아서 검증하고 반환할 뿐 재할당하지 않습니다.

METHOD get_sales_order_header.

  " 잘못된 패턴: 변경 의도 없음에도 DATA 사용
  SELECT SINGLE order_id, customer_id, total_amount, currency_code
    FROM zsales_order_hdr
    WHERE order_id = @iv_order_id
    INTO @DATA(ls_order_data).

  " 권장 패턴: FINAL로 불변성 표현
  SELECT SINGLE order_id, customer_id, total_amount, currency_code
    FROM zsales_order_hdr
    WHERE order_id = @iv_order_id
    INTO @FINAL(ls_order_header).

  IF ls_order_header IS INITIAL.
    RAISE EXCEPTION TYPE zcx_order_not_found
      EXPORTING iv_order_id = iv_order_id.
  ENDIF.

  rs_header = ls_order_header.

ENDMETHOD.

이 예제에서 ls_order_header는 조회 직후 검증만 수행하고 반환됩니다. FINAL로 선언하면 누군가 메서드를 수정하면서 실수로 ls_order_header-total_amount = 0처럼 값을 덮어쓰려 해도 컴파일러가 즉시 차단합니다.

2단계 — 실전 시나리오: 루프, 누적, 그리고 분기 처리

실무에서 자주 헷갈리는 지점은 "루프 안에서 사용하는 변수"입니다. 루프 카운터처럼 매 회 갱신되는 변수는 DATA가 맞고, 루프 본체 안에서 한 번 계산되고 그 회차 안에서만 쓰이는 임시 변수는 FINAL이 적절합니다.

METHOD calculate_invoice_totals.

  " 누적 변수: 반복적으로 갱신되므로 DATA가 맞음
  DATA(lv_running_total) = CONV decfloat34( 0 ).
  DATA(lv_line_count)    = 0.

  LOOP AT it_invoice_items INTO DATA(ls_item).

    " 각 회차에서만 유효한 계산값: FINAL이 적절
    FINAL(lv_line_amount) = ls_item-quantity * ls_item-unit_price.
    FINAL(lv_tax_amount)  = lv_line_amount * ls_item-tax_rate / 100.

    " 누적은 DATA 변수에만 수행
    lv_running_total = lv_running_total + lv_line_amount + lv_tax_amount.
    lv_line_count    = lv_line_count + 1.

    " 로깅: FINAL 값을 안전하게 참조
    log_line_calculation(
      iv_item_no    = ls_item-item_number
      iv_net_amount = lv_line_amount
      iv_tax_amount = lv_tax_amount ).

  ENDLOOP.

  rs_summary = VALUE #(
    total_amount = lv_running_total
    item_count   = lv_line_count ).

ENDMETHOD.

여기서 핵심은 루프 본체 안에서 매 회차마다 FINAL(lv_line_amount)가 새로 선언된다는 점입니다. ABAP 인라인 선언은 블록 스코프가 아닌 메서드 스코프이지만, FINAL의 "첫 할당 후 재할당 금지" 의미가 루프 다음 회차의 새 할당을 막지는 않습니다. 실제로는 같은 변수가 반복 할당되는 것이 아니라, 컴파일러가 루프 회차 내에서의 재할당만 검사합니다. 단, 일부 릴리스에서는 동일 메서드 안에서 같은 이름의 FINAL을 두 번 선언하는 것이 허용되지 않으므로, 루프 안 FINAL 사용 시에는 신택스 에러가 발생하는지 ADT에서 즉시 확인하는 것이 안전합니다. 안전한 대안은 헬퍼 메서드로 추출해 각 호출이 자체 스코프를 갖도록 하는 것입니다.

에러 처리 측면에서도 FINAL은 강력합니다. 예외 객체를 잡아 메시지를 추출할 때, FINAL로 받은 메시지 텍스트는 로깅 직전까지 변조될 수 없음이 보장됩니다.

TRY.
    process_invoice( iv_invoice_id = iv_invoice_id ).
  CATCH zcx_invoice_processing INTO FINAL(lo_exception).
    FINAL(lv_error_text) = lo_exception->get_text( ).
    log_error(
      iv_invoice_id = iv_invoice_id
      iv_message    = lv_error_text ).
    RAISE EXCEPTION lo_exception.
ENDTRY.

3단계 — 프로덕션 코드: 성능, 테스트 용이성, 보안

대량 데이터 처리에서 FINAL은 단순히 가독성 도구가 아니라 최적화 힌트로도 작동할 수 있습니다. 컴파일러는 재할당 금지가 명시된 변수를 더 공격적으로 레지스터 캐싱하거나, 참조 전달 시 복사 회피 결정을 내릴 여지가 생깁니다. 다음은 가격 마스터를 조회해 캐시처럼 사용하는 패턴입니다.

METHOD enrich_orders_with_prices.

  " 가격 마스터는 한 번 조회 후 변경되지 않음
  SELECT product_code, list_price, currency_code
    FROM zproduct_price
    FOR ALL ENTRIES IN @it_sales_orders
    WHERE product_code = @it_sales_orders-product_code
    INTO TABLE @FINAL(lt_price_master).

  " 정렬된 테이블로 변환 (한 번만 수행)
  FINAL(lt_price_sorted) = VALUE tt_price_sorted(
    FOR ls_price IN lt_price_master
    ( ls_price ) ).

  " 결과 누적용 변수만 DATA
  DATA lt_enriched TYPE tt_enriched_order.

  LOOP AT it_sales_orders INTO DATA(ls_order).

    READ TABLE lt_price_sorted
      WITH KEY product_code = ls_order-product_code
      INTO FINAL(ls_price_entry).

    IF sy-subrc = 0.
      APPEND VALUE #(
        order_id     = ls_order-order_id
        product_code = ls_order-product_code
        list_price   = ls_price_entry-list_price
        currency     = ls_price_entry-currency_code
        line_total   = ls_order-quantity * ls_price_entry-list_price
      ) TO lt_enriched.
    ENDIF.

  ENDLOOP.

  rt_enriched_orders = lt_enriched.

ENDMETHOD.

단위 테스트(ABAP Unit) 관점에서도 FINAL은 이점이 큽니다. 테스트 코드 내 setup 단계에서 만든 픽스처가 테스트 실행 중 부지불식간에 변경되는 사고를 원천 차단합니다. 특히 권한 체크 결과나 사용자 컨텍스트처럼 보안에 민감한 값은 FINAL로 받아 두면, 검증 후 사용 사이에 값이 바뀌는 TOCTOU(Time-Of-Check to Time-Of-Use) 류 실수를 줄일 수 있습니다.

METHOD authorize_and_post.

  FINAL(lv_current_user)   = cl_abap_context_info=>get_user_technical_name( ).
  FINAL(lv_allowed_company) = get_user_company( lv_current_user ).

  IF iv_target_company <> lv_allowed_company.
    RAISE EXCEPTION TYPE zcx_authorization_failed.
  ENDIF.

  post_document(
    iv_company = lv_allowed_company
    is_doc     = is_document ).

ENDMETHOD.

자주 부딪히는 실수와 해결법

Q1. FINAL로 선언했는데 내부 테이블에 APPEND가 안 되나요?
가능합니다. FINAL이 막는 것은 변수 자체에 대한 재할당(lt_x = ...)이지, 내부 테이블 내용을 수정하는 APPEND, INSERT, MODIFY가 아닙니다. 이는 직관과 어긋날 수 있어 주의가 필요합니다. 진정한 불변 컬렉션을 원한다면 별도의 immutable 래퍼 클래스를 설계해야 합니다.

Q2. 메서드의 RETURNING 파라미터로 받을 때 FINAL을 쓸 수 있나요?
네, FINAL(lv_result) = compute_total( ... ) 형태로 인라인 수신이 가능합니다. 다만 호출 결과를 즉시 다른 로직에서 누적/수정해야 한다면 DATA로 받는 것이 자연스럽습니다. "받아서 한 번 읽고 끝"이면 FINAL이 정답입니다.

Q3. 7.57 미만 시스템에서 비슷한 효과를 내려면?
완전한 컴파일러 강제는 어렵지만, 명명 규칙(예: lv_const_ 접두어)과 ATC 사용자 정의 체크, 코드 리뷰 체크리스트로 의도를 표현할 수 있습니다. 또한 가능한 경우 메서드를 잘게 쪼개 해당 변수의 사용 범위를 좁히면, 사실상 불변에 가까운 효과를 얻습니다.

Q4. CONSTANTS 대신 FINAL을 써도 되나요?
값이 컴파일 타임에 알려진 리터럴(통화 코드 'EUR', 최대 라인 수 999 등)은 반드시 CONSTANTS로 두는 것이 권장됩니다. CONSTANTS는 클래스 단위로 공유되고, 메타데이터 비용도 더 작습니다. FINAL은 런타임 계산 결과의 불변성을 표현하는 용도입니다.

Q5. CHANGING 파라미터에 FINAL 변수를 넘기면?
컴파일 에러가 발생합니다. CHANGING은 호출 측에서 값이 바뀔 수 있음을 전제하므로, 불변 약속과 충돌합니다. 이런 신호가 보이면 설계를 재검토할 시점입니다.

이어서 살펴보면 좋은 주제

이 글에서 다룬 FINAL의 사고방식은 RAP(RESTful ABAP Programming Model)의 read-only 엔티티 설계, ABAP Cloud의 안정 API 원칙과도 맞물립니다. 다음 주제로는 CONSTANTS와 enum-like 패턴, READ-ONLY 속성을 가진 클래스 멤버 설계, ABAP Unit 테스트 더블에서 불변 픽스처 만들기, 그리고 ATC의 "Prefer FINAL over DATA" 권장 체크를 활성화하는 방법을 권장합니다. 또한 ABAP Cloud Best Practices에서 제시하는 "표현된 의도(expressed intent)" 원칙을 함께 학습하면 코드 리뷰 품질이 한 단계 올라갑니다.

더 깊이 읽어볼 자료

댓글 0

아직 댓글이 없습니다.