개요 및 이 글에서 다루는 것
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
아직 댓글이 없습니다.