ABAP

MOVE TYPE vs CAST — ABAP 이렇게 바뀐다 #shorts #SAP #ABAP

▶ YouTube에서 보기

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

ABAP에서 객체 참조(class reference) 간의 형 변환은 오랜 기간 ?= 연산자 또는 MOVE ... ?TO ... 구문으로 수행되어 왔습니다. ABAP 7.40 SP08 이후 도입된 CAST 연산자는 인라인 선언과 다운캐스트를 한 줄로 처리해 코드 가독성을 크게 끌어올립니다. 이 글에서는 결재 문서(Document/Approval) 도메인을 예로 들어 CAST의 동작 원리, 안전한 예외 처리, 레거시 패턴과의 비교를 차례대로 살펴봅니다.

  • CAST 연산자의 기본 문법과 선언 동시화 패턴 이해
  • CX_SY_MOVE_CAST_ERROR 예외를 활용한 방어적 다운캐스트
  • 레거시 MOVE TYPE / FIELD-SYMBOL 패턴과 CAST 현대 패턴의 차이
  • RTTI(CL_ABAP_TYPEDESCR)와 CAST를 결합한 동적 처리
  • 단위 테스트와 성능 측면에서 CAST를 다루는 방법

먼저 알아두면 좋은 배경

이 글은 ABAP Objects의 클래스 상속, 인터페이스, 참조 변수(REF TO) 개념을 한 번이라도 작성해 본 개발자를 대상으로 합니다. DATA(...)를 이용한 인라인 선언, NEW 연산자, TRY ... CATCH ... ENDTRY 예외 처리 흐름도 익숙해야 코드 흐름을 따라가기 수월합니다. 업캐스트(부모로 좁힘)와 다운캐스트(자식으로 넓힘)의 개념 차이도 미리 정리해 두면 좋습니다.

실습 환경 및 준비물

예제 코드는 다음 환경에서 검증되는 것을 기준으로 작성되었습니다.

  • ABAP 플랫폼: SAP NetWeaver AS ABAP 7.52 이상 또는 SAP S/4HANA 1909 이상
  • 언어 버전: ABAP 7.40 SP08 이상(CAST 연산자 지원 시작). ABAP Cloud / Steampunk 환경에서도 동일하게 사용 가능
  • 개발 도구: ABAP Development Tools(ADT for Eclipse) 권장. SE80/SE24도 가능하지만 인라인 선언 가독성은 ADT가 우수합니다.
  • 패키지/권한: 로컬 객체($TMP) 또는 사용자 패키지에 클래스 2~3개 생성 권한
  • 테스트: ABAP Unit(CLASS ... FOR TESTING) 실행 가능 권한

예제는 결재 문서 처리(승인/반려)를 모델링한 가상의 zcl_doc_base / zcl_doc_invoice / zcl_doc_purchase 클래스 계층을 가정합니다. 실제 시스템에 옮길 때는 네임스페이스 접두어와 패키지 규칙에 맞춰 이름을 조정하세요.

핵심 개념: CAST 연산자란 무엇인가

CAST는 한 줄로 표현하면 "참조 변수를 더 구체적인 타입으로 본다고 ABAP에게 알려주는 연산자"입니다. 비유하자면, 회사에 도착한 우편물 상자 겉면에는 "문서"라고만 적혀 있지만, 내용물이 사실은 "세금계산서"라는 것을 알고 있을 때 "이 상자는 세금계산서로 처리해 주세요"라고 명시하는 것과 같습니다. 잘못된 라벨을 붙이면(실제 내용물이 세금계산서가 아니면) 런타임에 CX_SY_MOVE_CAST_ERROR가 발생합니다.

CAST와 기존 ?= 연산자는 의미상 동일한 다운캐스트를 수행하지만, 표현식 위치에서 사용할 수 있다는 점이 결정적인 차이입니다.

  • 업캐스트(Up-cast): 자식 → 부모. 안전하므로 일반 대입(=)으로 충분
  • 다운캐스트(Down-cast): 부모 → 자식. 실패 가능성이 있으므로 CAST 또는 ?= 필요
  • 인터페이스 캐스트: 클래스 ↔ 인터페이스 사이의 변환에도 CAST 사용 가능

도식적으로 표현하면 아래와 같은 흐름입니다.

