ABAP

아직도 APPEND? VALUE 연산자 3가지 패턴 #shorts #SAP #ABAP

개요 및 학습 체크리스트

ABAP 7.40 이후 도입된 VALUE 연산자는 내부 테이블이나 구조체를 선언과 동시에 채워 넣을 수 있게 해주는 인라인 생성 표현식입니다. 기존에는 빈 테이블을 만든 뒤 APPEND 문장을 여러 번 반복해야 했지만, 이제는 한 줄의 표현식으로 동일한 작업을 수행할 수 있습니다. 이 글에서는 VALUE 연산자의 동작 원리부터 실무 적용까지 단계적으로 살펴봅니다.

  • [ ] VALUE 연산자의 기본 문법 이해
  • [ ] 구조체와 내부 테이블 초기화 방법 구분
  • [ ] BASE 추가로 기존 데이터 보존하기
  • [ ] FOR 반복 표현식과 결합한 데이터 변환
  • [ ] 인라인 선언 DATA(...)과 함께 사용하기

먼저 알아두면 좋은 내용

이 글은 ABAP의 기본 데이터 타입과 TYPES BEGIN OF ... END OF 구조체 정의 문법을 알고 있는 분을 대상으로 합니다. 또한 DATA 선언, TYPES TABLE OF 내부 테이블 정의, 그리고 APPEND / INSERT 같은 전통적인 테이블 조작 문장을 한 번이라도 사용해 본 경험이 있으면 차이점을 빠르게 체감할 수 있습니다.

환경과 준비물

VALUE 연산자는 ABAP 7.40 SP02부터 사용 가능하며, 7.40 SP08에서 FOR 반복식이 추가되어 표현력이 크게 확장되었습니다. 따라서 다음 환경 중 하나가 권장됩니다.

  • SAP NetWeaver 7.40 SP08 이상 또는 SAP NetWeaver 7.50 이상
  • SAP S/4HANA 1709 이상 (On-Premise) 또는 SAP S/4HANA Cloud 최신 릴리스
  • SAP BTP, ABAP environment (Steampunk) — 모든 최신 기능 지원
  • 개발 도구: ABAP Development Tools (ADT) for Eclipse 권장, SE80은 일부 인라인 표현식 가독성이 떨어질 수 있음

실습은 클래스 기반 보고서 또는 CL_DEMO_OUTPUT 출력을 활용하면 결과를 빠르게 확인할 수 있습니다.

핵심 개념과 동작 원리

VALUE 연산자는 일반적으로 "특정 타입의 값을 표현식으로 만들어내는 생성자"라고 이해하면 됩니다. 기존 명령형 코드가 "빈 그릇을 준비하고 한 숟가락씩 떠 넣는" 방식이라면, VALUE는 "완성된 그릇을 한 번에 차려내는" 선언형 방식에 가깝습니다.

비유하자면, APPEND는 빈 박스에 물건을 하나씩 담는 것이고, VALUE #( ... )는 포장 완료된 선물 상자를 받는 것과 같습니다. 박스를 여는 순간 이미 모든 내용물이 정리되어 있습니다.

문법 구조는 다음과 같이 도식화할 수 있습니다.

VALUE 대상타입( [ BASE 기존값 ]
                구성요소1 = 값1
                구성요소2 = 값2
                ( 라인1 )
                ( 라인2 )
                ... )

여기서 대상타입 자리에는 컨텍스트로부터 타입이 추론 가능할 경우 #(샵 기호)를 쓸 수 있습니다. 컴파일러가 좌변의 타입을 분석해 자동으로 채워 넣기 때문에 코드가 간결해집니다.

주요 변형은 다음 세 가지로 분류할 수 있습니다.

  • 구조체 생성: VALUE ty_order( id = 1 amount = 1000 ) — 각 필드 = 값 형태
  • 내부 테이블 생성: VALUE ty_orders( ( ... ) ( ... ) ) — 각 라인을 괄호로 감싸 나열
  • 기존 테이블 확장: VALUE #( BASE old_tab ( ... ) ) — 기존 내용 보존하고 추가

중요한 차이점은 BASE를 생략하면 결과 테이블이 매번 새로 초기화된다는 점입니다. 즉, 기존에 데이터가 들어 있던 변수에 BASE 없이 VALUE를 대입하면 이전 내용이 모두 사라집니다. 이는 의도된 동작이며, "선언과 동시에 깨끗하게 채운다"는 사상에 맞춰진 결과입니다.

실전 예제 1단계 — 기본 사용법

먼저 판매 주문(SalesOrder) 내부 테이블을 가장 단순한 형태로 초기화해 봅니다. 전통적인 방식과 VALUE 방식을 나란히 비교합니다.

REPORT zr_value_basic.

TYPES: BEGIN OF ty_sales_order,
         order_id   TYPE n LENGTH 8,
         customer   TYPE string,
         amount     TYPE p LENGTH 10 DECIMALS 2,
         currency   TYPE c LENGTH 3,
       END OF ty_sales_order,
       ty_sales_orders TYPE STANDARD TABLE OF ty_sales_order WITH EMPTY KEY.

