ABAP

아직도 sy-subrc? Exception Class 3단계 #shorts #SAP #ABAP

개요 및 이 글에서 다루는 것

ABAP 개발자라면 CALL FUNCTION 뒤에 무의식적으로 IF sy-subrc <> 0를 붙이는 습관이 있을 것이다. 하지만 sy-subrc = 4가 의미하는 바가 "레코드 없음"인지 "권한 오류"인지 "락 실패"인지는 매뉴얼을 뒤져야 알 수 있다. 이 글에서는 절차적 에러 코드 방식의 한계를 짚고, 객체 지향 예외 처리(Exception Class)로 전환하는 3단계 패턴을 PurchaseOrder(구매 오더) 처리 시나리오로 실습한다.

  • sy-subrc 기반 처리의 구조적 한계와 유지보수 비용 이해
  • CX_ROOT 계층(CX_STATIC_CHECK / CX_DYNAMIC_CHECK / CX_NO_CHECK) 구분
  • RAISE EXCEPTION TYPE / TRY-CATCH-CLEANUP-ENDTRY 문법 숙달
  • GET_TEXT(), previous 체이닝을 이용한 의미 있는 오류 전달
  • 실무 프로덕션 코드에서 로깅 및 트랜잭션 롤백 안전 처리

미리 알고 있으면 좋은 개념

ABAP OO 기초(CLASS/METHOD 정의, INTERFACE), 전역 클래스와 로컬 클래스 차이, MESSAGE 문 기본 사용법을 이해하고 있어야 한다. SE24 또는 ADT(ABAP Development Tools in Eclipse)에서 클래스 편집이 가능해야 하며, SE80/SE38에서 리포트를 실행할 수 있어야 한다. RAP(RESTful ABAP Programming) 경험이 있다면 예외를 EML 결과 매핑까지 연결하는 실전 감각을 얻을 수 있다.

실습 환경과 준비물

이 글의 예제는 SAP NetWeaver AS ABAP 7.52 이상 또는 SAP S/4HANA 2021 이상(ABAP Platform 2021)에서 검증되었다. CX_STATIC_CHECK 기반 문법은 ABAP 6.10부터 지원되므로 대부분의 운영 시스템에서 동작하지만, 최신 문법(MESSAGE ... INTO DATA(...), 인라인 선언)은 7.40 SP08 이상에서 권장된다.

  • 개발 도구: ADT(Eclipse) 최신 버전 또는 SE24/SE38
  • 테스트 데이터: EKKO/EKPO 헤더 및 아이템 테이블(구매 오더)
  • 권한: S_DEVELOP(개발), S_TCODE(SE24, SE38)
  • 선택: ST22(덤프 분석), SLG1(애플리케이션 로그) 접근 권한

핵심 개념 — 왜 sy-subrc를 넘어서야 하는가

sy-subrc는 마지막 명령의 결과 코드를 담는 시스템 필드다. 이 방식은 "성공/실패의 이진 판단"에는 편리하지만, 다음과 같은 구조적 한계가 있다.

  • 의미의 손실: sy-subrc = 4가 "락 획득 실패"인지 "권한 없음"인지 호출된 명령에 따라 달라진다. 컨텍스트를 잃는다.
  • 전파 불가: 하위 서브루틴에서 발생한 오류를 상위 호출자에게 전달하려면 매번 EXPORTING 파라미터로 코드를 넘겨야 한다.
  • 디버깅 어려움: 스택 정보가 없다. 어디서 왜 실패했는지 별도 로그를 심어야 한다.
  • 즉시 확인 강제: 다음 명령이 실행되면 sy-subrc는 덮어써진다. 검사 타이밍을 놓치면 사라진다.

Exception Class는 이 문제를 객체로 해결한다. 오류가 발생하면 예외 객체가 생성되어 콜 스택을 거슬러 올라가며, 처리자(CATCH)를 만날 때까지 자동으로 전파된다. 클래스이므로 속성(어떤 오더 번호에서 실패했는지)과 메서드(GET_TEXT로 메시지 반환)를 가질 수 있다.

