ABAP

CAST vs ?= — 다운캐스팅 실수 3가지 #shorts #SAP #ABAP

이 글이 답하는 질문

레거시 ABAP 코드를 유지보수하다 보면 MOVE ?TO?= 연산자로 객체 참조를 다루는 패턴을 흔히 만납니다. 이런 코드는 ABAP 7.40 이후 도입된 CAST 연산자로 대체되어야 합니다. 단순한 문법 변경이 아니라 가독성, 표현식 기반 프로그래밍, 인라인 호출 체이닝, 그리고 예외 처리 일관성까지 영향을 미칩니다.

  • CAST와 ?= 다운캐스트 구문은 어떻게 다른가?
  • CX_SY_MOVE_CAST_ERROR는 언제 발생하는가?
  • 실무 클래스 계층에서 CAST를 어떻게 적용하는가?
  • TRY-CATCH와 CAST를 어떻게 결합해야 안전한가?

이 글을 보기 전에

ABAP Objects의 클래스 상속, 인터페이스 구현, 정적/동적 타입 개념을 이해하고 있어야 합니다. 또한 ABAP 7.40 SP08 이상에서 도입된 인라인 선언(DATA(...))과 표현식 기반 문법에 익숙하면 학습 곡선이 훨씬 완만해집니다. 예외 클래스 계층(CX_ROOT, CX_DYNAMIC_CHECK)에 대한 기본 지식도 도움이 됩니다.

테스트 환경과 사용한 버전

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

  • ABAP 플랫폼: SAP NetWeaver 7.50 이상 또는 SAP S/4HANA 1909 이상 (CAST는 7.40 SP02부터 사용 가능)
  • 개발 도구: ABAP Development Tools(ADT) for Eclipse 권장, SE80에서도 동작
  • 언어 버전: Standard ABAP, ABAP for Cloud Development 모두 지원
  • 권한: 패키지 생성, 클래스 생성 권한 (S_DEVELOP)

ABAP Cloud 환경에서도 CAST 연산자는 동일하게 동작하며, 클린 코어 전략에서 표현식 기반 문법이 권장됩니다.

핵심 개념: 왜 CAST인가

먼저 가상의 실무 상황을 떠올려 봅니다. 판매 문서를 다루는 슈퍼클래스 cl_sales_doc_base가 있고, 그 아래 견적(cl_quotation_handler), 수주(cl_sales_order_handler), 반품(cl_return_handler) 클래스가 상속받습니다. 외부 모듈에서 슈퍼클래스 참조로 받은 객체에서 자식 클래스 고유 메서드를 호출하려면 다운캐스트가 필요합니다.

옛날 방식은 다음과 같았습니다.

DATA: lo_base   TYPE REF TO cl_sales_doc_base,
      lo_quote  TYPE REF TO cl_quotation_handler.

lo_base = get_document( iv_doc_id = '00012345' ).
lo_quote ?= lo_base.       " 또는 MOVE lo_base ?TO lo_quote.
lo_quote->approve_quote( ).

?=는 다운캐스트로, 런타임에 타입이 맞지 않으면 즉시 단락(short dump)을 일으킵니다. 변수가 좌측에 와야 하므로 표현식 중간에 사용할 수 없고, 별도 임시 변수를 거쳐야 합니다.

세 가지 다운캐스트 방식 비교

구문 도입 버전 표현식 사용 인라인 선언 가독성
MOVE source ?TO target. ~ 4.6 불가 불가 낮음
target ?= source. ~ 6.10 불가 불가 중간
CAST type( source ) 7.40 SP02 가능 가능 높음

CAST는 함수형 연산자라서 표현식 위치에 그대로 들어갈 수 있습니다. 즉 메서드 호출 체이닝에 자연스럽게 녹아듭니다. 비유하자면 옛 방식은 택배를 받아서 박스를 열고 다시 다른 박스에 넣어 전달하는 과정이라면, CAST는 받은 박스에 라벨만 새로 붙여서 바로 사용하는 흐름입니다.

타입이 맞지 않으면 두 방식 모두 CX_SY_MOVE_CAST_ERROR 예외를 던지지만, CAST는 표현식이기 때문에 TRY-CATCH 블록 안에서 한 번에 여러 캐스팅과 메서드 호출을 묶을 수 있어 예외 경계가 명확해집니다.

1단계: 가장 기본적인 CAST 사용

먼저 인터페이스 기반의 간단한 예제부터 봅니다. if_sales_handler 인터페이스를 구현한 두 클래스가 있다고 가정합니다.