START-OF-SELECTION.

* 전통 방식 — 빈 테이블 + APPEND 반복
  DATA(lt_orders_old) = VALUE ty_sales_orders( ).
  DATA ls_row TYPE ty_sales_order.

  ls_row-order_id = '10000001'.
  ls_row-customer = '서울무역'.
  ls_row-amount   = '1500.00'.
  ls_row-currency = 'KRW'.
  APPEND ls_row TO lt_orders_old.

* VALUE 연산자 방식 — 선언과 동시에 채움
  DATA(lt_orders_new) = VALUE ty_sales_orders(
    ( order_id = '10000001' customer = '서울무역'   amount = '1500.00' currency = 'KRW' )
    ( order_id = '10000002' customer = '부산상사'   amount = '2300.50' currency = 'KRW' )
    ( order_id = '10000003' customer = 'GlobalCo'  amount = '980.00'  currency = 'USD' ) ).

  cl_demo_output=>display( lt_orders_new ).

코드량 차이가 분명히 보입니다. 각 라인은 괄호 ( ... )로 묶이며, 그 안에 필드명 = 값을 공백으로 나열합니다. 좌변에서 DATA(...) 인라인 선언과 결합하면 타입 추론이 어려워지므로 VALUE ty_sales_orders(...)처럼 명시적 타입 지정이 권장됩니다.

실전 예제 2단계 — BASE와 FOR로 데이터 가공

실무에서는 기존 테이블에 새 라인을 추가하거나, 다른 테이블을 변환해 채우는 패턴이 자주 등장합니다. 이때 BASEFOR 표현식이 큰 위력을 발휘합니다.

REPORT zr_value_advanced.

TYPES: BEGIN OF ty_booking,
         booking_id TYPE n LENGTH 10,
         passenger  TYPE string,
         price_net  TYPE p LENGTH 10 DECIMALS 2,
         price_tax  TYPE p LENGTH 10 DECIMALS 2,
         price_gross TYPE p LENGTH 10 DECIMALS 2,
       END OF ty_booking,
       ty_bookings TYPE STANDARD TABLE OF ty_booking WITH EMPTY KEY.

CLASS lcl_booking_loader DEFINITION.
  PUBLIC SECTION.
    METHODS load_with_tax IMPORTING it_raw         TYPE ty_bookings
                          RETURNING VALUE(rt_result) TYPE ty_bookings
                          RAISING   cx_sy_arithmetic_error.
ENDCLASS.

CLASS lcl_booking_loader IMPLEMENTATION.
  METHOD load_with_tax.
    CONSTANTS c_vat_rate TYPE p LENGTH 3 DECIMALS 2 VALUE '0.10'.

    TRY.
        rt_result = VALUE #(
          FOR <ls_raw> IN it_raw
          LET tax = <ls_raw>-price_net * c_vat_rate IN
          ( booking_id  = <ls_raw>-booking_id
            passenger   = <ls_raw>-passenger
            price_net   = <ls_raw>-price_net
            price_tax   = tax
            price_gross = <ls_raw>-price_net + tax ) ).
      CATCH cx_sy_arithmetic_overflow INTO DATA(lx_overflow).
        MESSAGE lx_overflow->get_text( ) TYPE 'I'.
        RAISE EXCEPTION lx_overflow.
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  DATA(lt_raw) = VALUE ty_bookings(
    ( booking_id = '1' passenger = 'Alice' price_net = '100.00' )
    ( booking_id = '2' passenger = 'Bob'   price_net = '250.00' )
    ( booking_id = '3' passenger = 'Carol' price_net = '480.00' ) ).

  DATA(lo_loader) = NEW lcl_booking_loader( ).

  TRY.
      DATA(lt_with_tax) = lo_loader->load_with_tax( lt_raw ).
      cl_demo_output=>display( lt_with_tax ).
    CATCH cx_root INTO DATA(lx_root).
      WRITE: / '오류:', lx_root->get_text( ).
  ENDTRY.

FOR ... IN 은 입력 테이블을 순회하며 새 라인을 생성합니다. LET ... IN으로 중간 값을 임시 변수에 담을 수도 있습니다. 이렇게 하면 동일한 계산을 두 번 수행하지 않아 가독성과 성능 모두 개선됩니다.

실전 예제 3단계 — 프로덕션 코드 패턴

실 운영 코드에서는 단위 테스트 가능성과 성능 측면을 함께 고려해야 합니다. 다음 예제는 메모리 할당을 최소화하면서 VALUE를 결합한 패턴입니다.

CLASS zcl_booking_aggregator DEFINITION
  PUBLIC FINAL CREATE PUBLIC.

  PUBLIC SECTION.
    TYPES: BEGIN OF ty_summary,
             currency     TYPE c LENGTH 3,
             total_gross  TYPE p LENGTH 15 DECIMALS 2,
             line_count   TYPE i,
           END OF ty_summary,
           ty_summaries TYPE SORTED TABLE OF ty_summary
                        WITH UNIQUE KEY currency.

    METHODS aggregate
      IMPORTING it_bookings        TYPE ty_bookings
      RETURNING VALUE(rt_summary)  TYPE ty_summaries.
