ABAP

CASE TYPE OF 없이 타입 분기 — 3가지 함정 #shorts #SAP #ABAP

▶ YouTube에서 보기

1. CASE TYPE OF가 필요한 상황과 배경

상속 계층을 설계하다 보면 결국 마주치는 순간이 있습니다. 부모 클래스 타입의 참조 변수 하나에 여러 자식 객체가 담겨 흘러 들어오고, 그중 특정 타입일 때만 다르게 처리해야 하는 상황입니다. ABAP 7.40 이전에는 이럴 때 TRY ... CATCH cx_sy_move_cast_error로 다운캐스트를 감싸거나, 클래스 이름 문자열을 비교하는 우회 코드가 흔했습니다. 둘 다 읽기 어렵고 예외를 제어 흐름으로 남용한다는 문제가 있었습니다.

ABAP 7.50(AS ABAP 7.50 이상, SAP BTP ABAP Environment 및 S/4HANA 전 버전 포함)부터는 CASE TYPE OFIS INSTANCE OF가 도입되어 런타임 타입 체크를 언어 차원에서 지원합니다. 이 글에서는 판매 문서 처리 시나리오를 예로 들어 다음을 다룹니다.

  • 런타임 타입(dynamic type)과 정적 타입(static type)의 차이 이해
  • IS INSTANCE OFCASE TYPE OF의 선택 기준
  • 분기와 다운캐스트를 한 번에 처리하는 안전한 패턴
  • 인터페이스 구현체 판별과 설계상 주의점

2. 미리 알아두면 좋은 것

ABAP OO의 기본기, 즉 클래스 정의(CLASS ... DEFINITION), 상속(INHERITING FROM), 인터페이스 구현(INTERFACES), 그리고 업캐스트/다운캐스트 개념을 알고 있다면 충분합니다. 참조 변수의 정적 타입과 실제 가리키는 객체의 타입이 다를 수 있다는 점만 확실히 잡고 시작하면 됩니다. 인라인 선언(DATA(...), FINAL(...)) 문법도 예제에 등장합니다.

3. 환경과 버전 요구사항

이 글의 문법은 다음 환경에서 동작합니다.

  • AS ABAP 7.50 이상CASE TYPE OF, IS INSTANCE OF가 이 릴리스에서 도입되었습니다.
  • SAP S/4HANA (온프레미스 1511 이후 전 버전) — 커널이 7.50 이상이므로 모두 사용 가능합니다.
  • SAP BTP ABAP Environment(Steampunk) — ABAP Cloud 언어 버전에서도 그대로 지원됩니다.

개발 도구는 ABAP Development Tools(ADT) for Eclipse를 권장합니다. 예제는 클래스 라이브러리만 사용하므로 별도 트랜스포트 외 준비물은 없습니다. NetWeaver 7.40 이하 시스템이라면 이 문법은 구문 오류가 나므로, 후반부에 언급하는 예전 방식(TRY ... CATCH 캐스트)을 사용해야 합니다.

4. 상속 계층과 런타임 타입 — 동작 원리

참조 변수에는 두 가지 타입이 공존합니다. 선언 시점에 고정되는 정적 타입과, 실행 중 실제로 가리키는 객체의 동적 타입(런타임 타입)입니다. 우편함에 비유하면, 우편함 라벨에는 "문서"라고만 적혀 있지만(정적 타입) 실제로 꺼내 보면 청구서일 수도, 배송 통지서일 수도 있는 것(동적 타입)과 같습니다.

DATA lo_doc TYPE REF TO zcl_sales_document.   " 정적 타입: zcl_sales_document
lo_doc = NEW zcl_invoice( ).                  " 동적 타입: zcl_invoice

컴파일러는 정적 타입 기준으로만 멤버 접근을 허용하므로, zcl_invoice에만 있는 메서드를 호출하려면 다운캐스트가 필요합니다. 여기서 런타임 타입 체크가 등장합니다.

  • obj IS INSTANCE OF class — 객체의 동적 타입이 해당 클래스이거나 그 하위 클래스이면 참을 반환하는 관계식입니다. IF, COND, xsdbool( ) 어디서든 쓸 수 있습니다.
  • CASE TYPE OF obj — 여러 타입 후보를 위에서 아래로 검사하며, 처음 매칭되는 WHEN TYPE 분기를 실행하는 제어 구조입니다.

중요한 규칙 하나. 매칭은 "정확히 그 클래스"가 아니라 "그 클래스에 대입 가능한가"를 봅니다. 즉 상위 클래스 분기가 하위 클래스 객체도 잡아냅니다. 그래서 CASE TYPE OF에서는 구체적인(하위) 타입을 먼저, 일반적인(상위) 타입을 나중에 나열해야 합니다. 이 순서를 어기면 하위 타입 분기는 절대 실행되지 않는 죽은 코드가 됩니다.