ABAP의 모든 예외 클래스는 CX_ROOT를 최상위로 하는 3개 계통으로 나뉜다.

  • CX_STATIC_CHECK: 컴파일러가 시그니처(RAISING)에 선언되었는지 검사한다. 예측 가능한 비즈니스 오류에 적합(예: 승인되지 않은 오더).
  • CX_DYNAMIC_CHECK: 런타임에만 검사. 조건에 따라 발생 여부가 갈리는 상황(예: 0으로 나누기)에 쓴다.
  • CX_NO_CHECK: 시그니처 선언 불필요. 언제든 발생 가능한 시스템 오류(메모리 부족 등)에 사용.
비유하자면 sy-subrc는 "회사 로비 전광판에 마지막 방문객 번호만 표시하는 방식"이다. 다음 사람이 오면 사라진다. Exception Class는 "방문객에게 명찰(객체)을 걸어주고 담당 부서(CATCH)까지 안내하는 방식"이다.

실전 코드 3단계

1단계 — 기본 예제: 예외 클래스 정의와 발생

먼저 sy-subrc 방식과 Exception Class 방식을 나란히 비교한다. PurchaseOrder를 조회하는 상황을 가정한다.

" [Before] sy-subrc 방식 - 의미가 숫자에 갇힘
DATA: lv_po_id   TYPE ekko-ebeln VALUE '4500001234',
      ls_po_hdr  TYPE ekko.

SELECT SINGLE * FROM ekko INTO ls_po_hdr WHERE ebeln = lv_po_id.
IF sy-subrc <> 0.
  " 4가 나왔는데, 없는 건지 권한이 없는 건지 모른다
  WRITE: / 'Error occurred, sy-subrc =', sy-subrc.
  EXIT.
ENDIF.

이제 의미 있는 예외 클래스를 정의한다. SE24 또는 ADT에서 ZCX_PURCHASE_ORDER라는 전역 클래스를 CX_STATIC_CHECK 상속으로 생성한다.

CLASS zcx_purchase_order DEFINITION
  PUBLIC
  INHERITING FROM cx_static_check
  FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    CONSTANTS:
      BEGIN OF not_found,
        msgid TYPE symsgid  VALUE 'ZPO',
        msgno TYPE symsgno  VALUE '001',
        attr1 TYPE scx_attrname VALUE 'PO_ID',
        attr2 TYPE scx_attrname VALUE '',
      END OF not_found.

    DATA po_id TYPE ekko-ebeln READ-ONLY.

    METHODS constructor
      IMPORTING
        textid   LIKE if_t100_message=>t100key OPTIONAL
        previous LIKE previous                 OPTIONAL
        po_id    TYPE ekko-ebeln               OPTIONAL.
ENDCLASS.

CLASS zcx_purchase_order IMPLEMENTATION.
  METHOD constructor.
    super->constructor( previous = previous ).
    me->po_id = po_id.
    CLEAR me->textid.
    IF textid IS INITIAL.
      if_t100_message~t100key = not_found.
    ELSE.
      if_t100_message~t100key = textid.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

발생시키는 쪽은 다음과 같다.

METHODS get_po_header
  IMPORTING iv_po_id       TYPE ekko-ebeln
  RETURNING VALUE(rs_hdr)  TYPE ekko
  RAISING   zcx_purchase_order.

METHOD get_po_header.
  SELECT SINGLE * FROM ekko INTO rs_hdr WHERE ebeln = iv_po_id.
  IF sy-subrc <> 0.
    RAISE EXCEPTION TYPE zcx_purchase_order
      EXPORTING
        textid = zcx_purchase_order=>not_found
        po_id  = iv_po_id.
  ENDIF.
ENDMETHOD.

2단계 — 실무 시나리오: TRY-CATCH와 로깅, 계층적 예외

구매 오더 처리는 헤더 조회, 아이템 검증, 승인 확인 등 여러 단계를 거친다. 각 단계마다 별도 예외를 두고, 상위에서 계층적으로 처리한다.

