ABAP

아직도 ABAP 어댑터 패턴 모른다? 핵심 3가지 #shorts #SAP #ABAP

1. 개요 및 이 글에서 다룰 것

ABAP으로 구축된 시스템은 수십 년 동안 누적된 레거시 클래스/함수 모듈을 포함합니다. 새로 도입된 신규 모듈(예: RAP 비즈니스 객체, SAP Cloud Application Studio 연동)은 표준화된 인터페이스를 요구하지만, 레거시 코드의 시그니처는 신규 인터페이스와 맞지 않습니다. 이때 레거시 코드를 한 줄도 손대지 않고 신규 인터페이스를 만족시키도록 중간에 끼워 넣는 변환 클래스가 어댑터(Adapter)입니다.

이 글에서 다루는 범위는 다음과 같습니다.

  • 어댑터 패턴의 본질과 ABAP OO에서의 위치
  • 타겟 인터페이스(INTERFACE) 선언과 어댑팅 대상(adaptee) 분리
  • CONSTRUCTOR 주입을 통한 어댑터 구현
  • NEW/CAST 연산자를 활용한 클라이언트 호출 패턴
  • 실무 환경에서 자주 만나는 트러블슈팅 포인트
적용 대상: ABAP 변경 권한이 있는 ABAP Developer / RAP 마이그레이션을 진행 중인 팀.

2. 이 글을 보기 전에

아래 내용을 알고 있다면 훨씬 매끄럽게 읽힙니다.

  • ABAP Objects 기본: CLASS, METHOD, INTERFACE 선언
  • 참조 변수와 TYPE REF TO의 의미
  • 인라인 선언(DATA(...))과 NEW, CAST 연산자 (7.40 SP08+)
  • 예외 클래스(CX_*)의 기본 사용법

객체지향 디자인 패턴(GoF) 중 구조 패턴(Structural Pattern)의 한 종류라는 정도만 알고 있어도 충분합니다.

3. 환경 / 버전 / 준비물

예제는 다음 환경에서 일반적으로 동작합니다.

  • ABAP Platform: SAP NetWeaver 7.50 이상 또는 ABAP Platform 2022/2023
  • ABAP 문법 레벨: 7.40 SP08 이상 (NEW, CAST, 인라인 선언 사용)
  • IDE: ABAP Development Tools for Eclipse(ADT) 권장. SE80도 가능하나 인라인 선언 디버깅이 다소 불편합니다.
  • 권한: S_DEVELOP 권한과 임시 패키지/로컬 객체($TMP) 권한
  • 테스트 도구: ABAP Unit (선택), 콘솔 출력은 cl_demo_output 사용

예제는 모두 로컬 클래스(lcl_*) 와 로컬 인터페이스(lif_*) 로 구성되어 단일 리포트(REPORT z_demo_adapter)에 붙여 넣고 바로 실행할 수 있도록 작성했습니다. 운영 코드로 옮길 때는 글로벌 클래스(ZCL_*) 와 글로벌 인터페이스(ZIF_*) 로 승격하는 것을 권장합니다.

4. 핵심 개념

어댑터 패턴은 비유하자면 전기 변환 플러그와 같습니다. 한국형 220V 플러그(레거시)를 유럽형 콘센트(신규 시스템)에 꽂으려면, 두 모양 사이를 변환해 주는 어댑터가 필요합니다. 양쪽 기기 모두 그대로 두고, 사이에 변환 장치만 끼우는 것이죠.

4.1 등장 인물

  • Target(타겟 인터페이스): 클라이언트가 호출하고 싶어 하는 표준 인터페이스 (lif_data_provider)
  • Adaptee(레거시): 이미 존재하지만 시그니처가 다른 기존 클래스 (lcl_legacy_system)
  • Adapter(어댑터): 타겟 인터페이스를 구현(INTERFACES)하면서 내부적으로 Adaptee를 호출/변환하는 클래스 (lcl_legacy_adapter)
  • Client(클라이언트): 타겟 인터페이스 타입으로만 코드를 작성하는 호출자

4.2 도식

