ABAP

ABAP 80%가 모르는 VALUE 초기화 방법 #shorts #SAP #ABAP

▶ YouTube에서 보기

개요와 이 글에서 다루는 범위

ABAP 7.40 이후로 도입된 VALUE 연산자는 내부 테이블과 구조체를 다루는 방식을 근본부터 바꿔놓은 표현식 기반 연산자입니다. 이 글은 APPEND 문을 반복적으로 사용하던 전통적인 방식과 비교하여, 선언과 동시에 데이터를 채워 넣는 인라인 초기화 패턴을 처음부터 끝까지 다룹니다.

  • VALUE 연산자가 등장한 배경과 해결하는 문제
  • 스칼라·구조체·내부 테이블 초기화 문법
  • 중첩 구조체와 BASE 키워드를 활용한 부분 초기화
  • 판매 오더 라인 아이템 시나리오 기반 실전 예제
  • 흔히 마주치는 컴파일 에러와 트러블슈팅

마지막 섹션에서는 코드 리뷰 시 자주 지적되는 실수 패턴과 함께, 단위 테스트 친화적인 작성법까지 다룹니다.

읽기 전에 알아두면 좋은 배경

이 글의 예제는 ABAP 문법 7.40 SP08 이상을 전제로 합니다. 내부 테이블(STANDARD/SORTED/HASHED) 선언과 TYPES, DATA 문법, 그리고 구조체 정의를 다뤄본 경험이 있다면 충분합니다. ADT(ABAP Development Tools) 또는 SE80에서 직접 실행해보면 동작이 더 명확히 보입니다. RAP나 CDS 지식은 필요하지 않으며, 클래식 ABAP만으로 모든 예제를 검증할 수 있습니다.

실행 환경과 준비물

예제는 다음 환경에서 검증되었습니다.

  • ABAP 플랫폼: SAP S/4HANA 2022 온프레미스 (커널 7.93), ABAP 7.57
  • 대체 가능 환경: SAP NetWeaver AS ABAP 7.52 이상, ABAP Cloud(Steampunk) 환경에서도 동일하게 동작
  • 개발 도구: Eclipse + ABAP Development Tools 3.36 권장, 보조적으로 SE38/SE80 사용 가능
  • 테스트 트랜잭션: SE38(리포트 실행), SE24(클래스 단위 테스트), SAT(런타임 분석)

참고로 7.40 SP02~SP05 시점에는 VALUE 연산자에 일부 제약이 있었으나, SP08부터 거의 모든 패턴이 안정적으로 동작하므로 현장에서 사용하는 시스템이 그 이상인지 먼저 확인하는 것이 좋습니다.

핵심 개념과 동작 원리

전통적으로 ABAP에서 내부 테이블에 데이터를 채울 때는 다음 흐름을 따랐습니다. 임시 작업 영역(work area)에 필드 값을 하나씩 대입하고, APPEND로 테이블에 행을 추가하는 작업을 반복하는 패턴이죠. 행이 5개라면 같은 코드가 5번 반복되고, 그 사이에 work area를 CLEAR하는 코드가 끼어들어 가독성이 급격히 떨어집니다.

VALUE 연산자는 이 문제를 "표현식(expression)"으로 풀어냅니다. 즉, 값을 만들어내는 작업 자체를 하나의 식으로 묶어서 변수에 바로 대입할 수 있게 합니다. 비유하자면 기존 방식이 "빈 박스를 만들고, 종이를 한 장씩 접어 넣는 공정"이라면, VALUE는 "이미 종이가 채워진 박스를 한 번에 만들어내는 주물 틀"에 가깝습니다.

문법의 기본형은 다음과 같습니다.

VALUE 타입( 컴포넌트1 = 값1 컴포넌트2 = 값2 ... )

여기서 타입은 명시적으로 적을 수도 있고, 컨텍스트에서 추론될 경우 #로 대체할 수 있습니다. 내부 테이블의 경우 괄호 안에 다시 ( ... )를 중첩하여 한 행씩을 표현합니다. 즉, 바깥쪽 괄호는 테이블 전체를, 안쪽 괄호 하나하나는 각 행(row)을 의미한다고 이해하면 직관적입니다.

또한 BASE 키워드를 사용하면 기존 테이블을 기반으로 추가 행을 덧붙이거나, 기존 구조체의 일부 필드만 덮어쓰는 형태로 새 값을 만들 수 있습니다. 이는 불변(immutable) 스타일의 코드를 작성할 때 매우 유용합니다.