ENDCLASS.

CLASS zcl_booking_aggregator IMPLEMENTATION.
  METHOD aggregate.
    rt_summary = VALUE #(
      FOR GROUPS <grp> OF <ls_b> IN it_bookings
      GROUP BY ( currency = 'KRW' )
      ( currency    = <grp>-currency
        total_gross = REDUCE #(
                        INIT s TYPE p LENGTH 15 DECIMALS 2 VALUE 0
                        FOR <ls_m> IN GROUP <grp>
                        NEXT s = s + <ls_m>-price_gross )
        line_count  = REDUCE i(
                        INIT n = 0
                        FOR <ls_c> IN GROUP <grp>
                        NEXT n = n + 1 ) ) ).
  ENDMETHOD.
ENDCLASS.

CLASS ltc_aggregator DEFINITION FOR TESTING
  RISK LEVEL HARMLESS DURATION SHORT.
  PRIVATE SECTION.
    METHODS test_empty_input FOR TESTING.
    METHODS test_single_line FOR TESTING.
ENDCLASS.

CLASS ltc_aggregator IMPLEMENTATION.
  METHOD test_empty_input.
    DATA(lo) = NEW zcl_booking_aggregator( ).
    DATA(lt_in) = VALUE ty_bookings( ).
    cl_abap_unit_assert=>assert_initial( lo->aggregate( lt_in ) ).
  ENDMETHOD.

  METHOD test_single_line.
    DATA(lo) = NEW zcl_booking_aggregator( ).
    DATA(lt_in) = VALUE ty_bookings(
      ( booking_id = '1' passenger = 'Test' price_gross = '100.00' ) ).
    DATA(lt_out) = lo->aggregate( lt_in ).
    cl_abap_unit_assert=>assert_equals(
      exp = 1 act = lines( lt_out ) ).
  ENDMETHOD.
ENDCLASS.

주목할 점은 VALUEREDUCE, FOR GROUPS 같은 다른 표현식과 자연스럽게 중첩된다는 점입니다. 단위 테스트에서도 빈 테이블을 VALUE ty_bookings( )로 명확히 표현하면 의도가 분명해집니다.

흔한 실수와 트러블슈팅 FAQ

Q1. VALUE #(...)에서 "타입을 추론할 수 없다"는 컴파일 오류가 발생합니다.

A. # 사용은 좌변 또는 함수 호출 컨텍스트에서 타입이 명확할 때만 가능합니다. DATA(lt) = VALUE #( ... )처럼 양쪽 모두 추론이 필요한 경우엔 사용할 수 없습니다. 이때는 VALUE ty_orders( ... )처럼 타입을 명시하세요.

Q2. 기존 데이터가 사라집니다. 왜 그런가요?

A. VALUE는 새 값을 생성하는 표현식이므로, BASE 없이 대입하면 좌변 변수는 새 값으로 덮어쓰여 이전 내용이 사라집니다. 누적이 필요하다면 반드시 VALUE #( BASE lt_existing ( ... ) ) 형태로 작성해야 합니다.

Q3. 필드 사이를 콤마로 구분해도 되나요?

A. 안 됩니다. VALUE 내부에서 필드와 값은 공백으로만 구분합니다. 콤마는 문법 오류를 일으킵니다. 또한 각 라인 사이에도 콤마가 아닌 공백/줄바꿈으로 분리합니다.

Q4. 성능상 APPEND와 차이가 있나요?

A. 일반적으로 VALUE는 내부적으로 최적화된 라인 생성 경로를 사용하지만, 동일한 작업이라면 큰 차이는 없습니다. 다만 가독성과 의도 표현 면에서 우위가 있어 권장됩니다. 수십만 건 이상 대량 데이터에서는 FOR 표현식과 일반 LOOP ... APPEND를 프로파일링 후 선택하는 것이 좋습니다.

Q5. SORTED/HASHED 테이블에도 쓸 수 있나요?

A. 가능합니다. 단, 키 중복이나 정렬 순서를 위반하는 라인이 포함되면 런타임 예외가 발생합니다. 정렬되지 않은 입력을 SORTED TABLE로 변환할 때는 사전 정렬이 필요합니다.

이어서 살펴볼 만한 주제

VALUE를 익혔다면 동일 계열의 표현식인 CORRESPONDING 연산자(구조 다른 두 테이블 간 매핑), REDUCE 연산자(집계), FILTER 연산자(조건 필터링), CONV(타입 변환), NEW(객체 인스턴스화)로 학습을 확장하는 것을 권장합니다. 또한 RAP(RESTful ABAP Programming Model) 의 BDEF 액션 구현부에서 VALUE가 자주 등장하므로, CDS View 및 BDEF 기초도 함께 보면 시너지가 큽니다.

더 읽어볼 자료

댓글 0

아직 댓글이 없습니다.