Client ──> lif_data_provider (Target)
               ▲
               │ implements
               │
        lcl_legacy_adapter ──has──▶ lcl_legacy_system (Adaptee)

4.3 왜 상속이 아니라 합성(Composition)인가

ABAP에서도 단일 상속만 허용되며, 레거시 클래스가 이미 다른 상위 클래스를 상속하고 있다면 클래스 어댑터(상속 기반)는 불가능합니다. 또한 레거시를 상속하면 변경에 강하게 결합되어, 레거시 시그니처가 바뀔 때마다 어댑터가 깨지기 쉽습니다. 따라서 ABAP에서는 일반적으로 객체 어댑터(합성 기반)가 권장됩니다.

4.4 책임 경계

어댑터는 "형식 변환"에만 집중해야 하며, 비즈니스 로직을 새로 추가해서는 안 됩니다. 단위 변환, 필드 매핑, 예외 변환, 인코딩 변환 정도가 어댑터의 정상적인 책임 범위입니다.

5. 실전 코드 3단계

5.1 1단계 — 레거시 클래스와 타겟 인터페이스 선언

먼저 손댈 수 없는 레거시 클래스 lcl_legacy_system이 있다고 가정합니다. 이 클래스는 ABAP 7.0 시절에 작성되어 get_data_old라는 비표준 이름과 STRING 반환 시그니처를 가지고 있습니다.

REPORT z_demo_adapter.

*&---------------------------------------------------------------------*
*& 1) Adaptee: 수정 불가능한 레거시 클래스
*&---------------------------------------------------------------------*
CLASS lcl_legacy_system DEFINITION FINAL.
  PUBLIC SECTION.
    METHODS get_data_old
      IMPORTING iv_customer_no TYPE c LENGTH 10
      RETURNING VALUE(rv_csv)  TYPE string
      RAISING   cx_sy_conversion_error.
ENDCLASS.

CLASS lcl_legacy_system IMPLEMENTATION.
  METHOD get_data_old.
    " 실제로는 RFC, DB Select 등 수행한다고 가정
    rv_csv = |{ iv_customer_no };HONG GILDONG;SEOUL;ACTIVE|.
  ENDMETHOD.
ENDCLASS.

*&---------------------------------------------------------------------*
*& 2) Target Interface: 신규 시스템이 요구하는 표준 시그니처
*&---------------------------------------------------------------------*
INTERFACE lif_data_provider.
  TYPES:
    BEGIN OF ty_customer,
      id     TYPE string,
      name   TYPE string,
      city   TYPE string,
      status TYPE string,
    END OF ty_customer.

  METHODS fetch
    IMPORTING iv_id            TYPE string
    RETURNING VALUE(rs_result) TYPE ty_customer
    RAISING   cx_static_check.
ENDINTERFACE.

레거시는 STRING 한 줄을 세미콜론으로 구분해서 돌려주지만, 신규 인터페이스는 구조체(structure) 형태를 요구한다는 점이 핵심 포인트입니다. 두 형식 사이의 변환이 곧 어댑터의 일거리가 됩니다.

5.2 2단계 — 어댑터 클래스 작성 (생성자 주입 + 예외 변환 + 로깅)

이제 lif_data_provider를 구현하면서 내부적으로 lcl_legacy_system을 호출하는 어댑터를 만듭니다. 운영 환경에서는 예외를 의미 있는 어플리케이션 예외로 다시 던지고, 호출 로그를 남기는 것이 일반적입니다.

*&---------------------------------------------------------------------*
*& 어플리케이션 예외 (예시)
*&---------------------------------------------------------------------*
CLASS cx_data_provider_error DEFINITION
      INHERITING FROM cx_static_check FINAL.
  PUBLIC SECTION.
    DATA mv_reason TYPE string READ-ONLY.
    METHODS constructor
      IMPORTING iv_reason   TYPE string OPTIONAL
                previous    LIKE previous OPTIONAL.
ENDCLASS.

CLASS cx_data_provider_error IMPLEMENTATION.
  METHOD constructor.
    super->constructor( previous = previous ).
    mv_reason = iv_reason.
  ENDMETHOD.
ENDCLASS.

