News

DI vs 서비스 로케이터 차이 #shorts #SAP #ABAP

▶ YouTube에서 보기

1. 개요 및 이 글의 목표

ABAP 객체지향 설계에서 클래스 간 결합도를 낮추는 두 가지 대표적인 기법이 의존성 주입(Dependency Injection, DI)서비스 로케이터(Service Locator) 패턴입니다. 두 기법 모두 "객체가 자신이 필요로 하는 의존 객체를 직접 NEW로 생성하지 않도록" 만든다는 점에서 비슷해 보이지만, 의존성을 어떻게 획득하는지에 따라 테스트 가능성, 코드 명료성, 유지보수성에서 큰 차이를 보입니다.

본 글에서는 ABAP 7.5x 이상(Steampunk / S/4HANA on-prem 포함)에서 두 패턴을 실제 코드로 비교하고, ABAP Unit에서의 Mock 주입 차이, 실무 도입 시 흔히 빠지는 함정, 그리고 마지막으로 ABAP만으로 구현하는 경량 DI 컨테이너까지 단계적으로 살펴봅니다.

  • 두 패턴의 의존성 획득 방식 차이 이해
  • ABAP Unit에서 Mock 주입의 난이도 비교
  • RAP / BOPF / 레거시 모듈풀에서의 선택 기준 정립
  • 간단한 DI 컨테이너 작성으로 응용력 확보

2. 핵심 개념 — DI vs 서비스 로케이터

의존성을 획득하는 방식은 크게 세 가지로 나뉩니다. 첫째, 클래스 내부에서 직접 NEW로 생성하는 방식(가장 강한 결합). 둘째, 외부의 글로벌 레지스트리(=서비스 로케이터)에게 "내가 필요한 객체 줘"라고 요청하는 방식. 셋째, 외부에서 객체를 생성한 뒤 생성자/세터를 통해 주입받는 방식(DI)입니다.

비유하자면, DI는 "요리사에게 재료를 미리 손질해서 건네주는 것"이고, 서비스 로케이터는 "요리사가 공용 냉장고를 열어 직접 재료를 꺼내 쓰는 것"입니다. 공용 냉장고는 편리하지만, 요리사가 어떤 재료를 꺼내 쓰는지 레시피(클래스 인터페이스)만 봐서는 알 수 없습니다.

이 차이는 "의존성이 명시적인가 암묵적인가"로 요약됩니다. DI는 생성자 시그니처에 모든 의존성이 드러나기 때문에 코드를 읽기만 해도 협력 객체를 파악할 수 있습니다. 반면 서비스 로케이터는 메서드 본문 깊숙한 곳에서 get_instance() 호출이 등장하기 전까지는 어떤 객체에 의존하는지 알 수 없으며, 이를 Mark Seemann은 "의존성 은닉(hidden dependency)"이라 부르며 안티패턴으로 분류하기도 합니다.

3. 1단계: 서비스 로케이터 패턴 구현 — 편의성과 숨겨진 의존성

ABAP에서 서비스 로케이터는 보통 CLASS-METHODS get_instance 형태의 정적 팩토리로 구현됩니다. 다음 예제는 주문 처리(ZCL_ORDER_PROCESSOR)가 통화 변환 서비스(ZCL_CURRENCY_SERVICE)와 로깅 서비스(ZCL_LOGGER)를 사용하는 경우입니다.

" 서비스 로케이터 — 글로벌 레지스트리
CLASS zcl_service_locator DEFINITION PUBLIC FINAL CREATE PRIVATE.
  PUBLIC SECTION.
    CLASS-METHODS:
      get_currency_service RETURNING VALUE(ro_service) TYPE REF TO zif_currency_service,
      get_logger           RETURNING VALUE(ro_logger)  TYPE REF TO zif_logger,
      set_currency_service IMPORTING io_service TYPE REF TO zif_currency_service,
      set_logger           IMPORTING io_logger  TYPE REF TO zif_logger.
  PRIVATE SECTION.
    CLASS-DATA:
      go_currency TYPE REF TO zif_currency_service,
      go_logger   TYPE REF TO zif_logger.
ENDCLASS.

CLASS zcl_service_locator IMPLEMENTATION.
  METHOD get_currency_service.
    IF go_currency IS INITIAL.
      go_currency = NEW zcl_currency_service( ).
    ENDIF.
    ro_service = go_currency.
  ENDMETHOD.
  METHOD get_logger.
    IF go_logger IS INITIAL.
      go_logger = NEW zcl_logger( ).
    ENDIF.
    ro_logger = go_logger.
  ENDMETHOD.
  METHOD set_currency_service. go_currency = io_service. ENDMETHOD.
  METHOD set_logger.           go_logger   = io_logger.  ENDMETHOD.
