ABAP

CAST vs MOVE ?TO — 다운캐스팅 비교 #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 및 핵심 포인트

ABAP 객체 지향 프로그래밍에서 참조 변수(reference variable) 간 형 변환은 일상적으로 발생합니다. 과거에는 MOVE source ?TO target 또는 MOVE source TO target TYPE 구문을 사용했지만, ABAP 7.40 SP02부터 도입된 CAST 연산자가 더 안전하고 표현력 있는 대안으로 자리 잡았습니다. 이 글에서는 두 구문의 차이, 업캐스팅·다운캐스팅의 동작 원리, 그리고 실무에서 CAST를 선택해야 하는 명확한 이유를 다룹니다.

  • 참조 변수의 정적 타입(static type)과 동적 타입(dynamic type) 구분
  • 업캐스팅과 다운캐스팅의 차이, 컴파일러가 보장하는 범위
  • CAST 연산자의 인라인 사용과 메서드 체이닝 패턴
  • CX_SY_MOVE_CAST_ERROR 예외 처리 전략
  • 레거시 ?TO/MOVE TYPE 코드를 안전하게 마이그레이션

알아두면 좋은 배경 지식

이 글을 편하게 읽으려면 ABAP OO의 기본 개념인 클래스·인터페이스·상속 관계를 이해하고 있어야 합니다. DATA(lo_ref) = NEW zcl_demo( ) 형태의 인라인 선언, 인터페이스 참조 변수, 그리고 TRY ... CATCH ... ENDTRY 예외 처리 구문에 익숙하다면 충분합니다. 또한 7.40 이상의 표현식 기반 ABAP 문법(NEW, VALUE, CONV 등 생성자 연산자)에 대한 경험이 있으면 CAST의 디자인 철학을 빠르게 파악할 수 있습니다.

실행 환경 및 버전 전제

이 글의 코드는 다음 환경을 기준으로 작성되었습니다.

  • SAP NetWeaver AS ABAP 7.52 이상 또는 SAP S/4HANA 2022/2023 (On-Premise·Private Cloud)
  • ABAP Cloud / Steampunk(ABAP Environment) 환경에서도 동일하게 동작
  • ADT(ABAP Development Tools for Eclipse) 사용을 권장 — SE80에서도 컴파일은 되지만 인라인 선언 가독성은 ADT가 더 낫습니다
  • CAST 연산자는 7.40 SP02부터 가용, EXACT/CORRESPONDING 등의 동반 연산자는 7.40 SP05 이상
  • 예제는 ABAP 표준 SQL 객체나 사용자 정의 클래스에 의존하지 않으며, 별도 마스터 데이터 없이 실행 가능합니다

버전이 낮은 시스템에서는 CAST 구문이 신택스 오류를 일으키므로, 시스템의 sy-saprl 또는 ADT의 ABAP 릴리스 정보로 사전 확인이 필요합니다.

핵심 개념: 정적 타입, 동적 타입, 그리고 두 가지 캐스팅

참조 변수에는 두 개의 타입이 동시에 존재한다는 점을 먼저 이해해야 합니다. 정적 타입은 변수를 선언할 때 명시한 클래스/인터페이스이며, 컴파일러가 메서드·속성 호출 가능 여부를 판단하는 기준입니다. 동적 타입은 런타임에 실제로 가리키고 있는 객체의 클래스이며, 다형성(polymorphism)의 원천입니다.

비유하자면 정적 타입은 "택배 송장에 적힌 품목 분류"이고, 동적 타입은 "상자 안에 실제로 들어 있는 물건"입니다. 송장에는 "전자제품"이라고 적혀 있어도 안에는 노트북이나 헤드폰처럼 더 구체적인 물건이 들어 있을 수 있죠.