*&---------------------------------------------------------------------*
*& 3) Adapter
*&---------------------------------------------------------------------*
CLASS lcl_legacy_adapter DEFINITION FINAL.
  PUBLIC SECTION.
    INTERFACES lif_data_provider.

    METHODS constructor
      IMPORTING io_legacy TYPE REF TO lcl_legacy_system.

  PRIVATE SECTION.
    DATA mo_legacy TYPE REF TO lcl_legacy_system.

    METHODS parse_csv
      IMPORTING iv_csv           TYPE string
      RETURNING VALUE(rs_result) TYPE lif_data_provider=>ty_customer.
ENDCLASS.

CLASS lcl_legacy_adapter IMPLEMENTATION.
  METHOD constructor.
    mo_legacy = io_legacy.
  ENDMETHOD.

  METHOD lif_data_provider~fetch.
    DATA(lv_id_10) = CONV c10( iv_id ).

    TRY.
        DATA(lv_csv) = mo_legacy->get_data_old( iv_customer_no = lv_id_10 ).
      CATCH cx_sy_conversion_error INTO DATA(lx_prev).
        " 로깅 (운영에서는 BAL_LOG_ 사용 권장)
        cl_demo_output=>write(
          |[ADAPTER][ERROR] legacy call failed for id={ iv_id }| ).
        RAISE EXCEPTION TYPE cx_data_provider_error
          EXPORTING iv_reason = |Legacy conversion failed: { lx_prev->get_text( ) }|
                    previous  = lx_prev.
    ENDTRY.

    rs_result = parse_csv( lv_csv ).
  ENDMETHOD.

  METHOD parse_csv.
    SPLIT iv_csv AT ';' INTO rs_result-id
                              rs_result-name
                              rs_result-city
                              rs_result-status.
  ENDMETHOD.
ENDCLASS.

핵심은 세 가지입니다. 첫째, 인터페이스 메서드는 lif_data_provider~fetch 형식으로 구현합니다. 둘째, 생성자 주입(Constructor Injection)으로 레거시 인스턴스를 외부에서 받기 때문에 테스트 시 mock 객체로 쉽게 교체할 수 있습니다. 셋째, 레거시 예외를 어플리케이션 예외로 변환해서 클라이언트가 레거시 구현 세부사항(cx_sy_conversion_error)에 결합되지 않도록 합니다.

5.3 3단계 — 클라이언트 호출 및 프로덕션 고려사항

클라이언트는 어댑터의 존재를 모르고 오직 lif_data_provider만 바라봅니다. 이렇게 하면 나중에 레거시를 신규 RAP 구현으로 교체하더라도 클라이언트 코드는 단 한 줄도 바뀌지 않습니다.

*&---------------------------------------------------------------------*
*& 4) Client
*&---------------------------------------------------------------------*
CLASS lcl_app DEFINITION FINAL.
  PUBLIC SECTION.
    CLASS-METHODS main.

    " 의존성을 인터페이스 타입으로만 주입받음
    METHODS process
      IMPORTING io_provider TYPE REF TO lif_data_provider
                iv_id       TYPE string.
ENDCLASS.

CLASS lcl_app IMPLEMENTATION.
  METHOD main.
    " 1) Adaptee 생성
    DATA(lo_legacy) = NEW lcl_legacy_system( ).

    " 2) Adapter로 감싸기 — CAST로 타겟 인터페이스 참조 획득
    DATA(lo_provider) = CAST lif_data_provider(
                          NEW lcl_legacy_adapter( io_legacy = lo_legacy ) ).

    " 3) 클라이언트는 인터페이스만 본다
    DATA(lo_app) = NEW lcl_app( ).
    lo_app->process( io_provider = lo_provider
                     iv_id       = '0000004711' ).
  ENDMETHOD.

  METHOD process.
    TRY.
        DATA(ls_customer) = io_provider->fetch( iv_id ).
        cl_demo_output=>display( ls_customer ).
      CATCH cx_data_provider_error INTO DATA(lx).
        cl_demo_output=>display( |Failed: { lx->mv_reason }| ).
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  lcl_app=>main( ).

