News

TYPE 선언 그만 — ABAP 컴파일러 타입 추론 완성 #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 및 이 글의 목표

ABAP 7.40 SP08 이후 도입된 인라인 선언(Inline Declaration)과 생성자 표현식(Constructor Expressions)은 ABAP 개발 방식을 근본적으로 바꾸어 놓았습니다. 과거에는 변수 하나를 쓰기 위해 DATA 블록에서 명시적으로 타입을 선언해야 했지만, 이제는 컴파일러가 우변의 표현식을 분석하여 좌변의 타입을 자동으로 결정합니다. 이를 타입 추론(Type Inference)이라고 부릅니다.

본 글에서는 DATA(), FINAL(), VALUE #(), CAST(), FOR expression 다섯 가지 핵심 구문을 중심으로 ABAP 컴파일러가 어떻게 타입을 추론하는지, 그리고 실무에서 어떤 패턴이 권장되는지를 단계별로 살펴봅니다. 다음 항목을 익히는 것이 목표입니다.

  • 인라인 선언이 가능한 위치와 불가능한 위치 구분
  • DATA()FINAL()의 의미론적 차이
  • VALUE #()# 기호가 의미하는 "연산자 우선순위(operator-priority)" 타입 결정
  • CAST()를 활용한 다운캐스트 패턴과 안전한 예외 처리
  • FOR expression 내부에서의 루프 변수 추론

핵심 개념 — 타입 추론이란 무엇인가

ABAP 컴파일러의 타입 추론은 크게 두 갈래로 나뉩니다. 하나는 좌변 추론(Left-hand inference)이고, 다른 하나는 우변 추론(Operand-position inference)입니다. DATA(lv_x) = expression 형태에서는 컴파일러가 우변 expression의 정적 타입을 분석해 lv_x의 타입을 결정합니다. 반대로 VALUE #( ... )에서 #은 "이 위치에서 요구되는 타입을 호출 컨텍스트로부터 가져오라"는 지시어입니다.

비유하자면 컴파일러는 퍼즐 조각의 모양을 보고 빈자리를 채우는 작업을 합니다. DATA()는 빈자리가 좌변에 있고 우변이 모양을 결정하는 경우이고, VALUE #()은 빈자리가 우변에 있고 좌변(또는 함수 시그니처)이 모양을 결정하는 경우입니다.

" 좌변 추론: 우변 SELECT가 ty_sflight 표를 만들어 줌
SELECT * FROM sflight INTO TABLE @DATA(lt_flights).

" 우변 추론: 매개변수 타입이 ty_addr 이므로 # 위치에 ty_addr 추론
cl_demo->set_address( VALUE #( city = 'Seoul' zip = '04524' ) ).

타입 추론은 단순한 문법 설탕이 아니라, 리팩토링 비용을 낮추는 메커니즘입니다. 테이블 구조가 바뀌어도 인라인 선언을 사용한 변수는 자동으로 새로운 구조를 따라가기 때문입니다. 다만 가독성을 해치지 않도록 변수 이름을 명확히 짓는 것이 일반적으로 권장됩니다.

1단계: DATA() 인라인 선언으로 로컬 변수 타입 자동 결정

DATA()는 가장 널리 쓰이는 인라인 선언 구문입니다. 변수가 처음 등장하는 자리에서 좌변에 한 번만 쓰며, 컴파일러는 우변 표현식의 정적 타입을 그대로 가져옵니다. 단, DATA()는 쓰기 가능 위치(write position)에서만 사용할 수 있습니다.

REPORT z_inline_data_demo.

DATA: gt_carriers TYPE TABLE OF scarr.

START-OF-SELECTION.
  " 1. SELECT INTO TABLE — 가장 흔한 패턴
  SELECT carrid, carrname, currcode
    FROM scarr
    INTO TABLE @DATA(lt_scarr).        " lt_scarr 의 타입은 STANDARD TABLE OF (carrid, carrname, currcode)

  " 2. LOOP AT ... INTO DATA(...)
  LOOP AT lt_scarr INTO DATA(ls_carrier).
    WRITE: / ls_carrier-carrid, ls_carrier-carrname.
  ENDLOOP.

  " 3. CALL METHOD RECEIVING / EXPORTING 인라인 수신
  cl_abap_typedescr=>describe_by_data( lt_scarr )
    ->get_relative_name( RECEIVING p_relative_name = DATA(lv_typename) ).
  WRITE: / lv_typename.

주의할 점은 DATA()가 선언과 동시에 초기화되는 자리에서만 동작한다는 것입니다. 다음 코드는 컴파일 오류가 납니다.

" 컴파일 에러: DATA() 는 읽기 위치에서 쓸 수 없음
IF DATA(lv_flag) = abap_true.   " ERROR
ENDIF.

" 올바른 사용
DATA(lv_flag) = check_status( ).
IF lv_flag = abap_true.
ENDIF.

2단계: FINAL()로 불변 변수 선언 + 타입 추론

FINAL()은 ABAP 7.57(7.58)에서 도입된 비교적 최신 구문으로, 한 번 대입되면 다시 쓸 수 없는 불변(immutable) 로컬 변수를 선언합니다. 타입 추론 방식은 DATA()와 동일하지만, 의도가 다릅니다. 읽기 전용임을 컴파일러가 검증해 주므로 부주의한 재할당을 막을 수 있습니다.

METHOD calculate_total.
  " 한 번만 채워지고 이후로는 읽기 전용
  FINAL(lt_items) = get_items_for_order( iv_order_id ).

  FINAL(lv_total) = REDUCE decfloat34(
    INIT sum = CONV decfloat34( 0 )
    FOR ls_item IN lt_items
    NEXT sum = sum + ls_item-price * ls_item-quantity ).

  " lt_items[ 1 ]-price = 0.   " 컴파일 에러: FINAL 은 재할당 금지
  rv_total = lv_total.
ENDMETHOD.

실무에서 FINAL()은 다음과 같은 상황에서 일반적으로 권장됩니다.

  • SELECT 결과처럼 한 번 읽은 후 변경하지 않을 데이터셋
  • REDUCE/FILTER 결과를 받는 임시 변수
  • 메서드 시작부에서 한 번 계산하고 끝까지 참조만 하는 상수성 값

반대로 LOOP 내부에서 누적 변경되는 변수나 명시적으로 MODIFY 해야 하는 내부 테이블에는 DATA()를 써야 합니다.

3단계: VALUE #() 생성자와 타입 추론 조합

VALUE는 구조체, 내부 테이블, 참조 등 복합 타입의 값을 한 줄에 만들어 내는 생성자 표현식입니다. VALUE 뒤에는 반드시 타입이 와야 하는데, 이 자리에 #을 쓰면 "호출 컨텍스트에서 추론하라"는 의미가 됩니다.

TYPES: BEGIN OF ty_addr,
         city TYPE string,
         zip  TYPE string,
       END OF ty_addr.

DATA: ls_addr TYPE ty_addr,
      lt_addr TYPE STANDARD TABLE OF ty_addr.

" 좌변 타입이 ty_addr 로 명시되어 있으므로 # 위치에 ty_addr 추론
ls_addr = VALUE #( city = 'Seoul' zip = '04524' ).

" 내부 테이블도 동일
lt_addr = VALUE #(
  ( city = 'Seoul'  zip = '04524' )
  ( city = 'Busan'  zip = '48058' )
  ( city = 'Daegu'  zip = '41940' )
).