이 관계에서 두 가지 변환이 나옵니다.

  • 업캐스팅(Upcasting, 와이드닝): 자식 → 부모 방향. 항상 안전하므로 컴파일러가 자동으로 처리하고 단순 대입(=)으로 충분합니다.
  • 다운캐스팅(Downcasting, 내로잉): 부모 → 자식 방향. 실패할 수 있으므로 명시적인 캐스트 연산이 필요합니다. 여기서 ?=, MOVE ... ?TO ..., 그리고 CAST가 등장합니다.

세 구문이 수행하는 런타임 동작은 본질적으로 같습니다. 그러나 CAST표현식이라는 점이 결정적인 차이를 만듭니다. 변환과 동시에 메서드 호출이나 인라인 선언이 가능하고, 임시 변수를 줄여 코드 의도가 한눈에 드러납니다. 반면 MOVE ... ?TO문장이라 별도 변수를 거쳐야 하고, 자연스럽게 코드가 길어집니다.

또 한 가지 자주 혼동되는 MOVE source TO target TYPE type_name 구문은 RTTS(런타임 타입 시스템) 기반의 동적 타입 검사를 수행하는 별개의 문장으로, 일반적인 객체 다운캐스팅과는 다른 사용처(예: 동적 RTTI 분기)에 가깝습니다. 객체 참조 변환이라면 CAST로 통일하는 것이 모던 ABAP의 컨벤션입니다.

실전 코드 1단계: 기본 다운캐스팅 비교

먼저 간단한 클래스 계층을 만들고, 동일한 다운캐스팅을 세 가지 방식으로 작성해 차이를 확인합니다. 시나리오는 "배송 항목(DeliveryItem) 계층"입니다.

CLASS zcl_delivery_item DEFINITION PUBLIC CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS get_item_id RETURNING VALUE(rv_id) TYPE string.
  PROTECTED SECTION.
    DATA mv_id TYPE string VALUE 'BASE-0001'.
ENDCLASS.

CLASS zcl_delivery_item IMPLEMENTATION.
  METHOD get_item_id.
    rv_id = mv_id.
  ENDMETHOD.
ENDCLASS.

CLASS zcl_cold_chain_item DEFINITION INHERITING FROM zcl_delivery_item.
  PUBLIC SECTION.
    METHODS get_temperature RETURNING VALUE(rv_temp) TYPE i.
  PRIVATE SECTION.
    DATA mv_temp TYPE i VALUE -18.
ENDCLASS.

CLASS zcl_cold_chain_item IMPLEMENTATION.
  METHOD get_temperature.
    rv_temp = mv_temp.
  ENDMETHOD.
ENDCLASS.

이제 자식 객체를 부모 참조에 담은 뒤, 다시 자식 타입으로 끌어내리는 세 가지 방법입니다.

DATA(lo_base) = CAST zcl_delivery_item( NEW zcl_cold_chain_item( ) ).

" 방법 1: 레거시 MOVE ?TO 구문
DATA lo_child_a TYPE REF TO zcl_cold_chain_item.
MOVE lo_base ?TO lo_child_a.
WRITE / lo_child_a->get_temperature( ).

" 방법 2: 단축 대입 연산자 ?=
DATA lo_child_b TYPE REF TO zcl_cold_chain_item.
lo_child_b ?= lo_base.
WRITE / lo_child_b->get_temperature( ).

" 방법 3: CAST 연산자 (권장)
DATA(lo_child_c) = CAST zcl_cold_chain_item( lo_base ).
WRITE / lo_child_c->get_temperature( ).

세 방법 모두 동일한 런타임 결과를 내지만, 세 번째 방식은 별도 선언 라인이 필요 없고 결과 타입이 코드의 흐름 안에 명시되어 있습니다. 더 나아가 CAST는 메서드 호출과 결합할 수 있습니다.

WRITE / CAST zcl_cold_chain_item( lo_base )->get_temperature( ).

실전 코드 2단계: 예외 처리와 로깅이 있는 실무 패턴

