ABAP

DB 직접 호출하면 큰일 — ABAP DAC 패턴 3단계 #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 및 이 글에서 다룰 것

ABAP 개발에서 가장 흔한 문제 중 하나는 비즈니스 로직과 DB 액세스 코드가 한 클래스 안에 뒤섞이는 것입니다. 이런 코드는 단위 테스트가 불가능하고, 테이블 구조가 바뀌면 곳곳을 수정해야 하며, 동일 SELECT가 여러 곳에서 중복됩니다. DAC(Data Access Class) 패턴은 DB 접근을 별도의 클래스로 격리하여 비즈니스 로직을 순수 ABAP 로직으로 유지하도록 돕는 설계 기법입니다.

이 글에서 다룰 것 체크리스트
  • DAC 인터페이스(lif_material_dao) 정의 방법
  • DAC 구현 클래스(lcl_material_dao)로 SELECT 격리
  • 서비스 클래스에 생성자 주입(Constructor Injection)으로 DAC 연결
  • ABAP Unit 테스트용 페이크 DAC 구현
  • 흔한 실수와 트러블슈팅 FAQ

이 글을 보기 전에

이 글은 ABAP OO(클래스, 인터페이스, REF TO) 기본 문법에 익숙한 개발자를 대상으로 합니다. INTERFACE 선언, INTERFACES 키워드로 인터페이스 구현, NEW 연산자, ABAP Unit 기초 정도를 알고 있으면 충분합니다. SAP S/4HANA, NetWeaver 7.40 이상 등 ABAP 7.40+ 환경을 가정합니다.

환경 / 버전 / 준비물

  • SAP 시스템: SAP NetWeaver 7.40 SP08 이상 또는 SAP S/4HANA 1909 이상 (인라인 선언, NEW 연산자 사용)
  • 개발 도구: ABAP Development Tools (ADT) for Eclipse 권장. SE80도 동작하지만 Local Class 편집은 ADT가 편리합니다.
  • 테이블: 표준 MARA 테이블 (자재 마스터). 권한이 없다면 사용자 정의 Z 테이블로 대체 가능
  • 패키지: 로컬 객체 $TMP 또는 전용 개발 패키지
  • ABAP Unit: 표준 내장 테스트 프레임워크 사용

이 글의 코드는 로컬 클래스(lcl_, lif_) 형태로 작성되며, 실무에서는 글로벌 클래스(SE24 또는 ADT의 zcl_*)로 옮겨 재사용성을 높이는 것이 일반적입니다.

핵심 개념

DAC 패턴이란?

DAC(Data Access Class)는 일반적으로 OOP 세계에서 DAO(Data Access Object)로 불리는 패턴의 ABAP 버전입니다. 핵심 아이디어는 단순합니다.

"DB에 닿는 모든 코드(SELECT, INSERT, UPDATE, MODIFY, DELETE)를 전용 클래스 안에 가둔다."

즉, 비즈니스 로직 클래스는 "어떤 테이블의 어떤 필드를 어떻게 조회하는가"를 몰라야 하며, 단지 get_by_id( matnr ) 같은 의미 있는 메서드만 호출합니다. 그리고 그 메서드의 계약은 인터페이스로 정의됩니다.

왜 필요한가

  • 테스트 가능성: 실제 DB에 접근하지 않는 페이크/모크 구현체를 주입해 단위 테스트를 작성할 수 있습니다.
  • 관심사 분리(SoC): 가격 계산 로직과 DB 접근 로직이 한 메서드에 섞이지 않습니다.
  • 변경 격리: 테이블이 CDS View로 바뀌거나 HANA Calculation View로 교체되어도 DAC 한 곳만 수정하면 됩니다.
  • 중복 제거: 같은 SELECT를 여러 프로그램에서 반복하지 않게 됩니다.

구조 도식

* 의존성 흐름 (화살표 = "사용함")
*
*   [lcl_price_service]  ----uses---->  [lif_material_dao] (인터페이스)
*                                              ^
*                                              | implements
*                                              |
*                                       [lcl_material_dao]  ----SELECT---->  MARA

서비스는 인터페이스에만 의존합니다. 구현체는 외부(테스트 코드, 메인 프로그램)에서 주입합니다. 이것이 의존성 역전(DIP)의 ABAP식 적용입니다.

실전 코드 3단계

1단계 — 기본 예제: DAC 인터페이스와 구현 분리

먼저 DB 접근의 "계약"을 인터페이스로 선언합니다. 그리고 그 계약을 구현한 클래스가 실제 SELECT를 수행합니다.

INTERFACE lif_material_dao.
  METHODS get_by_id
    IMPORTING iv_matnr       TYPE matnr
    RETURNING VALUE(rs_mara) TYPE mara
    RAISING   cx_static_check.
ENDINTERFACE.

CLASS lcl_material_dao DEFINITION.
  PUBLIC SECTION.
    INTERFACES lif_material_dao.