함수형 메서드 호출에서도 자주 등장합니다. 매개변수의 시그니처가 타입을 결정합니다.

cl_log=>write(
  is_entry = VALUE #(
    severity = 'E'
    code     = 'ORD-001'
    message  = |주문 { iv_order_id } 처리 실패|
  )
).

타입을 추론할 수 없는 위치, 예컨대 DATA(...) = VALUE #( ... )처럼 좌변도 추론을 요청하는 상황에서는 컴파일 에러가 납니다. 한쪽은 반드시 명시적이어야 합니다.

" 컴파일 에러: 양쪽 모두 # 또는 DATA() — 단서 부족
" DATA(ls_x) = VALUE #( city = 'Seoul' ).

" 해결 1: 우변 타입 명시
DATA(ls_x) = VALUE ty_addr( city = 'Seoul' ).

" 해결 2: 좌변 타입 명시
DATA ls_y TYPE ty_addr.
ls_y = VALUE #( city = 'Seoul' ).

CAST() 연산자와 타입 변환

CAST는 객체 참조(Object Reference)나 데이터 참조(Data Reference)에 대해 다운캐스트(narrowing cast)를 수행하는 연산자입니다. 컴파일 타임에는 결과 타입이 결정되지만, 실제 대입 가능 여부는 런타임에 검사되며 실패 시 CX_SY_MOVE_CAST_ERROR가 발생합니다.

INTERFACE if_animal.
  METHODS speak.
ENDINTERFACE.

CLASS lcl_dog DEFINITION.
  PUBLIC SECTION.
    INTERFACES if_animal.
    METHODS bark.
ENDCLASS.

CLASS lcl_dog IMPLEMENTATION.
  METHOD if_animal~speak.
    WRITE: / 'Woof'.
  ENDMETHOD.
  METHOD bark.
    WRITE: / 'Bark Bark!'.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  DATA(lo_animal) = CAST if_animal( NEW lcl_dog( ) ).
  lo_animal->speak( ).

  " 다운캐스트: if_animal -> lcl_dog
  TRY.
      DATA(lo_dog) = CAST lcl_dog( lo_animal ).
      lo_dog->bark( ).
    CATCH cx_sy_move_cast_error INTO DATA(lx_cast).
      WRITE: / |캐스트 실패: { lx_cast->get_text( ) }|.
  ENDTRY.

CAST의 진가는 메서드 체이닝에서 드러납니다. 중간 임시 변수를 두지 않고도 다운캐스트 직후 메서드를 호출할 수 있습니다.