1단계 - 스칼라와 단순 테이블 초기화 예제

먼저 가장 기본적인 형태부터 살펴봅니다. 판매 부서에서 자주 사용하는 통화 코드 목록을 내부 테이블로 만들어야 한다고 가정합시다. 전통적인 방식이라면 다음과 같이 작성하게 됩니다.

DATA: lt_currency TYPE STANDARD TABLE OF waers,
      lv_currency TYPE waers.

lv_currency = 'KRW'. APPEND lv_currency TO lt_currency.
lv_currency = 'USD'. APPEND lv_currency TO lt_currency.
lv_currency = 'EUR'. APPEND lv_currency TO lt_currency.
lv_currency = 'JPY'. APPEND lv_currency TO lt_currency.

같은 결과를 VALUE 연산자로 작성하면 한 줄로 끝납니다.

DATA(lt_currency) = VALUE string_table(
  ( `KRW` ) ( `USD` ) ( `EUR` ) ( `JPY` ) ).

구조체가 포함된 테이블도 같은 원리로 처리합니다. 영업소(sales office) 마스터 데이터를 임시로 구성해야 하는 상황을 예로 들어보겠습니다.

TYPES: BEGIN OF ty_sales_office,
         office_id   TYPE c LENGTH 4,
         office_name TYPE string,
         country     TYPE land1,
       END OF ty_sales_office,
       ty_sales_office_tab TYPE STANDARD TABLE OF ty_sales_office
                           WITH EMPTY KEY.

DATA(lt_offices) = VALUE ty_sales_office_tab(
  ( office_id = 'SO01' office_name = 'Seoul HQ'    country = 'KR' )
  ( office_id = 'SO02' office_name = 'Busan Branch' country = 'KR' )
  ( office_id = 'SO03' office_name = 'Tokyo Office' country = 'JP' )
).

여기서 주목할 점은 DATA(lt_offices)라는 인라인 선언과 VALUE가 결합된 형태입니다. 컴파일러는 우변의 타입을 분석해 좌변 변수의 타입을 자동으로 결정합니다. 그 결과 임시 work area도, APPEND도, CLEAR도 모두 사라집니다.

2단계 - 중첩 구조체와 BASE 키워드를 활용한 실무 시나리오

실무에서는 내부 테이블 안에 다시 구조체가 들어 있는 경우가 흔합니다. 예를 들어 판매 오더 헤더 안에 배송 정보 구조체가 포함되어 있고, 이 헤더 자체가 다시 내부 테이블의 한 행이 되는 형태입니다.

TYPES: BEGIN OF ty_shipping,
         delivery_date TYPE dats,
         shipping_type TYPE c LENGTH 2,
       END OF ty_shipping,

       BEGIN OF ty_order_header,
         order_id    TYPE c LENGTH 10,
         customer_id TYPE kunnr,
         net_amount  TYPE p LENGTH 13 DECIMALS 2,
         currency    TYPE waers,
         shipping    TYPE ty_shipping,
       END OF ty_order_header,

       ty_order_header_tab TYPE STANDARD TABLE OF ty_order_header
                           WITH EMPTY KEY.

DATA(lt_orders) = VALUE ty_order_header_tab(
  ( order_id    = '4500001001'
    customer_id = '0001000123'
    net_amount  = '1250000.00'
    currency    = 'KRW'
    shipping    = VALUE #( delivery_date = '20260701'
                           shipping_type = 'ST' ) )

  ( order_id    = '4500001002'
    customer_id = '0001000456'
    net_amount  = '980.50'
    currency    = 'USD'
    shipping    = VALUE #( delivery_date = '20260705'
                           shipping_type = 'EX' ) )
).

내부에서 다시 VALUE #( ... )를 호출하는데, #는 "바깥 컨텍스트에서 타입을 추론하라"는 의미입니다. 컴파일러는 shipping 필드의 타입이 ty_shipping임을 알고 있으므로 명시적으로 타입을 다시 쓸 필요가 없습니다.

이제 한 단계 더 들어가서, 기본 오더 셋이 이미 있고 거기에 행을 추가하거나 필드를 일부만 바꿔야 하는 상황을 생각해봅시다. 이때 BASE 키워드가 빛을 발합니다.

DATA(lt_orders_extended) = VALUE ty_order_header_tab(
  BASE lt_orders
  ( order_id    = '4500001003'
    customer_id = '0001000789'
    net_amount  = '500.00'
    currency    = 'EUR'
    shipping    = VALUE #( delivery_date = '20260710'
                           shipping_type = 'ST' ) )
).