프로덕션 관점에서 다음을 추가로 고려합니다.

  • 성능: 어댑터는 호출당 인스턴스를 새로 만들지 말고, 가능하면 팩토리에서 싱글톤으로 캐싱하세요. 다만 상태를 가진다면 멀티 세션 충돌에 주의합니다.
  • 테스트: ABAP Unit에서 lcl_legacy_system을 직접 인스턴스화하기 어렵다면, 어댑터의 의존성을 인터페이스로 한 단계 더 추출하고 테스트 더블(test double)을 주입합니다.
  • 보안: 레거시가 권한 체크를 하지 않는 경우, 어댑터 진입 지점에서 AUTHORITY-CHECK를 수행하여 신규 시스템의 권한 정책과 일치시킵니다.
  • 로깅: cl_demo_output 대신 운영에서는 Application Log(BAL_LOG_*) 또는 NW 추적 로그 사용을 권장합니다.

6. 흔한 실수 / 트러블슈팅

FAQ 1. 어댑터에서 비즈니스 규칙을 같이 처리해도 되나요?

권장하지 않습니다. 어댑터에 비즈니스 로직이 섞이면 "Adapter"가 아니라 사실상 "Facade + Service"가 되어 책임이 흐려집니다. 변환/매핑만 어댑터에 두고, 비즈니스 규칙은 별도 서비스 클래스로 분리하세요.

FAQ 2. CAST lif_data_provider( ... )에서 CX_SY_MOVE_CAST_ERROR가 발생합니다.

대부분 어댑터 클래스에 INTERFACES lif_data_provider. 선언이 누락되었거나, 인터페이스 메서드를 lif_data_provider~fetch 형태로 구현하지 않았을 때 발생합니다. 클래스 정의의 PUBLIC SECTION에서 INTERFACES가 있는지 먼저 확인합니다.

FAQ 3. 레거시가 Function Module(예: BAPI_*)이라면 어떻게 감싸나요?

어댑터의 fetch 내부에서 CALL FUNCTION 'BAPI_...'을 호출하면 됩니다. 이때 BAPI의 RETURN 테이블을 검사해서 E/A 메시지를 어플리케이션 예외로 변환하는 것을 잊지 마세요.

FAQ 4. 한 어댑터가 여러 타겟 인터페이스를 구현해도 되나요?

가능하지만 권장되지 않습니다. ABAP 클래스는 다중 인터페이스 구현이 허용되지만, 한 어댑터가 너무 많은 책임을 가지면 SRP(단일 책임 원칙)를 어기게 됩니다. 인터페이스마다 별도 어댑터를 두고, 필요하면 그것들을 묶는 Facade를 추가하세요.

FAQ 5. NEW lcl_legacy_adapter( lo_legacy )처럼 위치 파라미터를 써도 되나요?

가능하지만 가독성을 위해 NEW lcl_legacy_adapter( io_legacy = lo_legacy )처럼 명명 파라미터 사용을 일반적으로 권장합니다. 특히 어댑터의 생성자 시그니처가 향후 늘어날 가능성이 높기 때문입니다.

7. 다음 단계 / 관련 주제

  • Facade 패턴: 여러 레거시 서브시스템을 하나의 단순한 인터페이스로 묶어 노출
  • Strategy 패턴: 런타임에 어댑터(또는 신규 구현)를 동적으로 선택
  • Decorator 패턴: 어댑터에 캐싱/로깅을 비침습적으로 추가
  • RAP Behavior Implementation: 어댑터로 감싼 레거시를 RAP 비즈니스 객체의 데이터 소스로 활용
  • Test Double / ABAP Unit: 어댑터 단위 테스트와 의존성 격리
  • Dependency Injection 프레임워크: 직접 NEW 대신 팩토리/DI 컨테이너로 어댑터 인스턴스 관리

8. 참고 자료 / 핵심 한 줄

핵심 한 줄: 어댑터는 "레거시는 그대로, 클라이언트는 표준대로"를 동시에 만족시키기 위해 사이에 끼우는 변환 클래스다.

댓글 0

아직 댓글이 없습니다.