INTERFACE if_sales_handler.
  METHODS: process,
           get_doc_type RETURNING VALUE(rv_type) TYPE c LENGTH 4.
ENDINTERFACE.

CLASS cl_quotation_handler DEFINITION.
  PUBLIC SECTION.
    INTERFACES if_sales_handler.
    METHODS approve_quotation
      IMPORTING iv_approver TYPE syuname.
ENDCLASS.

CLASS cl_sales_order_handler DEFINITION.
  PUBLIC SECTION.
    INTERFACES if_sales_handler.
    METHODS confirm_delivery
      IMPORTING iv_delivery_date TYPE dats.
ENDCLASS.

인터페이스 참조로 받은 객체에서 구체 클래스의 메서드를 호출해야 한다면, 옛 방식 대신 다음과 같이 작성합니다.

DATA(lo_handler) = factory->create_handler( iv_doc_type = 'QT' ).

" 옛 방식 (지양)
DATA lo_quotation TYPE REF TO cl_quotation_handler.
lo_quotation ?= lo_handler.
lo_quotation->approve_quotation( iv_approver = sy-uname ).

" CAST 방식 (권장)
CAST cl_quotation_handler( lo_handler )->approve_quotation(
  iv_approver = sy-uname ).

두 번째 방식은 임시 변수가 사라지고 의도가 한 줄로 드러납니다.

2단계: 실무 시나리오 — 예외 처리와 로깅

실무에서는 항상 캐스팅이 성공한다고 가정할 수 없습니다. 문서 타입이 잘못 라우팅될 수 있고, 외부 시스템에서 들어오는 객체가 예상 타입이 아닐 수도 있습니다. 다음은 판매 문서 라우터에서 CAST와 예외 처리를 결합한 패턴입니다.

CLASS cl_sales_doc_dispatcher DEFINITION FINAL.
  PUBLIC SECTION.
    METHODS dispatch
      IMPORTING io_handler TYPE REF TO if_sales_handler
                iv_action  TYPE string
      RAISING   cx_sales_dispatch_error.
  PRIVATE SECTION.
    METHODS log_cast_error
      IMPORTING ix_error TYPE REF TO cx_sy_move_cast_error.
ENDCLASS.

CLASS cl_sales_doc_dispatcher IMPLEMENTATION.
  METHOD dispatch.
    TRY.
        CASE iv_action.
          WHEN 'APPROVE_QUOTE'.
            CAST cl_quotation_handler( io_handler )->approve_quotation(
              iv_approver = sy-uname ).

          WHEN 'CONFIRM_DELIVERY'.
            CAST cl_sales_order_handler( io_handler )->confirm_delivery(
              iv_delivery_date = sy-datum ).

          WHEN OTHERS.
            RAISE EXCEPTION TYPE cx_sales_dispatch_error
              EXPORTING textid = cx_sales_dispatch_error=>unknown_action.
        ENDCASE.

      CATCH cx_sy_move_cast_error INTO DATA(lx_cast).
        log_cast_error( lx_cast ).
        RAISE EXCEPTION TYPE cx_sales_dispatch_error
          EXPORTING previous = lx_cast
                    textid   = cx_sales_dispatch_error=>type_mismatch.
    ENDTRY.
  ENDMETHOD.

  METHOD log_cast_error.
    " BAL_LOG_CREATE 등 응용 로그 API로 기록
    MESSAGE i001(zsd_log) WITH ix_error->get_text( ) INTO DATA(lv_msg).
  ENDMETHOD.
ENDCLASS.

여기서 핵심은 CX_SY_MOVE_CAST_ERROR를 잡아 도메인 예외(CX_SALES_DISPATCH_ERROR)로 변환한다는 점입니다. CAST는 메서드 호출과 한 표현식에 묶이기 때문에 어느 호출에서 실패했는지 스택 트레이스가 더 명확합니다.

3단계: 프로덕션 패턴 — 성능, 테스트, 안전성

대량 처리 루프에서 CAST를 잘못 쓰면 매 반복마다 예외가 발생할 수 있습니다. 이럴 때는 IS INSTANCE OF 가드와 함께 쓰는 것이 권장됩니다.

METHOD bulk_process_documents.
  LOOP AT it_handlers INTO DATA(lo_handler).
    " 가드 절: 타입 확인 후 캐스팅 (예외 비용 회피)
    IF lo_handler IS INSTANCE OF cl_quotation_handler.
      DATA(lo_quote) = CAST cl_quotation_handler( lo_handler ).
      lo_quote->approve_quotation( iv_approver = mv_default_approver ).

    ELSEIF lo_handler IS INSTANCE OF cl_sales_order_handler.
      CAST cl_sales_order_handler( lo_handler )->confirm_delivery(
        iv_delivery_date = mv_target_date ).
    ENDIF.
  ENDLOOP.