위 코드는 lt_orders의 모든 행을 그대로 가져온 뒤, 새로운 한 행을 추가한 새 테이블을 만듭니다. 원본 lt_orders는 변경되지 않으므로, 의도치 않은 부작용을 줄이는 함수형 스타일 코드 작성이 가능해집니다. 트랜잭션이 길고 디버깅이 어려운 ERP 환경에서 이 패턴은 의외로 큰 안정성을 가져다 줍니다.

로깅과 함께 사용하는 패턴도 살펴봅니다. 오더 생성 직후 BAL 로그에 행 수를 기록하는 예시입니다.

TRY.
    DATA(lt_log_orders) = VALUE ty_order_header_tab(
      BASE lt_orders
      ( order_id    = '4500001099'
        customer_id = '0001000999'
        net_amount  = '0.00'
        currency    = 'KRW'
        shipping    = VALUE #( ) ) ).

    MESSAGE |Order table prepared with { lines( lt_log_orders ) } rows.|
            TYPE 'S'.
  CATCH cx_sy_itab_line_not_found INTO DATA(lx_err).
    MESSAGE lx_err TYPE 'E'.
ENDTRY.

여기서 VALUE #( )는 "빈 구조체"를 의미합니다. 모든 컴포넌트가 초기값으로 채워진 구조체가 생성되며, 이는 CLEAR 동작과 등가입니다.

3단계 - 프로덕션 수준의 판매 오더 라인 아이템 초기화

이제 실제 운영 코드에 가까운 시나리오로 마무리합니다. 판매 오더 한 건과 그에 속한 라인 아이템 테이블을 한 번에 구성하고, 단위 테스트에서도 활용할 수 있도록 클래스 메서드로 캡슐화합니다.

CLASS zcl_sales_order_factory DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES: BEGIN OF ty_item,
             item_no   TYPE posnr,
             material  TYPE matnr,
             quantity  TYPE p LENGTH 13 DECIMALS 3,
             unit      TYPE meins,
             net_price TYPE p LENGTH 13 DECIMALS 2,
           END OF ty_item,
           ty_item_tab TYPE STANDARD TABLE OF ty_item
                       WITH NON-UNIQUE SORTED KEY by_item
                       COMPONENTS item_no.

    METHODS build_default_items
      RETURNING VALUE(rt_items) TYPE ty_item_tab.

    METHODS build_items_with_discount
      IMPORTING it_base         TYPE ty_item_tab
                iv_discount_pct TYPE p LENGTH 5 DECIMALS 2
      RETURNING VALUE(rt_items) TYPE ty_item_tab.
ENDCLASS.

CLASS zcl_sales_order_factory IMPLEMENTATION.
  METHOD build_default_items.
    rt_items = VALUE #(
      ( item_no = '000010' material = 'MAT-A001'
        quantity = '2.000'  unit = 'EA' net_price = '150000.00' )
      ( item_no = '000020' material = 'MAT-A002'
        quantity = '5.000'  unit = 'EA' net_price = '32000.00' )
      ( item_no = '000030' material = 'MAT-B015'
        quantity = '1.000'  unit = 'PC' net_price = '890000.00' ) ).
  ENDMETHOD.

  METHOD build_items_with_discount.
    rt_items = VALUE #(
      FOR <ls_item> IN it_base
      ( item_no   = <ls_item>-item_no
        material  = <ls_item>-material
        quantity  = <ls_item>-quantity
        unit      = <ls_item>-unit
        net_price = <ls_item>-net_price *
                    ( 1 - iv_discount_pct / 100 ) ) ).
  ENDMETHOD.
ENDCLASS.

두 번째 메서드 안에 등장한 FOR ... IN ... 구문은 VALUE와 결합되어 강력한 변환 표현식을 만들어 냅니다. 입력 테이블의 각 행에 대해 새 행을 생성하면서 net_price만 할인율을 적용해 다시 계산하는데, 이는 SQL의 SELECT ... FROM과 유사한 발상입니다. LOOP ATAPPEND를 명시적으로 쓸 필요가 없습니다.

단위 테스트 측면에서도 유리합니다. 테스트 픽스처를 다음과 같이 한 줄로 준비할 수 있기 때문입니다.