참조가 초기 상태(INITIAL)인 경우도 정의되어 있습니다. IS INSTANCE OF는 널 참조에 대해 항상 거짓이고, CASE TYPE OF는 어떤 WHEN TYPE과도 매칭되지 않아 WHEN OTHERS로 떨어집니다. 널 가드를 따로 두지 않아도 덤프가 나지 않는다는 뜻입니다.

5. 실전 코드 3단계

1단계 — 기본: 판매 문서 계층과 타입 분기

판매 문서 상속 계층을 만들고 CASE TYPE OF로 분기해 봅니다. WHEN TYPE ... INTO가 핵심인데, 타입 검사와 다운캐스트를 한 문장으로 끝내 줍니다.

CLASS zcl_sales_document DEFINITION.
  PUBLIC SECTION.
    METHODS get_doc_number RETURNING VALUE(rv_no) TYPE vbeln.
ENDCLASS.

CLASS zcl_invoice DEFINITION INHERITING FROM zcl_sales_document.
  PUBLIC SECTION.
    METHODS get_due_date RETURNING VALUE(rv_date) TYPE d.
ENDCLASS.

CLASS zcl_delivery_note DEFINITION INHERITING FROM zcl_sales_document.
  PUBLIC SECTION.
    METHODS get_carrier RETURNING VALUE(rv_carrier) TYPE string.
ENDCLASS.

" 호출부 --------------------------------------------------
DATA(lo_doc) = zcl_doc_factory=>create( iv_doc_key ).  " TYPE REF TO zcl_sales_document

CASE TYPE OF lo_doc.
  WHEN TYPE zcl_invoice INTO DATA(lo_invoice).
    " lo_invoice는 이미 zcl_invoice 타입 — 캐스트 불필요
    out->write( |청구서 만기일: { lo_invoice->get_due_date( ) DATE = USER }| ).
  WHEN TYPE zcl_delivery_note INTO DATA(lo_delivery).
    out->write( |운송사: { lo_delivery->get_carrier( ) }| ).
  WHEN OTHERS.
    out->write( |일반 문서: { lo_doc->get_doc_number( ) }| ).
ENDCASE.

단일 타입만 확인할 때는 IF 쪽이 간결합니다. IS INSTANCE OF 역시 INTO는 없지만, 조건이 참인 블록 안에서 CAST를 쓰면 안전합니다.

IF lo_doc IS INSTANCE OF zcl_invoice.
  DATA(lo_inv) = CAST zcl_invoice( lo_doc ).   " 검사 후 캐스트라 안전
  lo_inv->get_due_date( ).
ENDIF.

2단계 — 실무: 검사 없는 다운캐스트의 예외 처리와 로깅

레거시 코드나 외부 API에서 넘어온 참조를 다룰 때는 예외 방어와 로그가 필요합니다. 검사 없이 CAST를 시도하면 cx_sy_move_cast_error가 발생하므로, 타입 체크가 불가능한 구간에서는 예외를 잡아 애플리케이션 로그로 남기는 패턴이 일반적입니다.

METHOD process_incoming_ref.
  " io_ref TYPE REF TO object — 외부에서 받은 미상의 참조
  CASE TYPE OF io_ref.
    WHEN TYPE zcl_invoice INTO DATA(lo_invoice).
      post_to_fi( lo_invoice ).

    WHEN TYPE zcl_sales_document INTO DATA(lo_generic).
      " 하위 분기에 안 걸린 나머지 판매 문서 전부
      mo_logger->add_info( |표준 처리: { lo_generic->get_doc_number( ) }| ).
      process_default( lo_generic ).

    WHEN OTHERS.
      " 널 참조 포함 — 덤프 대신 로그로 흡수
      mo_logger->add_warning(
        |처리 불가 타입: { COND #( WHEN io_ref IS BOUND
                                   THEN cl_abap_classdescr=>get_class_name( io_ref )
                                   ELSE `INITIAL REFERENCE` ) }| ).
      RAISE EXCEPTION NEW zcx_unsupported_document( ).
  ENDCASE.
ENDMETHOD.

포인트는 두 가지입니다. 첫째, WHEN OTHERS에서 무엇이 들어왔는지 RTTI(cl_abap_classdescr)로 실제 클래스명을 로깅해 두면 운영 장애 분석이 훨씬 빨라집니다. 둘째, 상위 타입 분기(zcl_sales_document)를 하위 타입 분기(zcl_invoice) 뒤에 두어 폴백 역할을 시켰습니다.

3단계 — 프로덕션: 인터페이스 판별과 테스트 가능한 설계