ENDMETHOD.

예외 던지기와 잡기는 일반 비교 대비 비용이 큽니다. 1,000건 이상의 루프에서는 IS INSTANCE OF 사전 검사로 예외 발생 자체를 차단하는 편이 안정적입니다.

단위 테스트에서는 ABAP Unit으로 CAST 실패 케이스를 명시적으로 검증합니다.

CLASS ltc_dispatcher DEFINITION FOR TESTING DURATION SHORT
  RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    METHODS cast_mismatch_raises FOR TESTING.
ENDCLASS.

CLASS ltc_dispatcher IMPLEMENTATION.
  METHOD cast_mismatch_raises.
    DATA(lo_wrong) = NEW cl_return_handler( ).  " 반품 핸들러를 주입
    DATA(lo_dispatcher) = NEW cl_sales_doc_dispatcher( ).

    TRY.
        lo_dispatcher->dispatch( io_handler = lo_wrong
                                 iv_action  = 'APPROVE_QUOTE' ).
        cl_abap_unit_assert=>fail( msg = '도메인 예외가 발생해야 함' ).
      CATCH cx_sales_dispatch_error INTO DATA(lx).
        cl_abap_unit_assert=>assert_equals(
          act = lx->textid
          exp = cx_sales_dispatch_error=>type_mismatch ).
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

보안 측면에서는, 외부 입력으로 동적 타입 이름을 받아 CAST (lv_class_name)( ... ) 형태로 사용하는 경우 화이트리스트 검증을 반드시 거쳐야 합니다. 사용자 입력으로 임의 클래스명을 캐스팅 대상으로 받으면 의도하지 않은 클래스 로딩이 발생할 수 있습니다.

삽질 노트

Q. CAST 실패 시 sy-subrc로 분기할 수 없나? 불가능합니다. CAST는 무조건 예외를 던집니다. TRY-CATCH cx_sy_move_cast_error를 쓰거나, 호출 전에 IS INSTANCE OF로 가드해야 합니다. 옛 ?=도 마찬가지로 단락을 일으킵니다.

Q. CAST와 CONV의 차이가 헷갈립니다. CONV는 데이터 타입(예: STRINGI) 변환에 사용되고, CAST는 객체 참조의 정적 타입 변환에 사용됩니다. CONV는 값 변환, CAST는 참조의 시야(view) 변경입니다.

Q. 인터페이스 간 다운캐스트도 가능한가? 가능합니다. 실제 객체가 두 인터페이스를 모두 구현하고 있다면 CAST if_other_aspect( lo_first_aspect ) 형태로 시야를 옮길 수 있습니다.

Q. 팩토리 메서드가 부모 타입을 반환하면 매번 CAST를 써야 하나? 팩토리 메서드를 개선하거나, 호출자 측에서 한 번 CAST하여 지역 변수에 담은 뒤 재사용하는 편이 가독성이 좋습니다. 매 호출마다 캐스팅을 반복하는 것은 의도가 흐려집니다.

옛 코드에서 흔히 보는 안티 패턴은 캐스팅 결과를 검증 없이 쓰는 것입니다. lo_quote ?= lo_base. 뒤에 바로 lo_quote->approve( ).를 호출하면, 타입 불일치 시 단락이 사용자에게 그대로 노출됩니다. CAST + TRY-CATCH 조합으로 도메인 예외로 변환해 의미 있는 메시지를 전달하는 것이 권장됩니다.

더 파볼 주제

  • NEW 인스턴스 생성 연산자와 CAST를 조합한 팩토리 패턴
  • FOR 루프 표현식과 CAST를 함께 사용한 함수형 ABAP
  • RAP 핸들러 클래스에서 BDEF derived type 다루기
  • ABAP Unit Test Double Framework로 다형성 객체 모킹하기
  • ABAP Cleaner로 레거시 ?=를 일괄 CAST로 자동 변환하기

더 읽어볼 자료

  • SAP Help Portal — CAST, Casting Operator (ABAP Keyword Documentation)
  • SAP Help Portal — MOVE, Casting (구문 비교 레퍼런스)
  • SAP Help Portal — CX_SY_MOVE_CAST_ERROR 예외 클래스
  • SAP Clean ABAP Style Guide — Prefer CAST to ?=

댓글 0

아직 댓글이 없습니다.