"          [zcl_doc_base]    <-- 부모 (포괄적 타입)
"              ^      ^
"              |      |
"     [zcl_doc_invoice] [zcl_doc_purchase]  <-- 자식 (구체 타입)
"
" lo_base TYPE REF TO zcl_doc_base 가 실제로는 invoice 인스턴스라면
" DATA(lo_inv) = CAST zcl_doc_invoice( lo_base ).  " OK
" 그렇지 않으면 -> CX_SY_MOVE_CAST_ERROR 발생

CAST는 인스턴스 생성을 하지 않습니다. 메모리상의 같은 객체를 가리키되, 컴파일러/런타임이 더 구체적인 인터페이스로 다룰 수 있게 "관점을 바꿔주는" 역할만 합니다. 이 점이 MOVE-CORRESPONDING(구조체 필드 복사)와는 본질적으로 다릅니다.

1단계: CAST 기본 사용법

가장 단순한 시나리오부터 시작합니다. 결재 문서 기본 클래스와 인보이스 자식 클래스를 정의한 뒤, 부모 참조에서 자식 메서드를 호출해야 하는 상황입니다.

CLASS zcl_doc_base DEFINITION PUBLIC CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS get_doc_id RETURNING VALUE(rv_id) TYPE string.
  PROTECTED SECTION.
    DATA mv_doc_id TYPE string.
ENDCLASS.

CLASS zcl_doc_base IMPLEMENTATION.
  METHOD get_doc_id.
    rv_id = mv_doc_id.
  ENDMETHOD.
ENDCLASS.

CLASS zcl_doc_invoice DEFINITION PUBLIC INHERITING FROM zcl_doc_base
  CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS constructor IMPORTING iv_id     TYPE string
                                 iv_amount TYPE p LENGTH 13 DECIMALS 2.
    METHODS calc_vat RETURNING VALUE(rv_vat) TYPE p LENGTH 13 DECIMALS 2.
  PRIVATE SECTION.
    DATA mv_amount TYPE p LENGTH 13 DECIMALS 2.
ENDCLASS.

CLASS zcl_doc_invoice IMPLEMENTATION.
  METHOD constructor.
    super->constructor( ).
    mv_doc_id = iv_id.
    mv_amount = iv_amount.
  ENDMETHOD.
  METHOD calc_vat.
    rv_vat = mv_amount * '0.10'.
  ENDMETHOD.
ENDCLASS.

호출부에서 CAST를 사용하면 다음과 같이 간결해집니다.

DATA(lo_doc) = CAST zcl_doc_base(
  NEW zcl_doc_invoice( iv_id = 'INV-2026-0001'
                       iv_amount = 1500 ) ).

" lo_doc 는 부모 타입이지만 실제 인스턴스는 invoice
" 자식 전용 메서드 호출을 위해 다운캐스트 + 즉시 호출
DATA(lv_vat) = CAST zcl_doc_invoice( lo_doc )->calc_vat( ).

WRITE: / lo_doc->get_doc_id( ), lv_vat.

여기서 주목할 점은 두 가지입니다. 첫째, CAST는 표현식이므로 메서드 체이닝(CAST ...( )->method( ))이 가능합니다. 둘째, 중간 변수 선언이 사라져 노이즈가 줄어듭니다. ABAP 7.40 이전에는 같은 처리를 위해 보통 다음과 같이 작성했습니다.

DATA: lo_doc TYPE REF TO zcl_doc_base,
      lo_inv TYPE REF TO zcl_doc_invoice,
      lv_vat TYPE p LENGTH 13 DECIMALS 2.

CREATE OBJECT lo_inv EXPORTING iv_id = 'INV-2026-0001'
                               iv_amount = 1500.
lo_doc = lo_inv.
lo_inv ?= lo_doc.
lv_vat = lo_inv->calc_vat( ).

2단계: 안전한 다운캐스트와 예외 처리

실무에서는 어떤 자식 타입이 들어올지 호출 시점에 확정되지 않는 경우가 많습니다. 결재 큐에서 꺼낸 문서가 인보이스일 수도, 구매요청일 수도 있습니다. 이때 CAST를 무방비로 호출하면 CX_SY_MOVE_CAST_ERROR가 발생하면서 단기 덤프로 이어질 수 있습니다.

