News

Singleton vs 일반 클래스 — ABAP 전역 인스턴스 관리 #shorts #SAP #ABAP

▶ YouTube에서 보기

1. 개요 및 이 글의 목표

ABAP Objects의 Singleton 패턴은 특정 클래스의 인스턴스가 단일 세션 내에서 단 하나만 존재하도록 보장하는 객체 생성 패턴입니다. 설정 관리자, 캐시, 로거, 데이터베이스 연결과 같이 시스템 전반에서 동일한 상태를 공유해야 하는 컴포넌트를 구현할 때 일반적으로 사용됩니다. 본 글에서는 CREATE PRIVATE 부가 절을 활용한 기본 구현부터, GET_INSTANCE 정적 메서드를 통한 인스턴스 반환 패턴, 그리고 ABAP Unit 테스트에서의 모킹과 의존성 주입(DI)을 결합한 실무 응용 패턴까지 단계적으로 다룹니다.

  • 체크: CREATE PRIVATE의 의미와 외부 인스턴스화 차단 방법
  • 체크: 정적 속성(CLASS-DATA)으로 단일 인스턴스 보관
  • 체크: 멀티 세션 환경에서 Singleton 범위에 대한 오해 해소
  • 체크: ABAP Unit 테스트에서 Singleton 격리 전략

2. 핵심 개념 — Singleton vs 일반 클래스 (인스턴스 생명주기 차이)

일반 ABAP 클래스는 CREATE OBJECT 또는 NEW 연산자를 통해 호출자가 원하는 만큼 인스턴스를 생성할 수 있습니다. 각 인스턴스는 고유한 메모리 영역과 속성 값을 가지며, 가비지 컬렉터에 의해 참조가 끊어지면 회수됩니다. 반면 Singleton 클래스는 생성자를 PRIVATE로 감추고, 클래스 자신이 보관하는 단 하나의 인스턴스를 외부에 노출합니다.

비유하자면 일반 클래스가 "원하는 만큼 찍어내는 명함"이라면, Singleton은 "회사 대표 도장" 한 개입니다. 부서가 달라도 동일한 도장 하나를 빌려 쓰며, 도장 내부의 상태(잉크 농도, 마지막 사용 시각)는 전사적으로 공유됩니다.

ABAP에서 Singleton 인스턴스의 생명주기는 내부 모드(internal mode) 단위입니다. 즉, 동일한 ABAP 세션(예: 동일한 SAP GUI 트랜잭션이나 동일한 RFC 호출) 내에서만 공유되며, 다른 사용자 세션이나 다른 워크 프로세스에서는 별개의 인스턴스가 생성됩니다. 이 지점을 오해하면 "Singleton이니까 전역 캐시처럼 동작하겠지"라고 가정했다가 데이터 정합성 문제를 겪을 수 있습니다. 실제 전역 공유가 필요하다면 SHMA(Shared Memory Area) 또는 DB 기반 영속화를 별도로 고려해야 합니다.

도식으로 비교하면 다음과 같습니다.

" 일반 클래스
DATA(o1) = NEW zcl_user( 'A' ).
DATA(o2) = NEW zcl_user( 'B' ).   " 서로 다른 인스턴스

" Singleton
DATA(s1) = zcl_config=>get_instance( ).
DATA(s2) = zcl_config=>get_instance( ).
" s1 = s2 (동일 참조)

3. 1단계 — CREATE PRIVATE + 정적 멤버로 기본 Singleton 구현

Singleton 구현의 출발점은 외부에서 임의로 NEW를 호출하지 못하도록 막는 것입니다. ABAP에서는 클래스 선언 시 CREATE PRIVATE 부가 절로 생성자를 클래스 내부에서만 호출 가능하게 제한할 수 있습니다. 다음은 가장 단순한 형태의 Singleton 골격입니다.

CLASS zcl_config_basic DEFINITION
  PUBLIC
  FINAL
  CREATE PRIVATE.

  PUBLIC SECTION.
    CLASS-METHODS get_instance
      RETURNING VALUE(ro_instance) TYPE REF TO zcl_config_basic.

    METHODS get_value
      IMPORTING iv_key          TYPE string
      RETURNING VALUE(rv_value) TYPE string.

  PRIVATE SECTION.
    CLASS-DATA go_instance TYPE REF TO zcl_config_basic.
    DATA mt_config TYPE SORTED TABLE OF zconfig_line
                   WITH UNIQUE KEY key.

    METHODS constructor.
ENDCLASS.