ENDCLASS.

CLASS lcl_material_dao IMPLEMENTATION.
  METHOD lif_material_dao~get_by_id.
    SELECT SINGLE *
      FROM mara
      WHERE matnr = @iv_matnr
      INTO @rs_mara.
    IF sy-subrc <> 0.
      RAISE EXCEPTION TYPE cx_static_check.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_price_service DEFINITION.
  PUBLIC SECTION.
    METHODS constructor
      IMPORTING io_dao TYPE REF TO lif_material_dao.
    METHODS get_price
      IMPORTING iv_matnr        TYPE matnr
      RETURNING VALUE(rv_price) TYPE p LENGTH 9 DECIMALS 2.
  PRIVATE SECTION.
    DATA mo_dao TYPE REF TO lif_material_dao.
ENDCLASS.

CLASS lcl_price_service IMPLEMENTATION.
  METHOD constructor.
    mo_dao = io_dao.
  ENDMETHOD.
  METHOD get_price.
    DATA(ls_mat) = mo_dao->get_by_id( iv_matnr ).
    rv_price = ls_mat-verp1.
  ENDMETHOD.
ENDCLASS.

여기서 주목할 점은 lcl_price_service의 어디에도 SELECT 키워드가 없다는 것입니다. 서비스는 "어떻게 가져오는지"를 모르고, "무엇을 가져오는지"만 압니다. 메인 프로그램에서는 다음과 같이 조립합니다.

START-OF-SELECTION.
  DATA(lo_dao)     = NEW lcl_material_dao( ).
  DATA(lo_service) = NEW lcl_price_service( lo_dao ).
  TRY.
      DATA(lv_price) = lo_service->get_price( '000000000000000023' ).
      WRITE: / 'Price:', lv_price.
    CATCH cx_static_check.
      WRITE: / 'Material not found'.
  ENDTRY.

2단계 — 실무 시나리오: 전용 예외와 로깅 추가

cx_static_check를 그대로 던지는 것은 의미가 모호하므로, 도메인 예외 클래스를 도입하고 로깅을 추가합니다.

CLASS lcx_material_not_found DEFINITION
    INHERITING FROM cx_static_check.
  PUBLIC SECTION.
    DATA mv_matnr TYPE matnr READ-ONLY.
    METHODS constructor
      IMPORTING iv_matnr TYPE matnr OPTIONAL.
ENDCLASS.

CLASS lcx_material_not_found IMPLEMENTATION.
  METHOD constructor.
    super->constructor( ).
    mv_matnr = iv_matnr.
  ENDMETHOD.
ENDCLASS.

INTERFACE lif_material_dao.
  METHODS get_by_id
    IMPORTING iv_matnr       TYPE matnr
    RETURNING VALUE(rs_mara) TYPE mara
    RAISING   lcx_material_not_found.
ENDINTERFACE.

CLASS lcl_material_dao IMPLEMENTATION.
  METHOD lif_material_dao~get_by_id.
    SELECT SINGLE *
      FROM mara
      WHERE matnr = @iv_matnr
      INTO @rs_mara.
    IF sy-subrc <> 0.
      " 표준 Application Log 또는 BAL 로 대체 가능
      MESSAGE i001(zdac) WITH iv_matnr INTO DATA(lv_dummy).
      RAISE EXCEPTION NEW lcx_material_not_found( iv_matnr = iv_matnr ).
    ENDIF.
  ENDMETHOD.
ENDCLASS.

이렇게 하면 호출 측에서 어떤 자재가 없었는지 lo_ex->mv_matnr로 즉시 알 수 있습니다. 또한 SELECT 필드를 모두 가져오는 SELECT * 대신, 실무에서는 필요한 필드만 명시적으로 선언하는 것이 일반적으로 권장됩니다.

METHOD lif_material_dao~get_by_id.
  SELECT SINGLE matnr, mtart, matkl, meins
    FROM mara
    WHERE matnr = @iv_matnr
    INTO CORRESPONDING FIELDS OF @rs_mara.
  ...
ENDMETHOD.

3단계 — 프로덕션: 단위 테스트와 페이크 DAC

DAC 패턴의 가장 큰 보상은 단위 테스트입니다. 실제 DB에 의존하지 않는 페이크 구현체를 만들어 서비스 로직만 검증합니다.

CLASS ltc_price_service DEFINITION FINAL FOR TESTING
    DURATION SHORT
    RISK LEVEL HARMLESS.

  PRIVATE SECTION.
    " 페이크 DAC: 미리 정해진 값을 반환
    CLASS lcl_fake_dao DEFINITION.
      PUBLIC SECTION.
        INTERFACES lif_material_dao.
        DATA mv_price TYPE p LENGTH 9 DECIMALS 2.
    ENDCLASS.

    METHODS price_returned_correctly FOR TESTING RAISING cx_static_check.
    METHODS not_found_raises          FOR TESTING.