METHOD process_document.
  " IMPORTING io_doc TYPE REF TO zcl_doc_base
  " RAISING zcx_doc_processing

  TRY.
      DATA(lo_invoice) = CAST zcl_doc_invoice( io_doc ).
      DATA(lv_vat)     = lo_invoice->calc_vat( ).

      " 로깅: BAL (Application Log) 또는 LOG-POINT 사용 권장
      MESSAGE i001(zdoc) WITH lo_invoice->get_doc_id( ) lv_vat.

    CATCH cx_sy_move_cast_error INTO DATA(lx_cast).
      " 인보이스가 아닌 다른 자식 타입이거나 부모 인스턴스인 경우
      " 1) 다른 타입으로 한 번 더 시도
      TRY.
          DATA(lo_po) = CAST zcl_doc_purchase( io_doc ).
          lo_po->trigger_approval( ).
        CATCH cx_sy_move_cast_error.
          " 2) 둘 다 아닐 경우 도메인 예외로 래핑
          RAISE EXCEPTION TYPE zcx_doc_processing
            EXPORTING textid   = zcx_doc_processing=>unsupported_type
                      previous = lx_cast.
      ENDTRY.
  ENDTRY.
ENDMETHOD.

여러 분기를 다뤄야 한다면 CASE TYPE OF(ABAP 7.50+)를 함께 쓰는 편이 가독성에 유리합니다.

CASE TYPE OF io_doc.
  WHEN TYPE zcl_doc_invoice INTO DATA(lo_inv).
    lo_inv->calc_vat( ).
  WHEN TYPE zcl_doc_purchase INTO DATA(lo_po).
    lo_po->trigger_approval( ).
  WHEN OTHERS.
    RAISE EXCEPTION TYPE zcx_doc_processing.
ENDCASE.

CASE TYPE OF는 내부적으로 안전한 다운캐스트를 수행하므로 별도의 CAST + TRY 블록 없이도 동일한 결과를 얻을 수 있고, 누락된 타입을 코드 리뷰에서 발견하기도 쉬워집니다.

3단계: 레거시 패턴 vs CAST 현대 패턴 비교 + 프로덕션 고려사항

레거시 코드에서 자주 보이는 패턴은 MOVE ... ?TO ..., ASSIGN COMPONENT ... OF STRUCTURE ... TO <FS>, 또는 MOVE-CORRESPONDING입니다. 이 중 객체 참조에 해당하는 것은 MOVE ?TO / ?= 뿐이며, 나머지는 구조체용입니다. 혼동을 피하기 위해 비교표로 정리합니다.

목적 구식 패턴 현대 패턴 (CAST 계열)
다운캐스트 lo_child ?= lo_parent. DATA(lo_child) = CAST cl_child( lo_parent ).
다운캐스트 후 메서드 호출 변수 선언 → 캐스트 → 호출 3줄 CAST cl_child( lo_parent )->do_it( ).
타입 분기 IF ... INSTANCE OF ... . + ?= CASE TYPE OF ... WHEN TYPE ... INTO ...
구조체 필드 복사 MOVE-CORRESPONDING CORRESPONDING #( ... MAPPING ... )

프로덕션 코드에서는 CAST 자체보다 "왜 다운캐스트가 필요한가"를 먼저 검토하는 것이 좋습니다. 다형성을 제대로 설계하면 다운캐스트가 거의 사라지기 때문입니다. 그래도 외부 인터페이스 경계(BAdI, RFC, 큐 메시지)에서는 CAST가 불가피합니다. 다음은 ABAP Unit 테스트와 RTTI를 결합한 프로덕션 수준 예제입니다.

CLASS ltcl_doc_processor DEFINITION FOR TESTING
  DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    METHODS:
      cast_to_invoice_ok      FOR TESTING,
      cast_to_invoice_fails   FOR TESTING,
      dynamic_cast_via_rtti   FOR TESTING.
ENDCLASS.

CLASS ltcl_doc_processor IMPLEMENTATION.

  METHOD cast_to_invoice_ok.
    DATA(lo_doc) = CAST zcl_doc_base(
      NEW zcl_doc_invoice( iv_id = 'T1' iv_amount = 1000 ) ).
    DATA(lo_inv) = CAST zcl_doc_invoice( lo_doc ).
    cl_abap_unit_assert=>assert_equals(
      act = lo_inv->calc_vat( ) exp = 100 ).
  ENDMETHOD.

  METHOD cast_to_invoice_fails.
    DATA(lo_doc) = CAST zcl_doc_base(
      NEW zcl_doc_purchase( iv_id = 'T2' ) ).
    TRY.
        DATA(lo_inv) = CAST zcl_doc_invoice( lo_doc ).
        cl_abap_unit_assert=>fail( 'CAST가 실패해야 합니다' ).
      CATCH cx_sy_move_cast_error.
        " expected
    ENDTRY.
  ENDMETHOD.

  METHOD dynamic_cast_via_rtti.
    DATA(lo_doc) = CAST zcl_doc_base(
      NEW zcl_doc_invoice( iv_id = 'T3' iv_amount = 500 ) ).

    DATA(lo_descr) = cl_abap_classdescr=>describe_by_object_ref( lo_doc ).
    IF lo_descr->absolute_name CS 'ZCL_DOC_INVOICE'.
      DATA(lv_vat) = CAST zcl_doc_invoice( lo_doc )->calc_vat( ).
      cl_abap_unit_assert=>assert_equals( act = lv_vat exp = 50 ).
    ENDIF.
  ENDMETHOD.