CLASS zcl_config_basic IMPLEMENTATION.
  METHOD get_instance.
    IF go_instance IS INITIAL.
      go_instance = NEW #( ).
    ENDIF.
    ro_instance = go_instance.
  ENDMETHOD.

  METHOD constructor.
    " 최초 한 번만 호출됨 (세션당)
    SELECT key, value FROM zconfig_line
      INTO TABLE @mt_config.
  ENDMETHOD.

  METHOD get_value.
    READ TABLE mt_config WITH KEY key = iv_key
      ASSIGNING FIELD-SYMBOL(<ls>).
    IF sy-subrc = 0.
      rv_value = <ls>-value.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

이 구현의 포인트는 세 가지입니다. 첫째, CREATE PRIVATE로 외부에서 NEW zcl_config_basic( )을 호출하면 컴파일 오류가 발생합니다. 둘째, CLASS-DATA go_instance는 클래스 단위로 단 한 번 메모리에 할당되는 정적 속성입니다. 셋째, get_instance에서 lazy initialization을 수행하여 최초 호출 시점에만 인스턴스를 생성합니다.

4. 2단계 — GET_INSTANCE 메서드로 단일 인스턴스 반환 패턴

실무에서는 단순 lazy init만으로는 부족합니다. 초기화 파라미터, 예외 처리, 그리고 향후 확장 가능성을 고려해 GET_INSTANCE를 다듬어야 합니다. 다음 예제는 환경(개발/품질/운영)에 따라 다른 설정을 로드하면서도 단일 인스턴스를 유지하는 패턴입니다.

CLASS zcl_app_context DEFINITION
  PUBLIC
  FINAL
  CREATE PRIVATE.

  PUBLIC SECTION.
    CLASS-METHODS get_instance
      IMPORTING iv_landscape       TYPE string OPTIONAL
      RETURNING VALUE(ro_instance) TYPE REF TO zcl_app_context
      RAISING   zcx_app_context_error.

    METHODS get_landscape
      RETURNING VALUE(rv_landscape) TYPE string.

    METHODS is_production
      RETURNING VALUE(rv_flag) TYPE abap_bool.

  PRIVATE SECTION.
    CLASS-DATA go_instance TYPE REF TO zcl_app_context.
    DATA mv_landscape TYPE string.

    METHODS constructor
      IMPORTING iv_landscape TYPE string
      RAISING   zcx_app_context_error.
ENDCLASS.

CLASS zcl_app_context IMPLEMENTATION.
  METHOD get_instance.
    IF go_instance IS INITIAL.
      DATA(lv_landscape) = COND #(
        WHEN iv_landscape IS NOT INITIAL THEN iv_landscape
        ELSE sy-sysid ).
      go_instance = NEW #( lv_landscape ).
    ENDIF.
    ro_instance = go_instance.
  ENDMETHOD.

  METHOD constructor.
    IF iv_landscape IS INITIAL.
      RAISE EXCEPTION TYPE zcx_app_context_error
        EXPORTING textid = zcx_app_context_error=>empty_landscape.
    ENDIF.
    mv_landscape = iv_landscape.
  ENDMETHOD.

  METHOD get_landscape.
    rv_landscape = mv_landscape.
  ENDMETHOD.

  METHOD is_production.
    rv_flag = xsdbool( mv_landscape = 'PRD' ).
  ENDMETHOD.
ENDCLASS.

주의할 점은 get_instance에 파라미터를 받더라도, 두 번째 호출부터는 파라미터가 무시된다는 것입니다. 이는 "처음 한 번만 초기화" 라는 Singleton의 본질과 맞물려 있으며, 호출 순서에 따라 동작이 달라질 수 있으므로 호출 컨벤션을 팀 내에서 명문화하는 것이 권장됩니다.

5. 3단계 — 실무 적용: 설정 관리자, 캐시, 로거 패턴

실무에서 Singleton이 가장 자주 등장하는 세 가지 시나리오는 설정 관리자, 메모리 캐시, 그리고 로거입니다. 다음 예제는 트랜잭션 실행 중 발생한 메시지를 단일 로거에 누적했다가 종료 시점에 일괄 기록하는 패턴입니다.

CLASS zcl_app_logger DEFINITION
  PUBLIC
  FINAL
  CREATE PRIVATE.

  PUBLIC SECTION.
    TYPES:
      BEGIN OF ty_log,
        timestamp TYPE timestampl,
        severity  TYPE c LENGTH 1,
        message   TYPE string,
      END OF ty_log,
      tt_log TYPE STANDARD TABLE OF ty_log WITH EMPTY KEY.

    CLASS-METHODS get_instance
      RETURNING VALUE(ro_logger) TYPE REF TO zcl_app_logger.

    METHODS info
      IMPORTING iv_message TYPE string.

    METHODS error
      IMPORTING iv_message TYPE string.

    METHODS flush.

    METHODS get_entries
      RETURNING VALUE(rt_entries) TYPE tt_log.

  PRIVATE SECTION.
    CLASS-DATA go_instance TYPE REF TO zcl_app_logger.
    DATA mt_entries TYPE tt_log.

    METHODS append_entry
      IMPORTING iv_severity TYPE c
                iv_message  TYPE string.