ENDCLASS.

주문 처리기는 다음과 같이 로케이터를 통해 협력 객체를 획득합니다.

CLASS zcl_order_processor DEFINITION PUBLIC.
  PUBLIC SECTION.
    METHODS process IMPORTING is_order TYPE zorder
                    RETURNING VALUE(rv_total) TYPE p LENGTH 15 DECIMALS 2.
ENDCLASS.

CLASS zcl_order_processor IMPLEMENTATION.
  METHOD process.
    DATA(lo_currency) = zcl_service_locator=>get_currency_service( ).
    DATA(lo_logger)   = zcl_service_locator=>get_logger( ).

    rv_total = lo_currency->convert(
                 iv_amount = is_order-amount
                 iv_from   = is_order-currency
                 iv_to     = 'KRW' ).
    lo_logger->info( |Order { is_order-id } converted to { rv_total }| ).
  ENDMETHOD.
ENDCLASS.

보기에 깔끔하고 호출부도 NEW zcl_order_processor( )->process( ... ) 한 줄로 끝납니다. 그러나 클래스 외부에서 ZCL_ORDER_PROCESSOR의 시그니처만 봐서는 통화 서비스나 로거에 의존한다는 사실을 전혀 알 수 없습니다. 이것이 일반적으로 지적되는 서비스 로케이터의 핵심 단점입니다.

4. 2단계: DI 패턴 — 생성자 주입으로 의존성 명시화

같은 로직을 생성자 주입(Constructor Injection) 방식으로 바꿔 보겠습니다. 의존하는 인터페이스를 CONSTRUCTOR의 임포트 파라미터로 받도록 강제하면, 컴파일 시점에 의존성이 코드 표면에 드러납니다.

CLASS zcl_order_processor_di DEFINITION PUBLIC FINAL.
  PUBLIC SECTION.
    METHODS:
      constructor IMPORTING io_currency TYPE REF TO zif_currency_service
                            io_logger   TYPE REF TO zif_logger,
      process     IMPORTING is_order    TYPE zorder
                  RETURNING VALUE(rv_total) TYPE p LENGTH 15 DECIMALS 2.
  PRIVATE SECTION.
    DATA:
      mo_currency TYPE REF TO zif_currency_service,
      mo_logger   TYPE REF TO zif_logger.
ENDCLASS.

CLASS zcl_order_processor_di IMPLEMENTATION.
  METHOD constructor.
    mo_currency = io_currency.
    mo_logger   = io_logger.
  ENDMETHOD.

  METHOD process.
    rv_total = mo_currency->convert(
                 iv_amount = is_order-amount
                 iv_from   = is_order-currency
                 iv_to     = 'KRW' ).
    mo_logger->info( |Order { is_order-id } -> { rv_total }| ).
  ENDMETHOD.
ENDCLASS.

호출부(컴포지션 루트)에서 의존성을 조립합니다. 보통 프로그램 진입점이나 RAP의 Behavior Implementation 진입부에 위치시킵니다.

START-OF-SELECTION.
  DATA(lo_processor) = NEW zcl_order_processor_di(
                         io_currency = NEW zcl_currency_service( )
                         io_logger   = NEW zcl_logger( ) ).
  DATA(lv_total) = lo_processor->process( is_order = ls_order ).

이제 클래스 사용자는 ZCL_ORDER_PROCESSOR_DI가 무엇에 의존하는지 한눈에 파악할 수 있고, 정적 변수나 글로벌 상태에 의존하지 않으므로 병렬 호출에서도 안전합니다.

5. 3단계: ABAP Unit 테스트에서의 차이 — Mock 주입 vs 글로벌 상태 변경

두 패턴의 차이가 가장 극명하게 드러나는 곳이 단위 테스트입니다. ABAP Unit과 CL_ABAP_TESTDOUBLE(테스트 더블 프레임워크)을 사용해 비교해 보겠습니다.

" DI 버전 — 깔끔한 Mock 주입
CLASS ltc_processor_di DEFINITION FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    DATA:
      mo_cut       TYPE REF TO zcl_order_processor_di,
      mo_currency  TYPE REF TO zif_currency_service,
      mo_logger    TYPE REF TO zif_logger.
    METHODS:
      setup,
      convert_amount_to_krw FOR TESTING.
ENDCLASS.

