개요 및 이 글에서 다루는 것
ABAP 객체지향에서 상위 클래스 참조를 하위 클래스 참조로 변환하는 다운캐스팅(Downcast)은 다형성을 활용하는 코드에서 반드시 마주치는 작업입니다. 전통적으로 사용해 온 MOVE ?= 또는 ?= 연산자는 한 줄에 변수 선언을 함께 쓰기 어렵고, 예외 처리 흐름이 분산되어 가독성이 떨어집니다. ABAP 7.40 SP08부터 도입된 CAST 표현식은 인라인 선언과 결합하여 더 안전하고 짧은 코드를 가능하게 합니다. 이 글에서는 두 방식의 차이를 비교하고, 실무에서 안전하게 다운캐스팅하는 패턴을 단계별로 살펴봅니다.
- 업캐스팅과 다운캐스팅의 차이 이해
MOVE ?=의 한계점과CAST표현식의 장점 비교CX_SY_MOVE_CAST_ERROR예외 처리 패턴IS INSTANCE OF와 결합한 방어적 코딩 작성- 실무 시나리오: 판매문서 계층 구조에서 특수 문서 다운캐스트
이 글을 읽기 전에 알아두면 좋은 것
ABAP 객체지향의 기본 개념(클래스, 인스턴스, 상속), 참조 변수(REF TO)의 동작 방식, 예외 클래스(CX_*) 처리 구조(TRY/CATCH)에 대한 기본 이해가 필요합니다. 또한 ABAP 7.40 이상에서 추가된 인라인 선언(DATA(...)) 문법에 익숙하면 더 좋습니다.
환경 및 준비물
이 글의 코드는 다음 환경을 기준으로 작성되었습니다.
- ABAP 7.40 SP08 이상 (CAST 표현식 지원 시작 버전)
- 권장: ABAP 7.50 / 7.54 이상 또는 ABAP Platform 2022 이상 (안정적인 표현식 지원)
- SAP NetWeaver AS ABAP 또는 SAP S/4HANA on-premise / Cloud(ABAP Cloud)
- ABAP Development Tools (ADT) for Eclipse 또는 SE80 (클래식)
- 최소한 두 단계 상속 구조를 가진 클래스 정의 권한
ABAP Cloud(Steampunk, RAP) 환경에서도 CAST는 일반적으로 권장 문법이며, ?=는 레거시 코드에서 주로 발견됩니다. ABAP Cloud의 strict syntax check 모드에서는 표현식 기반 문법이 더 잘 어울립니다.
핵심 개념
객체 참조 변수 간의 대입은 두 가지 방향이 있습니다.
- 업캐스팅(Upcast / Widening Cast): 하위 클래스 참조를 상위 클래스 참조에 대입합니다. 컴파일러가 정적으로 검증할 수 있으므로 일반 대입 연산자
=로 충분합니다. - 다운캐스팅(Downcast / Narrowing Cast): 상위 클래스 참조를 하위 클래스 참조에 대입합니다. 실제 인스턴스가 하위 타입이 아닐 수 있으므로 런타임 검사가 필요하고, 실패하면
CX_SY_MOVE_CAST_ERROR예외가 발생합니다.
비유하자면, 상위 클래스 참조는 "포장된 택배 상자"입니다. 겉면 라벨만 보면 어떤 종류의 상자인지(부모 타입)는 알 수 있지만, 안에 들어 있는 실제 물건(자식 타입)이 무엇인지는 열어봐야 합니다. 다운캐스팅은 "이 상자 안에 든 물건이 특정 모델일 것"이라고 선언하는 행위이며, 잘못 추측하면 런타임 오류가 발생합니다.
전통 문법 lo_child ?= lo_parent.은 다음 단점이 있습니다.
- 대입 대상 변수를 미리 선언해야 하므로 코드가 길어짐
- 표현식 안에 중첩 사용 불가 — 메서드 인자로 바로 넣을 수 없음
- 한 줄의 의도가 "타입 변환"인지 "값 대입"인지 시각적으로 모호
반면 CAST 표현식은 다음과 같이 사용합니다.
DATA(lo_special) = CAST zcl_special_sales_doc( lo_sales_doc ).
이 형식은 다음 이점을 제공합니다.
- 인라인 선언(
DATA(...))과 결합되어 변수 선언과 캐스팅을 한 줄에 처리 - 메서드 인자, 체이닝 호출 등 표현식이 허용되는 모든 위치에서 사용 가능
- 의도("이 참조를 특정 타입으로 다룬다")가 명확
두 방식 모두 실패 시 동일한 CX_SY_MOVE_CAST_ERROR를 던집니다. 따라서 안전성 자체가 더 높아지는 것은 아니지만, 코드 구조가 단순해져 TRY/CATCH 블록을 더 좁고 명확하게 작성할 수 있고 결과적으로 안전한 코드를 쓰기 쉬워집니다.
실전 코드 1단계 — 기본 예제
가장 단순한 형태로, 판매문서(zcl_sales_document) 계층에 특수 판매문서(zcl_special_sales_document)가 상속받는 구조를 가정합니다.
CLASS zcl_sales_document DEFINITION PUBLIC CREATE PUBLIC.
PUBLIC SECTION.
METHODS get_doc_id RETURNING VALUE(rv_id) TYPE string.
DATA mv_doc_id TYPE string.
ENDCLASS.
CLASS zcl_sales_document IMPLEMENTATION.
METHOD get_doc_id.
rv_id = mv_doc_id.
ENDMETHOD.
ENDCLASS.
CLASS zcl_special_sales_document DEFINITION
PUBLIC INHERITING FROM zcl_sales_document CREATE PUBLIC.
PUBLIC SECTION.
METHODS apply_special_discount.
ENDCLASS.
CLASS zcl_special_sales_document IMPLEMENTATION.
METHOD apply_special_discount.
" 특수 할인 로직
ENDMETHOD.
ENDCLASS.
이제 상위 타입 참조에서 하위 타입 메서드를 호출해야 한다고 가정합니다. 전통 방식과 CAST 방식을 비교합니다.
" 전통 방식: MOVE ?= (legacy)
DATA: lo_doc TYPE REF TO zcl_sales_document,
lo_special TYPE REF TO zcl_special_sales_document.
lo_doc = NEW zcl_special_sales_document( ).
lo_special ?= lo_doc.
lo_special->apply_special_discount( ).
" 모던 방식: CAST 표현식
DATA(lo_doc2) = CAST zcl_sales_document( NEW zcl_special_sales_document( ) ).
CAST zcl_special_sales_document( lo_doc2 )->apply_special_discount( ).
두 번째 방식은 임시 변수가 필요 없고, 메서드 호출 체인에 바로 끼워 넣을 수 있습니다.
실전 코드 2단계 — 예외 처리와 로깅
실제 데이터는 항상 예상한 타입이 아닐 수 있습니다. 예를 들어 판매문서 컬렉션을 순회하면서 특수 문서만 골라 처리해야 한다면, 다운캐스팅 실패를 안전하게 처리해야 합니다.
METHOD process_special_docs.
DATA(lt_docs) = get_all_sales_documents( ).
LOOP AT lt_docs INTO DATA(lo_doc).
TRY.
DATA(lo_special) = CAST zcl_special_sales_document( lo_doc ).
lo_special->apply_special_discount( ).
" 처리 성공 로깅
log_info( |Special discount applied: { lo_doc->get_doc_id( ) }| ).
CATCH cx_sy_move_cast_error INTO DATA(lx_cast).
" 일반 문서는 스킵
log_debug( |Not a special document, skipped: { lo_doc->get_doc_id( ) }| ).
ENDTRY.
ENDLOOP.
ENDMETHOD.
그러나 이 패턴은 "타입이 아니면 예외 발생 → catch"라는 흐름이 비효율적입니다. ABAP에서 예외 처리는 정상 흐름보다 비용이 크고, 의도("타입을 확인하고 분기")가 흐려집니다. 따라서 IS INSTANCE OF와 결합한 방어적 코딩이 권장됩니다.
METHOD process_special_docs_v2.
DATA(lt_docs) = get_all_sales_documents( ).
LOOP AT lt_docs INTO DATA(lo_doc).
IF lo_doc IS INSTANCE OF zcl_special_sales_document.
DATA(lo_special) = CAST zcl_special_sales_document( lo_doc ).
lo_special->apply_special_discount( ).
log_info( |Special discount applied: { lo_doc->get_doc_id( ) }| ).
ELSE.
log_debug( |Standard document, no special handling: { lo_doc->get_doc_id( ) }| ).
ENDIF.
ENDLOOP.
ENDMETHOD.
IS INSTANCE OF는 런타임에 참조가 가리키는 객체가 특정 클래스 또는 그 하위 클래스의 인스턴스인지 검사합니다. 캐스팅 전에 호출하면 CX_SY_MOVE_CAST_ERROR가 발생할 가능성을 사전에 차단할 수 있습니다.
실전 코드 3단계 — 프로덕션 레벨 패턴
실무에서는 외부에서 들어온 참조, 팩토리에서 반환된 객체, RAP 핸들러 클래스 등 다양한 상황에서 다운캐스팅이 필요합니다. 안전성, 테스트 용이성, 로깅을 모두 고려한 프로덕션 패턴을 살펴봅니다.
CLASS zcl_sales_doc_processor DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS process
IMPORTING io_doc TYPE REF TO zcl_sales_document
RAISING zcx_processing_error.
PRIVATE SECTION.
METHODS handle_special
IMPORTING io_special TYPE REF TO zcl_special_sales_document.
METHODS handle_standard
IMPORTING io_standard TYPE REF TO zcl_sales_document.
ENDCLASS.
CLASS zcl_sales_doc_processor IMPLEMENTATION.
METHOD process.
" 1) 입력 유효성 검사
IF io_doc IS NOT BOUND.
RAISE EXCEPTION TYPE zcx_processing_error
EXPORTING textid = zcx_processing_error=>invalid_input.
ENDIF.
" 2) 타입 분기 — IS INSTANCE OF로 사전 체크
IF io_doc IS INSTANCE OF zcl_special_sales_document.
handle_special( CAST #( io_doc ) ).
ELSE.
handle_standard( io_doc ).
ENDIF.
ENDMETHOD.
METHOD handle_special.
TRY.
io_special->apply_special_discount( ).
CATCH cx_root INTO DATA(lx_err).
" 도메인 예외로 변환
RAISE EXCEPTION TYPE zcx_processing_error
EXPORTING previous = lx_err.
ENDTRY.
ENDMETHOD.
METHOD handle_standard.
" 기본 문서 처리
ENDMETHOD.
ENDCLASS.
이 패턴의 특징입니다.
CAST #( io_doc )는 컨텍스트(메서드 인자 타입)로부터 대상 타입을 추론합니다.handle_special의 인자 타입이zcl_special_sales_document이므로 별도 타입 명시가 불필요합니다.IS NOT BOUND로 null 참조 검사를 먼저 수행하여 NPE류 오류를 방지합니다.- 도메인 예외(
zcx_processing_error)로 래핑하여 호출 측이 SAP 시스템 예외를 직접 다루지 않게 합니다. - 단위 테스트에서는 자식 클래스의 가짜 인스턴스를 만들어 분기 동작을 검증할 수 있습니다.
성능 관점에서 IS INSTANCE OF와 CAST의 조합은 try/catch보다 일반적으로 가볍습니다. 대량 루프에서 캐스팅이 자주 실패할 가능성이 있다면 사전 체크 패턴을 선택하는 것이 좋습니다.
흔한 실수와 트러블슈팅
Q1. CAST를 쓰면 캐스팅 실패 예외가 자동으로 처리되나요?
아니요. CAST는 문법적 간결성을 제공할 뿐, 실패 시 CX_SY_MOVE_CAST_ERROR는 동일하게 발생합니다. TRY/CATCH 또는 IS INSTANCE OF로 감싸야 안전합니다. 인라인 DATA(...)로 받았더라도 예외는 그대로 전파됩니다.
Q2. 인터페이스 참조도 CAST로 변환할 수 있나요?
가능합니다. 클래스 → 인터페이스, 인터페이스 → 구현 클래스, 인터페이스 → 다른 인터페이스 변환 모두 CAST로 작성합니다. 단, IS INSTANCE OF는 클래스 타입에 대해서만 사용 가능하며, 인터페이스 구현 여부 확인에도 동일 문법으로 작성할 수 있습니다(예: lo_ref IS INSTANCE OF zif_billable).
Q3. CAST #( ... )의 #은 무슨 의미인가요?
#은 컨텍스트로부터 타입을 추론하라는 기호입니다. 메서드 인자, 반환 타입, 좌변 변수 타입 등 컴파일러가 대상 타입을 알 수 있는 위치에서만 사용 가능합니다. 인라인 DATA(...)의 우변에서는 추론할 컨텍스트가 없으므로 명시적 타입(CAST zcl_xxx( ... ))을 적어야 합니다.
Q4. ?=는 이제 쓰면 안 되나요?
기존 코드의 ?=는 정상 동작하므로 무리하게 모두 교체할 필요는 없습니다. 다만 신규 코드에서는 CAST 사용이 권장되며, ABAP Cloud 환경의 코드 스타일 가이드도 표현식 기반 문법을 선호하는 경향입니다.
Q5. 다운캐스트가 자주 필요하다면 설계가 잘못된 것 아닌가요?
일반적으로 그렇습니다. 빈번한 다운캐스팅은 다형성을 충분히 활용하지 못한다는 신호일 수 있습니다. 추상 메서드를 상위 클래스/인터페이스에 정의해 가상 호출로 대체하거나, Strategy/Visitor 패턴 적용을 검토해 보세요.
더 깊이 살펴볼 주제
이 글에서 다룬 CAST 표현식은 ABAP 7.40 이후 도입된 표현식 기반 문법 중 일부입니다. 더 깊이 학습하려면 다음 주제로 확장해 보길 권장합니다.
CONV표현식 — 기본 타입 간의 변환을 동일한 스타일로 처리NEW표현식 — 인라인 인스턴스 생성과 결합VALUE표현식 — 구조체/내부 테이블 인라인 생성- 인터페이스 기반 다형성과 ABAP Object Services
- RAP(RESTful ABAP Programming Model)에서 핸들러 클래스의 다운캐스트 패턴
- ABAP Unit으로 다운캐스트 분기 로직 테스트하기
댓글 0
아직 댓글이 없습니다.