ENDCLASS.

CLASS zcl_app_logger IMPLEMENTATION.
  METHOD get_instance.
    IF go_instance IS INITIAL.
      go_instance = NEW #( ).
    ENDIF.
    ro_logger = go_instance.
  ENDMETHOD.

  METHOD info.
    append_entry( iv_severity = 'I' iv_message = iv_message ).
  ENDMETHOD.

  METHOD error.
    append_entry( iv_severity = 'E' iv_message = iv_message ).
  ENDMETHOD.

  METHOD append_entry.
    GET TIME STAMP FIELD DATA(lv_ts).
    APPEND VALUE #(
      timestamp = lv_ts
      severity  = iv_severity
      message   = iv_message ) TO mt_entries.
  ENDMETHOD.

  METHOD flush.
    " 실제로는 BAL_LOG_CREATE 등 Application Log API 호출
    LOOP AT mt_entries INTO DATA(ls_entry).
      WRITE: / ls_entry-timestamp, ls_entry-severity, ls_entry-message.
    ENDLOOP.
    CLEAR mt_entries.
  ENDMETHOD.

  METHOD get_entries.
    rt_entries = mt_entries.
  ENDMETHOD.
ENDCLASS.

호출 측에서는 다음과 같이 사용합니다.

DATA(lo_log) = zcl_app_logger=>get_instance( ).
lo_log->info( |Sales Order { lv_vbeln } processed.| ).
TRY.
    process_order( lv_vbeln ).
  CATCH zcx_order_failed INTO DATA(lx_err).
    lo_log->error( lx_err->get_text( ) ).
ENDTRY.
lo_log->flush( ).

캐시 패턴 또한 구조가 유사합니다. 내부에 HASHED TABLE을 두고 get( key ) 호출 시 캐시 미스가 발생하면 DB나 외부 시스템에서 값을 로드한 뒤 반환하는 형태로 구성합니다. 이때 캐시 무효화(invalidate) 메서드를 반드시 함께 제공해야 장기 세션에서 stale 데이터가 누적되지 않습니다.

6. ABAP Unit 테스트에서의 Singleton 모킹 전략

Singleton은 테스트하기 까다로운 패턴으로 자주 지목됩니다. 정적 속성에 상태가 보관되기 때문에 테스트 메서드 사이에 상태가 전파되어 격리가 깨지기 쉽습니다. 이를 완화하기 위해 두 가지 기법이 일반적으로 권장됩니다. 첫째, 테스트 전용 reset 메서드를 두되, 가시성을 PROTECTED 또는 friend 관계로 제한합니다. 둘째, Singleton이 의존하는 협력 객체를 외부에서 주입할 수 있는 hook 메서드를 둡니다.

CLASS zcl_app_logger DEFINITION
  PUBLIC
  FINAL
  CREATE PRIVATE
  GLOBAL FRIENDS zcl_app_logger_test.   " 테스트 전용 friend
  " ... (생략)
ENDCLASS.

CLASS zcl_app_logger_test DEFINITION FOR TESTING
  DURATION SHORT
  RISK LEVEL HARMLESS.

  PRIVATE SECTION.
    METHODS setup.
    METHODS test_info FOR TESTING.
ENDCLASS.

CLASS zcl_app_logger_test IMPLEMENTATION.
  METHOD setup.
    " friend 권한으로 정적 인스턴스를 초기화
    zcl_app_logger=>go_instance = VALUE #( ).
  ENDMETHOD.

  METHOD test_info.
    DATA(lo_logger) = zcl_app_logger=>get_instance( ).
    lo_logger->info( 'unit test message' ).
    cl_abap_unit_assert=>assert_equals(
      act = lines( lo_logger->get_entries( ) )
      exp = 1 ).
  ENDMETHOD.
ENDCLASS.

friend 선언을 활용하면 production 코드의 캡슐화를 깨지 않으면서도 테스트에서 내부 상태를 리셋할 수 있어, 테스트 간 독립성을 확보하는 데 일반적으로 효과적입니다.

7. 자주 겪는 함정 — 멀티 세션 공유 오해, 메모리 누수, 테스트 격리 실패

Singleton을 도입할 때 팀에서 빈번하게 발생하는 세 가지 문제를 정리합니다.