" 별도 클래스: ZCX_PO_UNAUTHORIZED, ZCX_PO_ITEM_INVALID 도 유사하게 정의
" 두 클래스 모두 CX_STATIC_CHECK 상속

CLASS lcl_po_service DEFINITION.
  PUBLIC SECTION.
    METHODS process_po
      IMPORTING iv_po_id TYPE ekko-ebeln
      RAISING   zcx_purchase_order
                zcx_po_unauthorized
                zcx_po_item_invalid.
  PRIVATE SECTION.
    METHODS check_authority
      IMPORTING iv_po_id TYPE ekko-ebeln
      RAISING   zcx_po_unauthorized.
    METHODS validate_items
      IMPORTING iv_po_id TYPE ekko-ebeln
      RAISING   zcx_po_item_invalid.
ENDCLASS.

CLASS lcl_po_service IMPLEMENTATION.
  METHOD process_po.
    DATA(ls_hdr) = get_po_header( iv_po_id ).  " zcx_purchase_order
    check_authority( iv_po_id ).               " zcx_po_unauthorized
    validate_items( iv_po_id ).                " zcx_po_item_invalid
    " ... 이후 후속 처리
  ENDMETHOD.
  " check_authority, validate_items 구현 생략
ENDCLASS.

" 호출자: 계층별로 다르게 반응
START-OF-SELECTION.
  DATA(lo_svc) = NEW lcl_po_service( ).
  TRY.
      lo_svc->process_po( '4500001234' ).
      MESSAGE 'PO processed' TYPE 'S'.

    CATCH zcx_po_unauthorized INTO DATA(lx_auth).
      " 권한 오류는 감사 로그에 기록
      cl_bali_log=>get_instance( )->add_free_text(
        i_text = lx_auth->get_text( ) ).
      MESSAGE lx_auth->get_text( ) TYPE 'E'.

    CATCH zcx_po_item_invalid INTO DATA(lx_item).
      MESSAGE lx_item->get_text( ) TYPE 'W'.

    CATCH zcx_purchase_order INTO DATA(lx_po).
      " 부모격(공통) 예외를 마지막에
      MESSAGE lx_po->get_text( ) TYPE 'E'.
  ENDTRY.

중요한 원칙: CATCH 순서는 구체적인 것부터 일반적인 것으로 작성해야 한다. 반대로 하면 상위 클래스가 하위를 삼켜버려 세부 처리가 실행되지 않는다.

3단계 — 프로덕션: CLEANUP, 예외 체이닝, 재시도 정책

운영 코드에서는 리소스 정리(락 해제, 커서 닫기)와 원인 예외 보존이 중요하다. CLEANUP 블록과 previous 파라미터를 활용한다.