CLASS ltc_processor_di IMPLEMENTATION.
  METHOD setup.
    mo_currency ?= cl_abap_testdouble=>create( 'ZIF_CURRENCY_SERVICE' ).
    mo_logger   ?= cl_abap_testdouble=>create( 'ZIF_LOGGER' ).
    mo_cut = NEW #( io_currency = mo_currency
                    io_logger   = mo_logger ).
  ENDMETHOD.

  METHOD convert_amount_to_krw.
    cl_abap_testdouble=>configure_call( mo_currency )->returning( '1300.00' ).
    mo_currency->convert( iv_amount = 1 iv_from = 'USD' iv_to = 'KRW' ).

    DATA(lv_total) = mo_cut->process(
      VALUE #( id = '0001' amount = 1 currency = 'USD' ) ).

    cl_abap_unit_assert=>assert_equals( exp = '1300.00' act = lv_total ).
  ENDMETHOD.
ENDCLASS.

DI 버전은 생성자에 Mock을 그대로 넣으면 끝납니다. 반면 서비스 로케이터 버전은 테스트 전후로 글로벌 상태를 조작해야 하며, 이는 다른 테스트에 부수효과를 남길 위험이 있습니다.

" Service Locator 버전 — 글로벌 상태 오염 위험
CLASS ltc_processor_sl DEFINITION FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    METHODS:
      setup, teardown,
      convert_amount FOR TESTING.
ENDCLASS.

CLASS ltc_processor_sl IMPLEMENTATION.
  METHOD setup.
    DATA(lo_mock) = CAST zif_currency_service(
                      cl_abap_testdouble=>create( 'ZIF_CURRENCY_SERVICE' ) ).
    cl_abap_testdouble=>configure_call( lo_mock )->returning( '1300.00' ).
    lo_mock->convert( iv_amount = 1 iv_from = 'USD' iv_to = 'KRW' ).

    " 글로벌 상태 변경 — 테스트 격리 깨질 위험
    zcl_service_locator=>set_currency_service( lo_mock ).
  ENDMETHOD.

  METHOD teardown.
    " 반드시 원복하지 않으면 다른 테스트가 영향받음
    zcl_service_locator=>set_currency_service( NEW zcl_currency_service( ) ).
  ENDMETHOD.

  METHOD convert_amount.
    " ... 호출 및 검증 ...
  ENDMETHOD.
ENDCLASS.

즉 서비스 로케이터를 사용하면 테스트마다 setup/teardown에서 글로벌 상태를 손봐야 하고, 한 곳이라도 누락되면 후속 테스트가 예기치 않게 깨집니다. 권장되는 방식은 가능한 한 생성자 주입을 선택하는 것입니다.

6. 실무 선택 기준 — 언제 DI, 언제 서비스 로케이터를 쓸 것인가

두 패턴은 흑백이 아닙니다. 현실의 ABAP 프로젝트, 특히 BAdI/Enhancement 진입점이나 트랜잭션 코드에서 시작하는 모듈풀처럼 "생성 시점을 통제할 수 없는" 코드에서는 서비스 로케이터가 현실적인 대안이 되기도 합니다.

상황권장 패턴이유
신규 RAP/CDS 기반 비즈니스 로직DIBehavior Pool 진입부에서 한 번만 조립하면 됨
BAdI/Enhancement에서 인스턴스 생성 통제 불가서비스 로케이터(제한적)주입할 컴포지션 루트가 없음
레거시 모듈풀/리포트 점진적 리팩터링혼합점진적으로 DI 영역 확대
유틸리티성 싱글톤(설정, 캐시)서비스 로케이터 허용변하지 않는 의존성

일반적으로 "도메인 로직"은 DI, "인프라성 횡단 관심사(로깅, 설정)"는 서비스 로케이터를 혼합하는 패턴이 ABAP 생태계에서 무난한 절충안으로 자주 사용됩니다.

7. 자주 겪는 함정 — 순환 의존성, 서비스 로케이터 남용, DI 컨테이너 복잡도

함정 1: 순환 의존성. AB를, BA를 생성자에서 요구하면 컴포지션 루트에서 객체를 만들 수 없습니다. 이 경우 한쪽을 세터 주입(Setter Injection)으로 바꾸거나, 두 클래스가 의존하는 제3의 인터페이스를 추출해 의존 방향을 한 방향으로 정리해야 합니다.

함정 2: 서비스 로케이터 남용. 처음에는 "한두 군데만 쓰자" 했다가 점점 모든 클래스가 로케이터에 손을 뻗으면, 의존성이 코드 곳곳에 흩어져 영향 범위를 파악하기 어려워집니다. 이를 방지하려면 로케이터 접근을 오직 컴포지션 루트와 진입점 어댑터에서만 허용하는 규약(ATC 룰 등)을 두는 것이 좋습니다.

함정 3: 과한 DI 컨테이너. Java Spring 스타일의 풀 기능 컨테이너를 ABAP에 흉내 내려다 보면 메타데이터 테이블이 비대해지고 디버깅 난도가 올라갑니다. ABAP에서는 대부분 생성자 주입 + 간단한 팩토리로 충분하며, 컨테이너는 정말 필요한 경우에만 도입을 고려하는 편이 권장됩니다.

FAQ.
Q1) 정적 메서드만 있는 헬퍼 클래스도 주입해야 하나요? — 상태가 없고 SAP 표준 함수처럼 안정적이라면 직접 호출해도 무방합니다.
Q2) RAP Behavior Pool에서 DI가 어렵습니다. — Behavior Pool은 프레임워크가 인스턴스화하므로, 내부에서 사용하는 도메인 서비스만 DI로 조립하면 됩니다.
Q3) CL_ABAP_TESTDOUBLE 대신 수동 Mock을 만들어도 되나요? — 됩니다. 단, 인터페이스 변경 시 동기화 부담이 커지므로 표준 더블을 권장합니다.

