ABAP

ABAP Unit 80%가 쓰는 VALUE 데이터 패턴 #shorts #SAP #ABAP

▶ YouTube에서 보기

섹션 1. VALUE 연산자의 등장 배경과 ABAP Unit에서의 의미

ABAP 7.40부터 도입된 VALUE 연산자는 구조체나 내부 테이블을 인라인으로 초기화할 수 있게 해주는 핵심 표현식입니다. 특히 ABAP Unit 테스트 코드에서 VALUE의 가치는 더욱 두드러집니다. 단위 테스트의 본질은 "주어진 입력(Given) → 행위 실행(When) → 결과 검증(Then)" 패턴인데, 이 중 Given 단계의 테스트 데이터 준비 코드가 길어질수록 테스트의 의도가 흐려지고 가독성이 떨어집니다.

이 글에서 다룰 내용을 체크리스트로 정리하면 다음과 같습니다.

  • VALUE 연산자로 구조체 / 내부 테이블 / DEEP 구조 초기화하기
  • FOR 루프와 결합해 대량 테스트 데이터 한 줄로 생성하기
  • BASE 키워드로 기존 구조체에서 일부 필드만 덮어쓰기
  • Mock 객체에 주입할 입력 데이터를 가독성 있게 구성하기
  • VALUE 사용 시 컴파일러가 추론하는 타입과 흔한 오류 패턴 이해하기

특히 RAP(RESTful ABAP Programming Model) 기반 비즈니스 객체의 단위 테스트가 보편화된 SAP S/4HANA 2022 이후 환경에서는, 짧고 명료한 테스트 픽스처(fixture) 작성이 생산성을 좌우합니다.

섹션 2. 기존 방식의 한계 — 테스트 데이터 준비 코드가 길어지는 이유

먼저 VALUE 없이 테스트 데이터를 준비하면 어떤 모습이 되는지 비교해 봅시다. 다음은 수주(SalesOrder) 구조체 하나와 라인 아이템 3건을 준비하는 코드의 "기존 스타일"입니다.

DATA: ls_order TYPE zsales_order,
      lt_items TYPE ztt_line_item,
      ls_item  TYPE zline_item.

ls_order-order_id   = '4500001234'.
ls_order-customer   = 'C100023'.
ls_order-currency   = 'KRW'.
ls_order-created_on = sy-datum.

CLEAR ls_item.
ls_item-item_no  = '0010'.
ls_item-material = 'MAT-001'.
ls_item-quantity = 5.
APPEND ls_item TO lt_items.

CLEAR ls_item.
ls_item-item_no  = '0020'.
ls_item-material = 'MAT-002'.
ls_item-quantity = 10.
APPEND ls_item TO lt_items.

코드의 절반 이상이 "값 채우기"에 쓰입니다. 테스트 메서드 하나가 50줄을 넘어가면, 정작 검증하려는 핵심 로직이 데이터 준비 코드에 묻혀버립니다. 또한 CLEAR 누락 같은 휴먼 에러가 자주 발생합니다.

VALUE 연산자는 이 모든 보일러플레이트를 한 줄의 표현식으로 압축합니다. 핵심 비유는 "건축 도면을 한 장에 펼쳐놓고 보는 것"입니다. 구조체의 모든 필드 값을 한곳에서 시각적으로 확인할 수 있어, 테스트 시나리오의 의도가 그대로 드러납니다.

섹션 3. VALUE로 단일 구조체 초기화하기

가장 기본적인 형태는 단일 구조체를 인라인으로 만드는 것입니다. 위 코드를 VALUE로 다시 쓰면 다음과 같습니다.

DATA(ls_order) = VALUE zsales_order(
  order_id   = '4500001234'
  customer   = 'C100023'
  currency   = 'KRW'
  created_on = sy-datum
).

여기서 주목할 점은 세 가지입니다. 첫째, DATA(...) 인라인 선언과 결합하면 별도 DATA 선언이 필요 없습니다. 둘째, 명시하지 않은 필드는 자동으로 타입의 초기값(IS INITIAL)으로 설정됩니다. 셋째, 필드 순서는 자유롭습니다.

기존 구조체를 기반으로 일부 필드만 변경한 변형 데이터가 필요할 때는 BASE 키워드가 유용합니다.

DATA(ls_order_modified) = VALUE zsales_order(
  BASE ls_order
  customer = 'C999999'
  currency = 'USD'
).

이 패턴은 "정상 케이스 데이터 → 특정 필드만 비정상값으로 바꾼 경계 테스트"를 만들 때 매우 효과적입니다.