METHOD discount_should_reduce_price.
  DATA(lo_factory) = NEW zcl_sales_order_factory( ).

  DATA(lt_expected) = VALUE zcl_sales_order_factory=>ty_item_tab(
    ( item_no = '000010' material = 'MAT-A001'
      quantity = '2.000'  unit = 'EA' net_price = '135000.00' ) ).

  DATA(lt_base) = VALUE zcl_sales_order_factory=>ty_item_tab(
    ( item_no = '000010' material = 'MAT-A001'
      quantity = '2.000'  unit = 'EA' net_price = '150000.00' ) ).

  DATA(lt_actual) = lo_factory->build_items_with_discount(
                      it_base = lt_base iv_discount_pct = '10.00' ).

  cl_abap_unit_assert=>assert_equals(
    exp = lt_expected[ 1 ]-net_price
    act = lt_actual[ 1 ]-net_price ).
ENDMETHOD.

보안과 성능 관점에서 한 가지 덧붙이자면, VALUE 표현식은 내부적으로 work area를 한 번 할당해 재사용하므로, 같은 결과를 만드는 APPEND 루프와 비교했을 때 성능은 동등하거나 약간 더 빠른 편으로 알려져 있습니다. 다만 행 수가 수십만 건을 넘는 대량 처리에는 어차피 데이터베이스 측 처리(CDS, AMDP)가 권장되므로, VALUE는 마스터/메타 데이터, 테스트 픽스처, 중간 결과 가공 영역에서 가장 빛납니다.

흔한 실수와 트러블슈팅

실무에서 자주 마주치는 실수를 FAQ 형식으로 정리합니다.

Q1. "Operand type cannot be determined" 컴파일 에러가 납니다.

대부분 VALUE #( ... )에서 #를 썼는데 컨텍스트에서 타입을 추론할 수 없는 경우입니다. 예를 들어 DATA(lt) = VALUE #( ( a = 1 ) ).처럼 좌변도 인라인 선언이고 우변도 #이면 컴파일러는 타입을 추론하지 못합니다. VALUE 뒤에 명시적인 테이블 타입명을 적어주거나, DATA로 먼저 변수를 선언해야 합니다.

Q2. SORTED/HASHED 테이블에 중복 키를 넣으면 어떻게 되나요?

런타임 예외 CX_SY_ITAB_DUPLICATE_KEY가 발생합니다. VALUE로 한꺼번에 채울 때도 마찬가지이므로, 키 유일성이 보장되지 않는 데이터는 STANDARD 테이블로 먼저 만든 뒤 SORT ... DELETE ADJACENT DUPLICATES로 정리하거나, HASHED 테이블 대신 NON-UNIQUE 키를 가진 SORTED 테이블을 사용하는 편이 안전합니다.

Q3. BASE 키워드를 썼는데 원본이 변경된 것처럼 보입니다.

BASE 자체는 원본을 변경하지 않습니다. 다만 결과를 같은 변수에 다시 대입(lt = VALUE #( BASE lt ( ... ) ))했다면 그 시점에 변수의 내용이 교체됩니다. 원본을 보존하려면 결과를 다른 변수로 받으세요.

그 밖에 자주 보이는 실수로는 컴포넌트 이름의 오타(컴파일 에러가 친절하지 않을 수 있음), 행 괄호 ( ... )를 빠뜨리고 컴포넌트만 나열하는 경우, 그리고 VALUE 식 안에서 외부 변수 스코프를 잘못 이해해 의도와 다른 값이 들어가는 경우가 있습니다. ADT의 코드 어시스트와 Quick Fix를 적극 활용하면 대부분 사전에 잡을 수 있습니다.

이 글 이후에 살펴보면 좋은 주제

VALUE 연산자에 익숙해졌다면 자연스럽게 다음 표현식 기반 구문들로 시야를 넓혀보길 권장합니다.

  • FOR ... IN ... WHERE 조합과 그룹핑(GROUP BY)을 이용한 테이블 변환
  • REDUCE 연산자로 합계, 집계 표현식 작성
  • CORRESPONDING #( ... MAPPING ... )로 구조체 간 자동 매핑
  • SWITCHCOND 연산자로 조건부 값 계산
  • RAP 비즈니스 객체에서 EML(Entity Manipulation Language)과 VALUE 결합 패턴

특히 FORREDUCE는 VALUE와 거의 한 세트처럼 쓰이므로, 함께 익혀두면 데이터 가공 코드의 양이 절반 이하로 줄어드는 경험을 할 수 있습니다.

더 깊이 파고들 만한 자료

위 자료들을 따라가다 보면, 단순 문법 소개를 넘어 모던 ABAP 스타일 가이드가 왜 표현식 기반 코드를 권장하는지 그 근거까지 자연스럽게 이해할 수 있습니다.

댓글 0

아직 댓글이 없습니다.