CAST cl_salv_table(
  cl_salv_table=>factory(
    IMPORTING r_salv_table = DATA(lo_alv)
    CHANGING  t_table      = lt_data ) )->display( ).

런타임 검사 비용이 부담스러운 핫 루프에서는 ?= 대신 미리 한 번 캐스트해 두고 결과를 변수에 보관하는 패턴이 일반적으로 권장됩니다.

FOR expression에서의 타입 추론

FOR expression은 VALUE, REDUCE, NEW 등의 생성자 내부에서 반복을 표현하는 구문입니다. 루프 변수의 타입은 소스 내부 테이블의 행 타입으로부터 자동 추론됩니다.

TYPES: BEGIN OF ty_order,
         id    TYPE i,
         price TYPE decfloat34,
         qty   TYPE i,
       END OF ty_order,
       tt_order TYPE STANDARD TABLE OF ty_order WITH EMPTY KEY.

DATA(lt_orders) = VALUE tt_order(
  ( id = 1 price = '10.00' qty = 3 )
  ( id = 2 price = '25.50' qty = 2 )
  ( id = 3 price = '7.25'  qty = 5 )
).

" 1. VALUE + FOR — 매핑(map)
DATA(lt_lines) = VALUE string_table(
  FOR ls IN lt_orders
  ( |주문 { ls-id }: { ls-price * ls-qty }| )
).

" 2. REDUCE + FOR — 집계(reduce)
DATA(lv_total) = REDUCE decfloat34(
  INIT s = CONV decfloat34( 0 )
  FOR ls IN lt_orders
  NEXT s = s + ls-price * ls-qty
).

" 3. FOR + WHERE — 필터링
DATA(lt_bigorders) = VALUE tt_order(
  FOR ls IN lt_orders WHERE ( qty >= 3 )
  ( ls )
).

FOR ls IN lt_orders에서 lsty_order로 추론됩니다. FOR GROUPS나 중첩 FOR도 동일한 원리로 동작하며, 그룹 키 변수와 그룹 멤버 참조의 타입이 모두 자동 결정됩니다.

DATA(lt_grouped) = VALUE string_table(
  FOR GROUPS <grp> OF ls IN lt_orders
    GROUP BY ( key = ls-qty COUNT = GROUP SIZE )
  ( |수량 { <grp>-key }: { <grp>-count }건| )
).

자주 겪는 함정과 실전 팁

인라인 선언과 타입 추론은 강력하지만, 다음과 같은 함정이 흔히 보고됩니다.

  • 스코프 오해: DATA()는 선언된 블록(메서드/폼) 전체에서 유효합니다. IF 블록 안에서 선언했다고 그 안에서만 살아 있는 게 아니므로, 같은 이름의 중복 선언을 시도하면 컴파일 에러가 납니다.
  • SELECT INTO @DATA의 컬럼 순서: SELECT *와 컬럼을 명시한 SELECT가 만드는 인라인 타입은 다릅니다. SELECT *는 DB 테이블 구조 전체, 컬럼 지정은 그 컬럼들만 갖는 새로운 익명 구조체입니다.
  • VALUE #() 의 빈 라인: VALUE #( )처럼 비어 있으면 해당 타입의 초기값이 됩니다. 내부 테이블이면 빈 테이블, 구조체면 모든 필드가 초기값입니다.
  • CAST 실패 무시: CAST의 결과를 그대로 메서드 호출에 사용할 때 CX_SY_MOVE_CAST_ERROR를 잡지 않으면 덤프로 직행합니다. 다형성을 다룰 때는 항상 TRY..CATCH로 감싸는 것이 권장됩니다.
  • FINAL() 남발: 디버깅 중 임시로 값을 바꿔 보려 할 때 FINAL()로 선언된 변수는 수정이 불가능합니다. 의도적으로 불변임을 표현할 때만 쓰는 것이 일반적으로 권장됩니다.

실전 팁을 정리하면, 첫째, 의도를 코드에 새기는 도구로 생각하는 것이 좋습니다. FINAL()은 "이 값은 더 이상 안 바뀐다"는 약속이고, DATA()는 "여기서 처음 만든다"는 표식입니다. 둘째, 가독성이 떨어지는 자리에서는 과감히 명시적 타입으로 돌아가는 편이 유지보수에 유리합니다. 특히 외부 인터페이스 시그니처나 RAP BDEF 핸들러 메서드처럼 표준이 정해진 자리는 타입을 그대로 적어 주는 편이 디버깅에 도움이 됩니다. 셋째, ADT(ABAP Development Tools)에서 변수 위에 마우스를 올리면 추론된 정확한 타입을 확인할 수 있으니 의심스러울 때는 도구의 도움을 받는 것이 좋습니다.

ABAP 컴파일러의 타입 추론은 단순히 코드를 짧게 만들기 위한 장식이 아니라, 리팩토링 안전성과 의도 표현력이라는 두 마리 토끼를 잡는 설계 도구입니다. 작은 메서드부터 적용해 보면서 팀의 코딩 스타일에 자연스럽게 녹여 가는 접근이 권장됩니다.

댓글 0

아직 댓글이 없습니다.