섹션 4. 내부 테이블을 한 번에 채우기

내부 테이블 초기화는 VALUE의 진가가 가장 잘 드러나는 영역입니다. 라인 아이템 3건을 한 번에 만드는 코드는 다음과 같습니다.

DATA(lt_items) = VALUE ztt_line_item(
  ( item_no = '0010' material = 'MAT-001' quantity = 5  unit = 'EA' )
  ( item_no = '0020' material = 'MAT-002' quantity = 10 unit = 'EA' )
  ( item_no = '0030' material = 'MAT-003' quantity = 2  unit = 'BOX' )
).

각 행은 괄호 ( ... )로 둘러쌉니다. 행 내부에서 공통 필드 값을 기본값으로 지정하고 각 행에서 일부만 덮어쓰는 패턴도 가능합니다.

DATA(lt_items) = VALUE ztt_line_item(
  unit = 'EA' currency = 'KRW'
  ( item_no = '0010' material = 'MAT-001' quantity = 5 )
  ( item_no = '0020' material = 'MAT-002' quantity = 10 )
  ( item_no = '0030' material = 'MAT-003' quantity = 2 unit = 'BOX' )
).

섹션 5. 중첩 구조(DEEP Structure) 다루기

실무에서는 헤더와 아이템을 함께 담는 DEEP 구조가 자주 등장합니다.

DATA(ls_order_deep) = VALUE ty_sales_order_deep(
  order_id = '4500001234'
  customer = 'C100023'
  currency = 'KRW'
  items    = VALUE #(
               ( item_no = '0010' material = 'MAT-001' quantity = 5 )
               ( item_no = '0020' material = 'MAT-002' quantity = 10 )
             )
  partners = VALUE #(
               ( role = 'AG' partner_no = 'P0001' )
               ( role = 'WE' partner_no = 'P0002' )
             )
).

중첩된 내부 테이블의 타입은 부모 컴포넌트에서 추론할 수 있으므로 VALUE #(...)처럼 # 단축형을 쓸 수 있습니다.

섹션 6. FOR 루프와 결합한 동적 데이터 생성

경계값 테스트나 부하 테스트용 데이터를 만들 때는 FOR 루프와 결합합니다.

DATA(lt_bulk_items) = VALUE ztt_line_item(
  FOR i = 1 UNTIL i > 100
  ( item_no   = |{ i * 10 ALIGN = RIGHT WIDTH = 4 PAD = '0' }|
    material  = |MAT-{ i WIDTH = 3 PAD = '0' }|
    quantity  = i
    unit      = 'EA' )
).

기존 테이블을 변환할 때는 FOR ... IN ... 형태를 씁니다.

DATA(lt_test_input) = VALUE ztt_line_item(
  FOR  IN lt_materials WHERE ( category = 'FERT' )
  ( item_no  = -mat_no
    material = -mat_no
    quantity = 1
    unit     = -base_unit )
).

섹션 7. 실전 예제 — 수주 처리 ABAP Unit 테스트 전체 코드

지금까지의 기법을 종합해 수주 검증 클래스의 단위 테스트를 작성해 봅시다.

CLASS ltcl_sales_order_validator DEFINITION FINAL
  FOR TESTING
  RISK LEVEL HARMLESS
  DURATION SHORT.

  PRIVATE SECTION.
    DATA mo_cut TYPE REF TO zcl_sales_order_validator.

    METHODS:
      setup,
      validate_normal_order      FOR TESTING,
      validate_empty_items       FOR TESTING,
      validate_negative_quantity FOR TESTING,
      validate_bulk_order        FOR TESTING.

    METHODS:
      build_order
        IMPORTING iv_customer       TYPE c
                  it_item_overrides TYPE ztt_line_item OPTIONAL
        RETURNING VALUE(rs_order)   TYPE ty_sales_order_deep.
ENDCLASS.

