개요 및 이 글에서 다룰 것
ABAP에서 Observer(관찰자) 패턴은 한 객체의 상태 변화를 여러 의존 객체에게 자동으로 전파하기 위한 행위(behavioral) 디자인 패턴입니다. 주문이 생성되었을 때 로그를 남기고, 알림을 전송하고, 통계 시스템을 업데이트하는 등 서로 무관한 후속 처리들을 한 줄의 호출로 묶을 수 있어, 추가 요구사항이 생겨도 기존 비즈니스 로직 클래스를 손대지 않고 확장할 수 있다는 장점이 있습니다.
이번 문서에서는 ABAP Objects 기준으로 Observer 패턴을 직접 구현하고 실무에 어떻게 녹여 쓰는지 살펴봅니다.
- Subject와 Observer 인터페이스를 분리하여 느슨한 결합을 설계할 수 있다
- 이벤트 통지 시점에 다수의 Observer를 일괄 호출하는 코드를 작성할 수 있다
- 로그/알림/통계 등 횡단 관심사를 별도 클래스로 분리해 단일 책임 원칙(SRP)을 지킬 수 있다
- ABAP EVENTS 키워드와의 차이를 이해하고 상황에 맞게 선택할 수 있다
이 글을 보기 전에
본 문서는 ABAP Objects의 기본 문법, 즉 CLASS DEFINITION, INTERFACE, METHOD, 그리고 NEW를 활용한 인스턴스 생성에 익숙한 독자를 대상으로 합니다. 추가로 내부 테이블(STANDARD TABLE OF) 조작과 LOOP AT 문, 그리고 인터페이스 참조 변수에 구현 클래스를 대입하는 다형성 개념을 알고 있으면 코드를 빠르게 따라갈 수 있습니다. GoF 디자인 패턴 중 Observer의 일반적인 정의(Subject-Observer 1:N 관계)에 대한 가벼운 사전 지식이 있으면 더욱 좋습니다.
환경 및 준비물
이 글의 예제는 다음과 같은 환경에서 검증하기를 권장합니다. SAP NetWeaver 7.50 이상 또는 SAP S/4HANA 1909 이상의 ABAP 스택을 사용하면 본문에 등장하는 인라인 선언(DATA(...))과 새로운 생성자 표현식 NEW #( )이 자연스럽게 동작합니다.
- ABAP 릴리스: 7.50 이상 (S/4HANA on-premise 또는 ABAP Platform 2022 이상 권장)
- 개발 도구: ABAP Development Tools (ADT) for Eclipse 2024-03 이상
- 패키지: 로컬 객체($TMP) 또는 임시 개발 패키지
- 권한:
S_DEVELOP(CLAS, INTF Object Type에 대한 02/Change 권한) - 테스트 도구: ABAP Unit (SE80 또는 ADT의 Run As - ABAP Unit Test 메뉴)
실습은 SE24/SE80에서도 가능하지만, ADT를 사용하면 인터페이스/클래스 간 점프와 리팩토링이 훨씬 수월합니다. 예제는 모두 로컬 클래스(lcl_*) 기반이므로 글로벌 클래스를 만들지 않고 테스트 프로그램 한 개에 모아 두어도 무방합니다.
핵심 개념
Observer 패턴은 발행자(Subject, Publisher)와 구독자(Observer, Subscriber)라는 두 역할을 분리합니다. Subject는 자신의 상태가 바뀌었을 때 등록되어 있는 Observer 목록을 순회하며 약속된 메서드(update)를 호출하기만 합니다. Observer가 그 통지를 받아 무엇을 할지는 전적으로 Observer 자신의 책임이며, Subject는 누가 어떤 행동을 하는지 알 필요가 없습니다. 이 단방향 의존 구조 덕분에 새로운 Observer가 추가되어도 Subject 코드를 수정하지 않아도 됩니다.
비유: 뉴스레터 구독
가장 직관적인 비유는 뉴스레터입니다. 발행사(Subject)는 새 호가 나올 때마다 구독자(Observer) 명단을 보고 발송 버튼을 누를 뿐, 각 구독자가 받은 메일을 어디에 저장하는지, 자동으로 슬랙에 옮기는지는 신경 쓰지 않습니다. 구독을 끊거나 새로 시작하는 것도 발행사의 발행 로직과 무관합니다. ABAP에서는 이 명단을 내부 테이블로, 발송 버튼을 notify 메서드로 구현합니다.
구조 도식
* [lif_subject] [lif_observer]
* + subscribe( io ) 1..* + update( i_data )
* + unsubscribe( io ) ---->
* + notify( )
*
* ^ ^
* | |
* [lcl_order_subject] [lcl_log_observer]
* [lcl_alert_observer]ABAP EVENTS와의 차이
ABAP은 EVENTS/RAISE EVENT/SET HANDLER라는 언어 차원의 이벤트 메커니즘도 제공합니다. 작은 모듈 안에서 빠르게 알림을 흘려보내고 싶다면 이 기능만으로도 충분합니다. 다만 (1) 구독자 목록을 런타임에 동적으로 관리하거나, (2) 단위 테스트에서 Observer를 모킹(mock)하기 쉽게 하거나, (3) 통지 순서/실패 처리 같은 정책을 명시적으로 코드에 박아두고 싶다면 인터페이스 기반 Observer 패턴이 더 유연합니다. 일반적으로는 비즈니스 도메인 이벤트일수록 패턴 기반, UI/단순 콜백은 EVENTS 기반으로 나눠 쓰는 편이 깔끔합니다.
실전 코드
1단계: Observer와 Subject 인터페이스 선언
먼저 두 역할의 계약을 정의합니다. Observer는 통지를 받기 위한 update 메서드 하나만 가지고, Subject는 구독자 관리와 통지를 책임지는 세 개의 메서드를 가집니다. 주문 데이터는 간단한 구조체 ty_order로 표현하겠습니다.
REPORT zdemo_observer_pattern.
TYPES: BEGIN OF ty_order,
order_id TYPE char10,
customer TYPE char40,
amount TYPE p LENGTH 13 DECIMALS 2,
currency TYPE waers,
END OF ty_order.
" Observer 계약: 누군가 상태가 바뀌었다고 알려주면 받는다
INTERFACE lif_observer.
METHODS update
IMPORTING
is_order TYPE ty_order.
ENDINTERFACE.
" Subject 계약: 구독자 등록/해지/통지를 책임진다
INTERFACE lif_subject.
METHODS:
subscribe
IMPORTING io_observer TYPE REF TO lif_observer,
unsubscribe
IMPORTING io_observer TYPE REF TO lif_observer,
notify.
ENDINTERFACE.update 메서드의 인자로는 도메인 객체(여기서는 주문)를 통째로 넘기는 방식을 선택했습니다. 통지마다 전달할 정보가 다양하다면 별도의 이벤트 구조체(예: ty_event)를 두고 type 필드로 종류를 구분하는 방식도 흔히 쓰입니다.
2단계: Observer 구현 클래스 작성
이번 단계에서는 두 가지 Observer를 만듭니다. 하나는 통지 내용을 애플리케이션 로그에 남기는 lcl_log_observer, 다른 하나는 일정 금액 이상의 주문에만 반응해 알림을 보내는 lcl_alert_observer입니다. 실무에서는 한 Observer가 실패해도 다른 Observer까지 막히면 곤란하므로 각 구현체 안에서 TRY ... CATCH로 자신만의 예외를 흡수합니다.
CLASS lcl_log_observer DEFINITION.
PUBLIC SECTION.
INTERFACES lif_observer.
ENDCLASS.
CLASS lcl_log_observer IMPLEMENTATION.
METHOD lif_observer~update.
TRY.
DATA(lv_msg) =
|Order { is_order-order_id } | &&
|for { is_order-customer } | &&
|amount { is_order-amount } { is_order-currency } logged|.
WRITE: / '[LOG]', lv_msg.
" 실무에서는 BAL_LOG_CREATE / BAL_LOG_MSG_ADD 등으로 SLG1에 기록
CATCH cx_root INTO DATA(lx_log).
WRITE: / '[LOG] failed:', lx_log->get_text( ).
ENDTRY.
ENDMETHOD.
ENDCLASS.
CLASS lcl_alert_observer DEFINITION.
PUBLIC SECTION.
INTERFACES lif_observer.
METHODS constructor
IMPORTING iv_threshold TYPE p LENGTH 13 DECIMALS 2.
PRIVATE SECTION.
DATA mv_threshold TYPE p LENGTH 13 DECIMALS 2.
ENDCLASS.
CLASS lcl_alert_observer IMPLEMENTATION.
METHOD constructor.
mv_threshold = iv_threshold.
ENDMETHOD.
METHOD lif_observer~update.
CHECK is_order-amount >= mv_threshold.
TRY.
WRITE: / '[ALERT] High value order:',
is_order-order_id,
is_order-amount,
is_order-currency.
" 실무에서는 SO_NEW_DOCUMENT_SEND_API1 / Slack Webhook 호출
CATCH cx_root INTO DATA(lx_alert).
WRITE: / '[ALERT] failed:', lx_alert->get_text( ).
ENDTRY.
ENDMETHOD.
ENDCLASS.lcl_alert_observer는 생성자에서 임계값을 받습니다. 같은 클래스를 여러 인스턴스로 등록해서 임계값별로 서로 다른 채널(SMS, 메일, 슬랙)을 분기시키는 식의 확장이 자연스럽습니다. Observer가 늘어나더라도 Subject 쪽 코드는 단 한 줄도 바뀌지 않는다는 점에 주목해 주세요.
3단계: Subject 구현과 호출부, 그리고 프로덕션 고려사항
마지막으로 lcl_order_subject를 만들고, 구독자 명단을 내부 테이블로 관리한 뒤 notify에서 일괄 통지합니다. 프로덕션 코드에서는 (1) Observer 호출 중 예외가 Subject까지 전파되지 않도록 차단하고, (2) 동일 인스턴스가 중복 구독되지 않도록 키 비교를 하고, (3) 단위 테스트를 위해 Subject가 인터페이스에만 의존하도록 작성하는 것이 일반적으로 권장됩니다.
CLASS lcl_order_subject DEFINITION.
PUBLIC SECTION.
INTERFACES lif_subject.
METHODS:
set_order IMPORTING is_order TYPE ty_order,
place_order.
PRIVATE SECTION.
DATA:
mt_observers TYPE STANDARD TABLE OF REF TO lif_observer
WITH EMPTY KEY,
ms_order TYPE ty_order.
ENDCLASS.
CLASS lcl_order_subject IMPLEMENTATION.
METHOD lif_subject~subscribe.
" 중복 등록 방지
READ TABLE mt_observers TRANSPORTING NO FIELDS
WITH KEY table_line = io_observer.
IF sy-subrc <> 0.
APPEND io_observer TO mt_observers.
ENDIF.
ENDMETHOD.
METHOD lif_subject~unsubscribe.
DELETE mt_observers WHERE table_line = io_observer.
ENDMETHOD.
METHOD lif_subject~notify.
LOOP AT mt_observers INTO DATA(lo_obs).
TRY.
lo_obs->update( is_order = ms_order ).
CATCH cx_root INTO DATA(lx).
" 한 Observer의 실패가 다른 Observer로 전파되지 않도록 격리
WRITE: / '[SUBJECT] observer failed:', lx->get_text( ).
ENDTRY.
ENDLOOP.
ENDMETHOD.
METHOD set_order.
ms_order = is_order.
ENDMETHOD.
METHOD place_order.
" 1) 핵심 비즈니스 로직 (DB INSERT 등) 수행 후
" 2) 상태 변경을 구독자에게 알린다
lif_subject~notify( ).
ENDMETHOD.
ENDCLASS.
START-OF-SELECTION.
DATA(lo_subject) = NEW lcl_order_subject( ).
lo_subject->lif_subject~subscribe(
io_observer = NEW lcl_log_observer( ) ).
lo_subject->lif_subject~subscribe(
io_observer = NEW lcl_alert_observer( iv_threshold = '10000.00' ) ).
lo_subject->set_order(
is_order = VALUE #( order_id = '0000000042'
customer = 'ACME Corp.'
amount = '25000.00'
currency = 'EUR' ) ).
lo_subject->place_order( ).이 구조에서는 새로운 후속 처리(예: 통계 집계, KPI 대시보드 푸시)가 필요해지면 lif_observer를 구현하는 클래스를 하나 더 만들고 subscribe만 호출하면 됩니다. 단위 테스트도 lif_observer의 테스트 더블을 만들어 등록하고 place_order 호출 후 update가 의도한 데이터와 함께 호출되었는지 검증하는 식으로 깔끔하게 작성할 수 있습니다. 보안 측면에서는 update에 전달하는 페이로드에 민감 정보(예: 신용카드 번호 전체)가 포함되지 않도록 마스킹 단계를 Subject 쪽에서 한 번 거치도록 정책을 두는 것을 권장합니다.
흔한 실수와 트러블슈팅
Observer 패턴은 단순해 보이지만 ABAP 환경에서 자주 만나는 함정이 몇 가지 있습니다. 아래 FAQ를 통해 빠르게 점검해 보세요.
Q1. 통지(notify)는 동기로 동작하나요? 한 Observer가 오래 걸리면 어떻게 되나요?
위 예제의 LOOP AT 기반 호출은 모두 동기 순차 호출입니다. 따라서 한 Observer가 RFC나 HTTP 호출로 5초가 걸리면 그만큼 전체 트랜잭션이 지연됩니다. 일반적으로 무거운 후속 처리는 CALL FUNCTION ... IN BACKGROUND TASK 또는 bgRFC/qRFC, S/4HANA에서는 CL_BGMC_PROCESS_FACTORY(Background Processing Framework)를 이용해 비동기로 흘려보내는 것이 권장됩니다.
Q2. Observer를 등록했는데 가비지 컬렉션이 안 되는 것 같아요.
Subject가 Observer 참조를 내부 테이블에 들고 있는 한 GC 대상에서 제외됩니다. 화면을 떠나거나 트랜잭션이 종료될 때 unsubscribe를 명시적으로 호출하거나, 약한 참조가 필요한 경우 별도의 ID(예: GUID)를 키로 두고 외부 레지스트리에서 lookup하는 방식으로 우회할 수 있습니다. 글로벌 인스턴스(class-data)에 Subject를 두는 경우 특히 주의가 필요합니다.
Q3. ABAP의 EVENTS/SET HANDLER로 충분하지 않나요?
UI 클래스 내부에서 더블클릭, 토글 같은 작은 시그널을 흘려보낼 때는 EVENTS만으로 충분합니다. 그러나 (a) 어떤 구독자가 어떤 순서로 도는지 명시적으로 보이게 하고 싶을 때, (b) 단위 테스트에서 핸들러를 모킹하고 싶을 때, (c) 도메인 이벤트가 트랜잭션 경계와 맞물려야 할 때는 인터페이스 기반 Observer 패턴이 일반적으로 더 명확합니다. 두 방식은 배타적이지 않으며 한 시스템 안에서 공존시키는 경우도 많습니다.
기타 자주 만나는 함정
- 순환 통지: Observer가 같은 Subject의 메서드를 호출해 다시 notify를 유발하면 무한 루프에 빠질 수 있습니다.
notify진입 시 플래그로 가드하세요. - 예외 전파: 한 Observer가 던진 예외가 Subject 밖으로 새어 나가면 비즈니스 로직이 롤백될 수 있습니다.
TRY ... CATCH cx_root로 격리하는 것을 권장합니다. - 중복 구독: 동일 인스턴스를 두 번 등록하면 같은 알림이 두 번 발생합니다.
READ TABLE ... TRANSPORTING NO FIELDS로 사전 검사하세요.
다음 단계와 관련 주제
Observer 패턴을 익혔다면, 다음 주제로 자연스럽게 확장할 수 있습니다.
- Publish/Subscribe와 메시지 브로커: SAP Event Mesh, Kafka 연계를 통해 시스템 경계를 넘는 이벤트 통지로 확장
- 도메인 이벤트와 RAP: ABAP RESTful Application Programming Model에서
WITH DRAFT/determinations와 결합한 도메인 이벤트 발행 - BAL(Business Application Log)과 결합해 Observer 중 하나를 표준 로그 적재기로 표준화
- Mediator/Chain of Responsibility 등 인접 행위 패턴과의 비교
- ABAP Unit + Test Double Framework로 Observer 호출 횟수/인자 검증 테스트 작성
댓글 0
아직 댓글이 없습니다.