다운캐스팅은 본질적으로 실패 가능성을 내포합니다. 잘못된 동적 타입을 가진 참조를 강제로 변환하면 CX_SY_MOVE_CAST_ERROR가 발생합니다. 실무에서는 이를 그대로 노출하지 않고, 로깅·대체 처리·도메인 예외로 감싸는 것이 일반적입니다.

CLASS zcl_shipment_processor DEFINITION PUBLIC CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS process_item
      IMPORTING io_item TYPE REF TO zcl_delivery_item
      RAISING   zcx_shipment_invalid.
  PRIVATE SECTION.
    METHODS log_warning IMPORTING iv_msg TYPE string.
ENDCLASS.

CLASS zcl_shipment_processor IMPLEMENTATION.
  METHOD process_item.
    TRY.
        DATA(lo_cold) = CAST zcl_cold_chain_item( io_item ).
        IF lo_cold->get_temperature( ) > -10.
          log_warning( |온도 이상: { lo_cold->get_item_id( ) }| ).
        ENDIF.

      CATCH cx_sy_move_cast_error INTO DATA(lx_cast).
        log_warning( |냉장 항목 아님, 일반 처리: { io_item->get_item_id( ) }| ).
        RAISE EXCEPTION TYPE zcx_shipment_invalid
          EXPORTING
            previous = lx_cast
            item_id  = io_item->get_item_id( ).
    ENDTRY.
  ENDMETHOD.

  METHOD log_warning.
    cl_demo_output=>write( iv_msg ).
  ENDMETHOD.
ENDCLASS.

CAST가 단일 표현식이므로 TRY 블록의 범위가 명확합니다. MOVE ?TO를 사용하면 별도 선언 후 변환 라인을 TRY 안에 두어야 하고, 변수 스코프가 블록 외부로 새어 나가 가독성이 떨어집니다.

런타임 예외 비용이 부담스러운 루프 내부에서는 다음 패턴을 씁니다.

LOOP AT lt_items INTO DATA(lo_item).
  CHECK lo_item IS INSTANCE OF zcl_cold_chain_item.
  DATA(lo_cold) = CAST zcl_cold_chain_item( lo_item ).
  rt_temps = VALUE #( BASE rt_temps ( lo_cold->get_temperature( ) ) ).
ENDLOOP.

IS INSTANCE OF로 동적 타입을 먼저 확인하면 예외 비용 없이 안전하게 다운캐스팅할 수 있습니다.

실전 코드 3단계: 인터페이스 계층과 테스트 이중체 패턴

인터페이스 기반 설계와 팩토리 패턴이 결합된 코드에서 CAST의 가치는 더욱 커집니다. 결제 게이트웨이 추상화를 예로 들어 인터페이스 → 구체 클래스로 내려가는 경로를 살펴봅니다.

INTERFACE zif_payment_gateway.
  METHODS authorize
    IMPORTING iv_amount        TYPE p
    RETURNING VALUE(rv_auth_id) TYPE string.
ENDINTERFACE.

CLASS zcl_payment_stripe DEFINITION PUBLIC CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_payment_gateway.
    METHODS get_idempotency_key RETURNING VALUE(rv_key) TYPE string.
ENDCLASS.

CLASS zcl_payment_stripe IMPLEMENTATION.
  METHOD zif_payment_gateway~authorize.
    rv_auth_id = |STRIPE-{ sy-uzeit }|.
  ENDMETHOD.
  METHOD get_idempotency_key.
    rv_key = cl_system_uuid=>create_uuid_x16_static( ).
  ENDMETHOD.
ENDCLASS.

호출 측에서는 인터페이스 참조를 받지만, Stripe 특화 기능(멱등성 키)이 필요할 때만 다운캐스팅합니다.

METHOD charge_with_retry.
  DATA(lv_auth) = io_gateway->authorize( iv_amount = iv_amount ).

  IF io_gateway IS INSTANCE OF zcl_payment_stripe.
    DATA(lv_idem) = CAST zcl_payment_stripe( io_gateway )->get_idempotency_key( ).
    write_audit_trail( iv_auth = lv_auth iv_idem = lv_idem ).
  ENDIF.