METHOD post_po_to_backend.
  DATA lo_lock TYPE REF TO if_abap_lock_object.

  TRY.
      " 1. 락 획득
      cl_abap_lock_object_factory=>get_instance(
        EXPORTING iv_name = 'EZ_EKKO'
      )->enqueue( it_parameter = VALUE #( ( name = 'EBELN' value = REF #( iv_po_id ) ) ) ).

      " 2. 백엔드 호출 (외부 시스템 통신)
      DATA(lo_client) = cl_http_client=>create_by_destination( 'ZERP_POST_PO' ).
      lo_client->send( ).
      lo_client->receive( ).

      DATA(lv_status) = lo_client->response->get_header_field( '~status_code' ).
      IF lv_status <> '200'.
        RAISE EXCEPTION TYPE zcx_po_backend_error
          EXPORTING
            textid = zcx_po_backend_error=>http_failed
            po_id  = iv_po_id
            status = lv_status.
      ENDIF.

    CATCH cx_http_dest_provider_error
          cx_http_client_error INTO DATA(lx_http).
      " 3. 저수준 통신 오류를 도메인 예외로 감싸기 (체이닝)
      RAISE EXCEPTION TYPE zcx_po_backend_error
        EXPORTING
          textid   = zcx_po_backend_error=>connection_lost
          po_id    = iv_po_id
          previous = lx_http.                " 원인 예외 보존

    CLEANUP.
      " 4. 어떤 예외든 락은 반드시 해제
      IF lo_lock IS BOUND.
        TRY.
            lo_lock->dequeue_all( ).
          CATCH cx_abap_foreign_lock ##NO_HANDLER.
        ENDTRY.
      ENDIF.
  ENDTRY.
ENDMETHOD.

단위 테스트(ABAP Unit)에서 예외 발생을 검증하는 패턴은 다음과 같다.

METHOD test_missing_po_raises.
  TRY.
      cut->get_po_header( '9999999999' ).
      cl_abap_unit_assert=>fail( 'Expected exception was not raised' ).
    CATCH zcx_purchase_order INTO DATA(lx).
      cl_abap_unit_assert=>assert_equals(
        exp = '9999999999'
        act = lx->po_id ).
  ENDTRY.
ENDMETHOD.

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

Q1. CATCH 블록에서 예외를 잡았는데 아무 로그 없이 사라진다.
빈 CATCH(swallowing)는 최악의 안티패턴이다. 최소한 lx->get_text( )를 SLG1이나 애플리케이션 로그로 남기고, 재던지기가 어렵다면 ##NO_HANDLER 프래그마로 의도를 명시하라. 코드 리뷰어와 이후 유지보수자를 위한 신호다.

Q2. RAISING에 선언했더니 "Exception ZCX_XXX cannot be propagated" 문법 오류가 난다.
CX_STATIC_CHECK 상속 예외는 호출 체인의 모든 메서드 시그니처에 명시적으로 RAISING 선언이 필요하다. 이벤트 핸들러, 클래식 폼 루틴(FORM), 함수 그룹의 특정 콜백 등 시그니처에 RAISING을 쓸 수 없는 위치에서는 CX_DYNAMIC_CHECK 또는 CX_NO_CHECK 상속으로 바꿔야 한다.

Q3. GET_TEXT()가 "?????" 또는 빈 문자열을 반환한다.
메시지 클래스(SE91)의 메시지 번호와 예외 클래스 상수(TEXTID)의 msgid/msgno가 일치하는지, 그리고 attr1~attr4로 지정한 속성 이름이 클래스에 실제로 존재하는 DATA 선언과 정확히 일치하는지 확인하라. 이름 오타가 가장 흔한 원인이다.

Q4. TRY 블록에서 COMMIT WORK 후 예외가 발생했다. 롤백은 어떻게 하나?
COMMIT은 되돌릴 수 없다. 트랜잭션 경계를 TRY 바깥에 두고, 모든 검증이 성공한 후에만 COMMIT하는 것이 일반적으로 권장된다. CLEANUP은 미커밋 변경분을 롤백할 때 유용하지만 이미 커밋된 것은 보상 트랜잭션이 필요하다.

Q5. Legacy 코드에서 CATCH SYSTEM-EXCEPTIONS로 감싸도 되나?
CATCH SYSTEM-EXCEPTIONS는 obsolete로 분류된다. 신규 코드에서는 대응하는 CX_SY_* 클래스(CX_SY_ZERODIVIDE, CX_SY_CONVERSION_NO_NUMBER 등)를 TRY-CATCH로 처리하는 것이 권장된다.

심화 탐구 영역

  • RAP BO 예외 매핑: Behavior Definition의 failed/reported 구조에 예외를 어떻게 실어 보낼지 학습
  • ABAP Cloud 예외 규칙: Released API에서 사용 가능한 예외 클래스 확인 방법(ADT의 Released Object 표시)
  • Application Log(SLG1) 통합: BAL_LOG_CREATE, BAL_LOG_MSG_ADD_FREE_TEXT로 예외 메시지 영구 기록
  • if_t100_dyn_msg_error 인터페이스: 예외 메시지를 동적으로 조립해야 하는 상황에서의 표준 패턴
  • ABAP Unit Test Double Framework: 예외 발생 경로를 안정적으로 재현하는 목/스텁 기법

공식 문서 및 참고 링크

댓글 0

아직 댓글이 없습니다.