8. 응용 패턴 — ABAP에서 간단한 DI 컨테이너 구현

마지막으로, 자주 쓰이는 의존성을 등록/조회하는 가벼운 컨테이너를 만들어 봅니다. 타입 이름(인터페이스명)을 키로 하여 팩토리 람다를 저장하는 구조이며, 100줄 미만으로 구현 가능합니다.

CLASS zcl_di_container DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES:
      BEGIN OF ty_entry,
        type_name TYPE string,
        factory   TYPE REF TO zif_di_factory,
      END OF ty_entry.

    METHODS:
      register IMPORTING iv_type    TYPE string
                         io_factory TYPE REF TO zif_di_factory,
      resolve  IMPORTING iv_type    TYPE string
               RETURNING VALUE(ro_object) TYPE REF TO object
               RAISING   zcx_di_unresolved.
  PRIVATE SECTION.
    DATA mt_registry TYPE HASHED TABLE OF ty_entry
                     WITH UNIQUE KEY type_name.
ENDCLASS.

CLASS zcl_di_container IMPLEMENTATION.
  METHOD register.
    INSERT VALUE #( type_name = to_upper( iv_type )
                    factory   = io_factory )
           INTO TABLE mt_registry.
  ENDMETHOD.

  METHOD resolve.
    DATA(ls_entry) = VALUE #( mt_registry[ type_name = to_upper( iv_type ) ]
                              OPTIONAL ).
    IF ls_entry-factory IS INITIAL.
      RAISE EXCEPTION TYPE zcx_di_unresolved
        EXPORTING type_name = iv_type.
    ENDIF.
    ro_object = ls_entry-factory->create( me ).
  ENDMETHOD.
ENDCLASS.

사용 예는 다음과 같습니다. 부팅 시 한 번만 등록하고, 진입점에서 resolve로 도메인 서비스를 꺼내 씁니다.

DATA(lo_di) = NEW zcl_di_container( ).
lo_di->register( iv_type    = 'ZIF_CURRENCY_SERVICE'
                 io_factory = NEW zcl_currency_factory( ) ).
lo_di->register( iv_type    = 'ZIF_LOGGER'
                 io_factory = NEW zcl_logger_factory( ) ).

DATA(lo_processor) = NEW zcl_order_processor_di(
  io_currency = CAST zif_currency_service( lo_di->resolve( 'ZIF_CURRENCY_SERVICE' ) )
  io_logger   = CAST zif_logger( lo_di->resolve( 'ZIF_LOGGER' ) ) ).

이 컨테이너는 서비스 로케이터처럼 보일 수 있지만, 결정적인 차이는 도메인 클래스 내부에서는 컨테이너를 절대 참조하지 않는다는 점입니다. 오직 컴포지션 루트만이 컨테이너를 알고, 도메인 클래스는 여전히 생성자 주입을 통해 의존성을 명시적으로 받습니다. 이렇게 하면 DI의 명료성과 컨테이너의 재사용성을 동시에 얻을 수 있습니다.

다음 단계로는 SAP Cloud SDK for ABAP의 Service Consumption 패턴, RAP의 Behavior Implementation에서의 의존성 주입, 그리고 CL_ABAP_TESTDOUBLE 기반 테스트 자동화로 학습을 확장하는 것을 추천합니다.

댓글 0

아직 댓글이 없습니다.