ENDMETHOD.

ABAP Unit 테스트에서도 CAST는 유리합니다. 의존성을 더블(double)로 주입한 뒤 검증용 헬퍼 메서드에 접근할 때 임시 변수 없이 처리할 수 있습니다.

METHOD test_authorize_logs_audit.
  DATA(lo_double) = NEW zcl_payment_stripe_test_double( ).
  cut->set_gateway( lo_double ).
  cut->charge_with_retry( iv_amount = 1500 ).

  DATA(lv_calls) = CAST zcl_payment_stripe_test_double(
                     cut->get_gateway( )
                   )->get_call_count( ).
  cl_abap_unit_assert=>assert_equals( exp = 1 act = lv_calls ).
ENDMETHOD.

자주 마주치는 실수와 트러블슈팅

  • CAST가 신택스 오류를 일으킵니다 — 시스템 릴리스가 7.40 SP02 미만이거나 클래식 ABAP 프로그램에서 언어 버전이 다운그레이드되어 있을 수 있습니다. ADT에서 패키지/프로그램의 ABAP 언어 버전을 확인하세요.
  • CX_SY_MOVE_CAST_ERROR가 잡히지 않습니다 — 이 예외는 CX_DYNAMIC_CHECK의 하위 클래스로, 잡지 않으면 런타임 덤프로 이어집니다. 다운캐스팅이 있는 모든 경로에 TRY를 두거나 IS INSTANCE OF로 사전 검사하세요.
  • 인터페이스 참조를 다른 인터페이스로 캐스팅할 수 있나요 — 동일 객체가 두 인터페이스를 모두 구현하고 있다면 가능합니다. 그러나 처음부터 필요한 인터페이스 참조를 보유하도록 설계하는 편이 가독성이 더 좋습니다.
  • MOVE TO target TYPE 문자열로 타입을 주는 코드를 CAST로 바꿔도 되나요 — 동적 타입을 문자열로 받는 구문은 정적 CAST로 직접 치환할 수 없습니다. RTTS(cl_abap_typedescr)와 ASSIGN ... CASTING의 조합을 검토해야 합니다.
  • ATC가 ?= 를 경고합니다 — S/4HANA 클린 코드 룰에서 표현식 기반 ABAP을 선호하도록 구성된 경우입니다. CAST로 마이그레이션하면 자연스럽게 해소됩니다.

이 글을 통해 알게 된 것

ABAP의 참조 변수 형 변환에서 CAST 연산자가 MOVE ?TO/?= 대비 갖는 본질적인 우위는 표현식이라는 속성에서 나옵니다. 임시 변수 없이 인라인 선언, 메서드 체이닝, IS INSTANCE OF 가드와의 자연스러운 결합이 가능하고, 코드 의도가 한 줄에 드러납니다.

단계별 핵심을 정리하면 다음과 같습니다. 첫째, 업캐스팅은 컴파일러가 자동 처리하므로 =으로 충분합니다. 둘째, 다운캐스팅은 CAST 단일 표현식으로 작성하고, 루프 등 성능 민감 구간에서는 IS INSTANCE OF 가드를 앞에 둡니다. 셋째, 예외 처리는 항상 명시적 CATCH cx_sy_move_cast_error로 감싸고 도메인 예외로 변환하는 방식을 택합니다. 넷째, 레거시 ?=/MOVE ?TO 코드는 클린 ABAP 마이그레이션 기회가 생길 때 교체합니다.

모던 ABAP 표현식 패밀리(NEW, VALUE, CONV, REF)와 함께 CAST를 익히면, 불필요한 임시 변수를 줄이고 코드의 흐름과 의도를 함께 표현하는 ABAP OO 코드를 작성할 수 있습니다.

댓글 0

아직 댓글이 없습니다.