CLASS ltcl_sales_order_validator IMPLEMENTATION.

  METHOD setup.
    mo_cut = NEW zcl_sales_order_validator( ).
  ENDMETHOD.

  METHOD build_order.
    rs_order = VALUE ty_sales_order_deep(
      order_id = '4500009999'
      customer = iv_customer
      currency = 'KRW'
      items    = COND #(
        WHEN it_item_overrides IS NOT INITIAL
        THEN it_item_overrides
        ELSE VALUE #(
          ( item_no = '0010' material = 'MAT-001' quantity = 5  unit = 'EA' )
          ( item_no = '0020' material = 'MAT-002' quantity = 10 unit = 'EA' )
        )
      )
    ).
  ENDMETHOD.

  METHOD validate_normal_order.
    DATA(ls_order) = build_order( iv_customer = 'C100023' ).
    DATA(lt_messages) = mo_cut->validate( ls_order ).
    cl_abap_unit_assert=>assert_initial(
      act = lt_messages
      msg = '정상 수주는 검증 메시지가 없어야 함'
    ).
  ENDMETHOD.

  METHOD validate_empty_items.
    DATA(ls_order) = VALUE ty_sales_order_deep(
      order_id = '4500009999'
      customer = 'C100023'
      currency = 'KRW'
      items    = VALUE #( )
    ).
    DATA(lt_messages) = mo_cut->validate( ls_order ).
    cl_abap_unit_assert=>assert_not_initial(
      act = lt_messages
      msg = '아이템이 비어있으면 오류 메시지가 있어야 함'
    ).
  ENDMETHOD.

  METHOD validate_negative_quantity.
    DATA(ls_order) = build_order(
      iv_customer       = 'C100023'
      it_item_overrides = VALUE ztt_line_item(
        ( item_no = '0010' material = 'MAT-001' quantity = -1 unit = 'EA' )
      )
    ).
    DATA(lt_messages) = mo_cut->validate( ls_order ).
    cl_abap_unit_assert=>assert_equals(
      act = lines( lt_messages )
      exp = 1
      msg = '음수 수량은 1건의 오류여야 함'
    ).
  ENDMETHOD.

  METHOD validate_bulk_order.
    DATA(ls_order) = VALUE ty_sales_order_deep(
      order_id = '4500009999'
      customer = 'C100023'
      currency = 'KRW'
      items    = VALUE #(
        FOR i = 1 UNTIL i > 200
        ( item_no  = |{ i * 10 WIDTH = 6 PAD = '0' ALIGN = RIGHT }|
          material = |MAT-{ i WIDTH = 3 PAD = '0' }|
          quantity = i
          unit     = 'EA' )
      )
    ).
    DATA(lt_messages) = mo_cut->validate( ls_order ).
    cl_abap_unit_assert=>assert_initial(
      act = lt_messages
      msg = '대량 수주(200건)도 정상 처리되어야 함'
    ).
  ENDMETHOD.

ENDCLASS.

섹션 8. VALUE 활용 시 자주 하는 실수와 주의사항

실수 1. "Type cannot be derived" 컴파일 오류

VALUE #(...)#는 컨텍스트에서 타입을 추론할 수 있을 때만 사용 가능합니다. 좌변에 명시적 타입이 없거나 메서드 인자로 직접 전달 시 타입이 제네릭이라면 VALUE ztt_line_item(...)처럼 타입을 명시해야 합니다.

실수 2. BASE를 썼는데 필드가 의도와 다르게 비어 있음

BASE 뒤에 명시한 필드는 덮어쓰기 됩니다. 명시하지 않은 필드는 베이스 값을 유지합니다. "특정 필드를 명시적으로 초기화"하려면 field = VALUE #( )처럼 빈 값을 명시해야 합니다.

실수 3. FOR 루프 카운터 변수를 표현식 밖에서 참조

FOR i = 1 UNTIL i > 10i는 표현식 내부에서만 유효한 로컬 변수입니다. 표현식 바깥에서 참조하면 컴파일 오류가 납니다.

실수 4. FOR로 수만 건 생성해 테스트 느려짐

VALUE 자체는 빠르지만 FOR로 수만 건을 생성하면 메모리 할당 비용이 큽니다. 대량 데이터 테스트는 별도의 RISK LEVEL DANGEROUS DURATION LONG으로 분리하고, 일반 단위 테스트는 수십 건 이내로 유지하는 것이 권장됩니다.

Test Double Framework와 결합할 때도 VALUE는 강력합니다. Mock 반환 테이블을 한 줄로 구성할 수 있어 given 단계가 짧아집니다.

DATA(lo_material_reader) = CAST zif_material_reader(
  cl_abap_testdouble=>create( 'ZIF_MATERIAL_READER' )
).

cl_abap_testdouble=>configure_call( lo_material_reader
  )->returning( VALUE ztt_material(
    ( mat_no = 'MAT-001' description = '볼펜'   base_unit = 'EA' )
    ( mat_no = 'MAT-002' description = '연필'   base_unit = 'EA' )
    ( mat_no = 'MAT-003' description = '노트'   base_unit = 'BOX' )
  ) ).

lo_material_reader->read_by_ids( VALUE #( ( 'MAT-001' ) ( 'MAT-002' ) ) ).

댓글 0

아직 댓글이 없습니다.