ENDCLASS.

성능 측면에서 CAST 자체는 ?=와 동등한 오버헤드를 가지며, 일반적으로 무시할 수 있는 수준입니다. 다만 루프 안에서 매 반복마다 동일한 객체를 반복 캐스트하는 것은 가독성을 해칠 수 있어, 루프 진입 전에 한 번만 캐스트해 지역 변수에 보관하는 편이 권장됩니다. 보안 관점에서는 RFC 등 외부 입력에서 받은 참조를 무조건 CAST 하지 말고, 화이트리스트 기반으로 허용된 타입만 처리하도록 작성하는 것이 권장됩니다.

자주 만나는 함정과 FAQ

실제 도입 과정에서 반복되는 질문을 정리했습니다.

  • Q1. CASTCONV는 어떻게 다른가요?
    CONV는 기본 데이터 타입(문자열, 숫자, 날짜 등) 사이의 값 변환을 표현식으로 수행합니다. 반면 CAST는 객체 참조 또는 데이터 참조의 정적 타입을 바꿉니다. 둘은 이름이 비슷할 뿐 용도가 다릅니다.
  • Q2. 인터페이스 참조도 CAST 가능한가요?
    네, CAST zif_approvable( lo_doc )처럼 인터페이스로의 캐스트도 가능합니다. 단, 해당 객체가 인터페이스를 실제로 구현하지 않으면 동일한 예외가 발생합니다.
  • Q3. NULL 참조(IS INITIAL)에 CAST하면 어떻게 되나요?
    초기 참조를 CAST하면 결과 역시 초기 참조가 되며 예외가 발생하지 않습니다. 그러나 그 참조로 메서드를 호출하면 CX_SY_REF_IS_INITIAL 덤프가 발생하므로, CAST 결과를 사용하기 전 항상 IS BOUND 또는 IS NOT INITIAL 체크를 두는 편이 권장됩니다.
  • Q4. CAST가 실패했는지 미리 알 수 있나요?
    IF lo_doc IS INSTANCE OF zcl_doc_invoice. 구문으로 사전 검사할 수 있습니다. 이는 캐스트를 시도하지 않으므로 예외도 발생하지 않습니다.
  • Q5. ABAP Cloud(Steampunk)에서도 동일하게 동작하나요?
    CAST 연산자는 ABAP Cloud에서도 지원됩니다. 다만 사용하는 표준 클래스가 릴리스 컨트랙트(C1/C2) 범위 내인지 확인해야 합니다.

또 하나 자주 발생하는 실수는 "업캐스트에 CAST를 쓰는 것"입니다. 부모로의 변환은 일반 대입(=)이면 충분하며, 굳이 CAST를 쓰면 가독성이 떨어지고 의도가 흐려집니다. CAST는 다운캐스트/인터페이스 변환에만 명시적으로 사용하는 규칙을 팀 내 코드 가이드에 명시하는 것이 좋습니다.

심화로 이어 보면 좋은 주제

  • RTTI/RTTC: CL_ABAP_TYPEDESCR 계열로 런타임 타입 정보 활용하기
  • CORRESPONDING 연산자: 구조체/내부테이블 간 매핑을 CAST와 유사한 표현식 스타일로 작성
  • NEW / VALUE / REDUCE: ABAP 7.40+ 표현식 지향 패러다임의 다른 연산자들
  • CASE TYPE OF 패턴: 다형성 분기를 안전하게 구현하는 방법
  • ABAP Cleaner / ATC 룰: 레거시 ?= 패턴을 CAST로 자동 리팩토링

더 깊이 읽을 만한 자료

댓글 0

아직 댓글이 없습니다.