FAQ 1. "Singleton이니까 모든 사용자가 같은 데이터를 볼 수 있나요?"
아니요. ABAP의 Singleton은 internal session 범위에서만 유일합니다. 사용자 A의 세션과 사용자 B의 세션은 별개의 ABAP 메모리 영역을 가지며, 각자 별도의 go_instance를 보유합니다. 시스템 전역 공유가 필요하다면 SHMA(Shared Memory Area) 또는 DB 캐시 테이블을 사용하는 것이 권장됩니다.

FAQ 2. "Singleton에 캐시를 누적했는데 메모리 사용량이 계속 증가합니다."
Singleton 인스턴스의 내부 테이블은 세션이 종료될 때까지 살아 있으므로, 적절한 capacity 제한과 LRU 같은 만료 정책이 없으면 누수가 발생합니다. 주기적으로 FREE를 호출하거나, 일정 건수를 초과하면 가장 오래된 항목을 삭제하는 로직을 명시적으로 추가해야 합니다.

FAQ 3. "ABAP Unit 테스트가 단독으로는 통과하지만 전체 실행 시 실패합니다."
테스트 격리 실패의 전형적 증상입니다. 이전 테스트에서 채워둔 Singleton 상태가 다음 테스트에 그대로 남기 때문입니다. setup 또는 class_setup에서 정적 참조를 반드시 초기화하고, 가능하면 RISK LEVEL HARMLESS와 함께 friend 관계로 reset hook을 마련하는 방식이 권장됩니다.

FAQ 4. "GET_INSTANCE 호출 파라미터가 두 번째부터 무시됩니다."
의도된 동작입니다. 첫 호출 시 결정된 상태를 유지하는 것이 Singleton의 본질이므로, 호출 순서에 의존성이 생기지 않도록 초기화는 진입점(예: AT SELECTION-SCREEN 또는 main 메서드 초반)에서 일괄 수행하는 패턴이 일반적으로 안전합니다.

8. 응용 패턴 — 의존성 주입(DI)과 Singleton 조합

Singleton의 가장 큰 비판은 "전역 상태를 숨겨서 테스트와 유지보수를 어렵게 만든다"는 것입니다. 이를 해소하기 위해 Singleton을 직접 호출하지 않고 인터페이스 + DI로 추상화하는 패턴이 권장됩니다. 즉, 클라이언트 코드는 zif_logger 인터페이스에만 의존하고, Singleton은 production 환경의 한 가지 구현으로만 사용됩니다.

INTERFACE zif_logger PUBLIC.
  METHODS info  IMPORTING iv_message TYPE string.
  METHODS error IMPORTING iv_message TYPE string.
ENDINTERFACE.

CLASS zcl_order_service DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING io_logger TYPE REF TO zif_logger OPTIONAL.

    METHODS process
      IMPORTING iv_vbeln TYPE vbeln.

  PRIVATE SECTION.
    DATA mo_logger TYPE REF TO zif_logger.
ENDCLASS.

CLASS zcl_order_service IMPLEMENTATION.
  METHOD constructor.
    mo_logger = COND #(
      WHEN io_logger IS BOUND THEN io_logger
      ELSE zcl_app_logger=>get_instance( ) ).   " 기본은 Singleton
  ENDMETHOD.

  METHOD process.
    mo_logger->info( |Processing { iv_vbeln }| ).
    " ... 비즈니스 로직
  ENDMETHOD.
ENDCLASS.

테스트 코드에서는 mock logger를 주입하여 Singleton 상태를 건드리지 않고도 동작을 검증할 수 있습니다. 이렇게 하면 production에서는 Singleton의 편의성을 유지하면서도, 테스트와 확장 시점에는 인터페이스 기반 유연성을 확보할 수 있습니다. ABAP RAP나 Clean ABAP 가이드라인에서도 "전역 정적 접근을 피하고 의존성을 명시적으로 주입하라"는 원칙이 일반적으로 강조됩니다.

관련하여 함께 살펴볼 만한 주제는 다음과 같습니다. Factory Method 패턴(인스턴스 생성 책임을 별도 클래스로 분리), Service Locator 패턴(여러 서비스를 등록/조회), Shared Memory Objects(시스템 전역 캐시 구현), ABAP Unit Test Double Framework(cl_abap_testdouble를 활용한 인터페이스 자동 mock 생성), 그리고 Clean ABAP의 의존성 관리 챕터입니다. 이러한 주제들을 단계적으로 익히면 Singleton을 남용하지 않으면서도 필요한 곳에 적재적소로 적용하는 감각을 키울 수 있습니다.

댓글 0

아직 댓글이 없습니다.