ENDCLASS.

CLASS ltc_price_service=>lcl_fake_dao IMPLEMENTATION.
  METHOD lif_material_dao~get_by_id.
    rs_mara-matnr = iv_matnr.
    rs_mara-verp1 = mv_price.
  ENDMETHOD.
ENDCLASS.

CLASS ltc_price_service IMPLEMENTATION.
  METHOD price_returned_correctly.
    DATA(lo_fake) = NEW lcl_fake_dao( ).
    lo_fake->mv_price = '199.90'.

    DATA(lo_service) = NEW lcl_price_service( lo_fake ).

    cl_abap_unit_assert=>assert_equals(
      exp = CONV p( '199.90' )
      act = lo_service->get_price( '000000000000000023' ) ).
  ENDMETHOD.

  METHOD not_found_raises.
    " 예외 케이스 검증: 실제로는 raising 페이크를 별도로 작성
    " 여기서는 패턴 시연 목적
  ENDMETHOD.
ENDCLASS.

실제 DB를 띄우지 않고 0.01초 이내에 수십 개의 가격 시나리오를 검증할 수 있게 됩니다. 보안/성능 관점에서도 다음 사항을 권장합니다.

  • SQL 인젝션 방지: 호스트 변수(@iv_matnr)를 반드시 사용하고, 동적 WHERE는 화이트리스트 검증 후 사용
  • 인덱스 활용: WHERE 절은 기본키 또는 인덱스 컬럼 우선
  • 벌크 조회: FOR ALL ENTRIES 또는 SELECT ... FROM ... IN @itab로 N+1 문제 회피
  • 권한 체크: 필요 시 DAC 메서드 진입부에 AUTHORITY-CHECK 배치

흔한 실수 / 트러블슈팅

Q1. 서비스 클래스 안에서 NEW lcl_material_dao( )를 직접 만들고 있어요.

이는 DAC 패턴의 의미를 무력화합니다. 생성자에서 인터페이스 타입으로 주입받아야 테스트 시 페이크로 교체할 수 있습니다. 서비스 내부에서 직접 NEW를 호출하면 결합도가 높아져 단위 테스트가 다시 어려워집니다.

Q2. 인터페이스 메서드에서 RAISING cx_static_check를 쓰면 호출부가 지저분해져요.

가능하면 도메인 전용 예외(예: lcx_material_not_found)를 정의해 인터페이스 시그니처에 명시하세요. 호출부에서 의미 있는 CATCH가 가능해지고, "어떤 일이 잘못될 수 있는가"가 코드로 문서화됩니다.

Q3. ABAP Unit에서 페이크 DAC를 만들 때 인터페이스 메서드 구현을 깜빡합니다.

로컬 테스트 클래스에 중첩 클래스로 페이크를 정의했다면 CLASS ltc_xxx=>lcl_fake_dao IMPLEMENTATION 형태로 구현부를 반드시 분리해 작성해야 합니다. 컴파일 에러가 나면 인터페이스의 모든 메서드가 구현되었는지 점검하세요.

Q4. DAC가 너무 비대해져요. 한 테이블당 한 클래스인가요?

일반적으로 한 애그리거트(Aggregate) 단위로 묶는 것이 권장됩니다. 예를 들어 MARA + MAKT + MARC를 함께 다루는 material_dao가 자연스럽습니다. 단, 메서드가 20개를 넘어가면 책임을 분리할 시점이라는 신호입니다.

Q5. 글로벌 클래스로 옮길 때 인터페이스도 글로벌이어야 하나요?

네, 일반적으로 인터페이스(ZIF_MATERIAL_DAO)와 구현(ZCL_MATERIAL_DAO)을 모두 글로벌로 만들고, 패키지 인터페이스로 외부 공개 범위를 제어합니다.

다음 단계 / 관련 주제

  • Repository 패턴: DAC를 확장해 INSERT/UPDATE/DELETE 메타정보와 트랜잭션 경계를 함께 다루는 한 단계 상위 패턴
  • RAP(RESTful ABAP Programming Model): S/4HANA에서 BO 단위로 DB 접근을 추상화하는 공식 프레임워크. DAC 사고방식이 그대로 통용됩니다.
  • CDS View 기반 DAC: FROM mara 대신 CDS View로 교체해 HANA 최적화 코드를 한 곳에서 관리
  • ABAP Test Double Framework: 페이크 대신 테스트 더블 프레임워크로 더 풍부한 모킹
  • SOLID 원칙: DAC는 DIP(의존성 역전)와 SRP(단일 책임)의 ABAP식 실천 사례

참고 자료 & 핵심 한 줄

핵심 한 줄: 비즈니스 로직이 SELECT를 직접 모르게 만들면, 그 코드는 테스트 가능하고 변경에 강해진다 — 그것이 DAC 패턴이다.

댓글 0

아직 댓글이 없습니다.