실제 프로젝트에서는 클래스보다 인터페이스로 계약을 정의하는 경우가 많습니다. WHEN TYPE에는 인터페이스도 올 수 있어, "이 객체가 특정 능력(capability)을 구현했는가"를 판별하는 데 쓸 수 있습니다.

INTERFACE zif_archivable.
  METHODS archive IMPORTING iv_retention_days TYPE i.
ENDINTERFACE.

INTERFACE zif_printable.
  METHODS render_pdf RETURNING VALUE(rv_pdf) TYPE xstring.
ENDINTERFACE.

METHOD finalize_document.
  " 능력 기반 분기 — 구체 클래스명을 몰라도 됨
  CASE TYPE OF io_doc.
    WHEN TYPE zif_archivable INTO DATA(li_archivable).
      li_archivable->archive( iv_retention_days = 3650 ).
    WHEN TYPE zif_printable INTO DATA(li_printable).
      send_to_spool( li_printable->render_pdf( ) ).
    WHEN OTHERS.
      " 아무 능력도 없는 문서는 그대로 종료
  ENDCASE.
ENDMETHOD.

주의할 점은 한 객체가 두 인터페이스를 모두 구현하면 먼저 선언된 WHEN TYPE 하나만 실행된다는 것입니다. 두 처리가 모두 필요하면 CASE가 아니라 독립된 IF ... IS INSTANCE OF 블록 두 개로 풀어야 합니다. 단위 테스트에서는 각 분기에 해당하는 테스트 더블을 주입해 분기별로 검증합니다.

METHOD branch_selects_archivable FOR TESTING.
  DATA(lo_double) = NEW ltd_archivable_doc( ).   " zif_archivable 구현 더블
  mo_cut->finalize_document( lo_double ).
  cl_abap_unit_assert=>assert_true( lo_double->archive_called ).
ENDMETHOD.

보안 관점에서는, 동적 타입에 따라 권한이 달라지는 문서(예: 인사 관련 문서)라면 타입 분기 직후 해당 분기 안에서 권한 검사를 수행하는 편이 안전합니다. 분기 이전의 공통 검사만으로는 하위 타입 고유 데이터 접근을 통제하지 못하기 때문입니다.

6. 흔한 실수와 트러블슈팅 FAQ

Q1. 하위 클래스 분기가 전혀 실행되지 않습니다.

거의 항상 분기 순서 문제입니다. WHEN TYPE zcl_sales_documentWHEN TYPE zcl_invoice보다 위에 있으면 청구서 객체도 상위 분기에 흡수됩니다. ADT 구문 검사는 이를 잡아 주지 않으므로, 항상 가장 구체적인 타입부터 나열하는 습관이 필요합니다.

Q2. 널 참조를 넣었는데 덤프가 나지 않고 조용히 지나갑니다. 버그인가요?

의도된 동작입니다. 초기 참조는 모든 WHEN TYPE에 불일치 처리되어 WHEN OTHERS로 갑니다. 널이 비정상 상황이라면 WHEN OTHERS에서 IS BOUND를 검사해 명시적으로 예외를 던지는 것이 좋습니다.

Q3. CASE TYPE OF를 쓰면 다형성을 포기하는 것 아닌가요?

절반은 맞는 지적입니다. 계층 내 모든 타입이 같은 동작의 다른 구현을 가진다면 재정의된 메서드 호출(진짜 다형성)이 정답이고, 타입 분기는 코드 냄새입니다. 반면 특정 타입에만 존재하는 부가 동작을 외부(예: UI 어댑터, 직렬화기)에서 처리해야 하거나 계층 클래스를 수정할 수 없을 때는 CASE TYPE OF가 정당한 도구입니다. "분기 로직이 계층 안으로 들어갈 수 있는가"를 먼저 자문해 보길 권장합니다.

Q4. 7.40 시스템에서 구문 오류가 납니다.

두 문법 모두 7.50부터입니다. 하위 릴리스에서는 TRY. lo_inv ?= lo_doc. CATCH cx_sy_move_cast_error. ENDTRY. 패턴이 유일한 대안입니다.

7. 이어서 볼 만한 주제

런타임 타입 체크를 익혔다면 자연스럽게 이어지는 주제는 세 가지입니다. 첫째, RTTI/RTTC(cl_abap_typedescr 계열) — 클래스명 조회를 넘어 동적 타입 생성까지 다루는 저수준 도구입니다. 둘째, CAST와 구성 연산자(NEW, COND, SWITCH) 조합으로 표현식 위치에서 타입을 다루는 기법. 셋째, 타입 분기를 아예 없애는 방문자(Visitor) 패턴과 팩토리 설계 — 분기 로직이 반복적으로 늘어난다면 구조 리팩터링 신호이므로 함께 검토해 볼 가치가 있습니다.

8. 더 읽을거리

댓글 0

아직 댓글이 없습니다.