1. 개요 및 이 글에서 다룰 것
의존성 주입(Dependency Injection, DI)은 객체가 필요한 협력자(의존 객체)를 내부에서 직접 생성하지 않고 외부에서 받아 사용하도록 설계하는 패턴입니다. ABAP OO에서 DI는 단순한 트렌드가 아니라, RFC/DB/외부 API에 강하게 묶인 레거시 클래스를 단위 테스트 가능한 구조로 바꾸기 위한 핵심 도구입니다. 환율 조회 로직이 클래스 안에 박혀 있으면 운영 시스템에 붙지 않으면 테스트조차 못 하지만, 인터페이스 한 장만 끼워 넣으면 Mock으로 즉시 검증할 수 있습니다.
이 글에서 다룰 것
- 의존성 역전 원칙(DIP)과 결합도(coupling)의 관계
- ABAP INTERFACE로 협력자 추상화하기
- 생성자 주입(Constructor Injection) 코드 패턴
- 운영 구현체 / Mock 구현체 분리 작성
- ABAP Unit과 연계한 테스트 작성 흐름
- Test Double Framework, RAP 테스트로 가는 다음 단계
2. 이 글을 보기 전에
아래 내용을 미리 익히고 보시는 것을 권장합니다.
- ABAP Objects 기본:
CLASS,METHOD,NEW연산자,REF TO참조 변수 INTERFACE선언과INTERFACES를 통한 구현,~연산자로 인터페이스 메서드 호출- 생성자(
constructor) 메서드와IMPORTING파라미터 - ABAP Unit(
CLASS ... FOR TESTING)의 기본 구조
3. 환경 / 버전 / 준비물
이 글의 예제는 다음 환경에서 일반적으로 동작하도록 작성되었습니다.
- NetWeaver AS ABAP 7.50 이상 또는 SAP S/4HANA on-premise / Private Cloud — inline declaration(
DATA(...)),NEW연산자 사용을 위해 7.40 SP08 이상이 필요합니다. - ABAP Cloud / Steampunk(BTP ABAP Environment) — 동일한 패턴이 적용되며, RFC 대신 Communication Arrangement 기반 클라이언트가 협력자가 됩니다.
- 개발 도구: ABAP Development Tools(ADT) for Eclipse 권장. SE80에서도 가능하나 ABAP Unit 실행 UX는 ADT가 편리합니다.
- 예제 클래스는 로컬 클래스(
lcl_)로 작성합니다. 글로벌 클래스(ZCL_,ZIF_)로 옮길 때는 가시성과 패키지 분리만 추가로 신경 쓰면 됩니다.
4. 핵심 개념
결합도(Coupling)가 무엇을 망가뜨리는가
가격 계산 서비스 안에서 RFC 함수 모듈을 CALL FUNCTION 'Z_GET_RATE' DESTINATION 'EXT'로 직접 호출한다고 가정해 봅시다. 이 서비스는 RFC Destination이 살아 있는 시스템에서만 동작하고, 환율 API 응답 형식이 바뀌면 가격 로직까지 같이 깨집니다. 구현 세부사항(어디서 어떻게 환율을 가져오는가)이 비즈니스 정책(환율을 곱해서 원화를 계산한다)과 한 덩어리로 묶인 상태입니다.
의존성 역전 원칙(DIP)
Robert C. Martin의 DIP는 두 가지를 말합니다.
- 상위 정책(가격 계산)은 하위 세부사항(RFC 호출)에 의존해서는 안 된다.
- 둘 다 추상(Abstraction)에 의존해야 한다.
ABAP에서 이 추상은 INTERFACE가 담당합니다. 가격 서비스는 "환율을 돌려주는 누군가"가 필요할 뿐이며, 그 누군가가 RFC인지, DB 테이블인지, 테스트용 상수인지 알 필요가 없습니다.
DI를 콘센트로 비유하기
가전제품(가격 서비스)은 콘센트(인터페이스) 규격만 맞으면 동작합니다. 발전소(RFC 운영)든 보조 배터리(Mock)든 콘센트 모양만 같으면 꽂아서 쓸 수 있죠. 가전제품을 분해해 발전기를 내장하는 순간 그 가전제품은 이동도, 테스트도 불가능해집니다.
주입 방식 세 가지
- 생성자 주입(Constructor Injection): 객체 생성 시점에 협력자를 넘긴다. 가장 권장되며, 불변(immutable)하게 유지하기 좋습니다.
- 세터 주입(Setter Injection):
set_provider( )같은 메서드로 나중에 주입. 선택적 의존성에 적합합니다. - 인터페이스 주입: 잘 쓰이지 않으며 ABAP에서는 보통 생성자 주입으로 충분합니다.
5. 실전 코드 3단계
1단계: 인터페이스 선언과 서비스 골격
먼저 "환율 제공자"라는 추상을 인터페이스로 분리합니다. 가격 서비스는 이 인터페이스에만 의존하므로, 향후 구현이 RFC에서 OData로 바뀌어도 서비스 코드는 손대지 않습니다.
INTERFACE lif_rate_provider.
METHODS get_rate
IMPORTING iv_currency TYPE waers
RETURNING VALUE(rv_rate) TYPE p LENGTH 9 DECIMALS 4.
ENDINTERFACE.
CLASS lcl_price_service DEFINITION.
PUBLIC SECTION.
METHODS constructor
IMPORTING io_provider TYPE REF TO lif_rate_provider.
METHODS calc_krw
IMPORTING iv_amount TYPE p LENGTH 9 DECIMALS 2
iv_currency TYPE waers
RETURNING VALUE(rv_krw) TYPE p LENGTH 11 DECIMALS 2.
PRIVATE SECTION.
DATA mo_provider TYPE REF TO lif_rate_provider.
ENDCLASS.
CLASS lcl_price_service IMPLEMENTATION.
METHOD constructor.
mo_provider = io_provider.
ENDMETHOD.
METHOD calc_krw.
rv_krw = iv_amount * mo_provider->get_rate( iv_currency ).
ENDMETHOD.
ENDCLASS.핵심은 mo_provider가 TYPE REF TO lif_rate_provider라는 점입니다. 구체 클래스가 아니라 인터페이스 참조이므로, 같은 인터페이스를 구현한 어떤 객체든 받아들일 수 있습니다.
2단계: 운영 구현체와 Mock 구현체
같은 lif_rate_provider를 구현하는 두 클래스를 만듭니다. 운영용은 실제 외부 시스템을 호출하고, 테스트용 Mock은 미리 정해진 값을 돌려줍니다.
CLASS lcl_rfc_rate DEFINITION.
PUBLIC SECTION.
INTERFACES lif_rate_provider.
ENDCLASS.
CLASS lcl_rfc_rate IMPLEMENTATION.
METHOD lif_rate_provider~get_rate.
" RFC 호출로 실제 환율 조회
rv_rate = '1300.0000'.
ENDMETHOD.
ENDCLASS.
CLASS lcl_mock_rate DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES lif_rate_provider.
DATA mv_rate TYPE p LENGTH 9 DECIMALS 4.
ENDCLASS.
CLASS lcl_mock_rate IMPLEMENTATION.
METHOD lif_rate_provider~get_rate.
rv_rate = mv_rate.
ENDMETHOD.
ENDCLASS.실무에서는 lcl_rfc_rate~get_rate 안에 CALL FUNCTION ... DESTINATION ..., 예외 처리, 캐시, 로깅(cl_bali_log 또는 Application Log)이 들어갑니다. 중요한 것은 이 모든 복잡도가 가격 서비스로 새어 나가지 않는다는 점입니다. lcl_mock_rate의 FOR TESTING은 운영 클래스 풀에 끼어 들어가지 않도록 보장합니다.
3단계: 생성자 주입으로 운영과 테스트 분리
이제 동일한 lcl_price_service를 두 시나리오에서 그대로 사용합니다. 차이는 "무엇을 주입했는가"뿐입니다.
* 운영: 실제 RFC 주입
DATA(lo_svc) = NEW lcl_price_service(
io_provider = NEW lcl_rfc_rate( ) ).
DATA(lv_krw) = lo_svc->calc_krw( iv_amount = '10' iv_currency = 'USD' ).
* 테스트: Mock 주입
DATA(lo_mock) = NEW lcl_mock_rate( ).
lo_mock->mv_rate = '1300'.
DATA(lo_t) = NEW lcl_price_service( io_provider = lo_mock ).
DATA(lv_result) = lo_t->calc_krw( iv_amount = '10' iv_currency = 'USD' ).ABAP Unit 테스트로 감싸면 다음과 같은 모양이 됩니다.
CLASS ltc_price_service DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS calc_krw_with_mock FOR TESTING.
ENDCLASS.
CLASS ltc_price_service IMPLEMENTATION.
METHOD calc_krw_with_mock.
DATA(lo_mock) = NEW lcl_mock_rate( ).
lo_mock->mv_rate = '1300'.
DATA(lo_cut) = NEW lcl_price_service( io_provider = lo_mock ).
DATA(lv_result) = lo_cut->calc_krw(
iv_amount = '10' iv_currency = 'USD' ).
cl_abap_unit_assert=>assert_equals(
exp = CONV #( '13000.00' ) act = lv_result ).
ENDMETHOD.
ENDCLASS.이 테스트는 RFC Destination, 네트워크, 환율 API 응답 형식 어디에도 의존하지 않습니다. 오로지 "환율 1300일 때 10 USD는 13000 KRW가 되어야 한다"는 정책만 검증합니다. 빌드 파이프라인(gCTS, abapGit + CI)에서 수십 밀리초 안에 끝나며, 회귀 방지의 1차 방어선이 됩니다.
프로덕션 보강 포인트
- 예외 표준화: 인터페이스에
RAISING zcx_rate_error를 선언해 운영/Mock 양쪽이 동일한 예외 계약을 따르도록 합니다. - 팩토리 분리: 운영 측
NEW lcl_rfc_rate( )가 여러 곳에 흩어지면lcl_provider_factory=>create( )로 중앙화합니다. - 보안: 환율 API 호출 시 credential은
Communication Arrangement나SSF로 관리하고, 클래스 내부에 하드코딩하지 않습니다. - 성능: 동일 요청 내 반복 호출은 구현체 내부 캐시(
lt_cache또는cl_abap_memory_area)로 줄입니다. 서비스 계층에 캐시 로직을 넣으면 안 됩니다 — 책임이 흐려집니다.
6. 흔한 실수 / 트러블슈팅
FAQ 1. 생성자에서 NEW lcl_rfc_rate( )를 기본값으로 두면 안 되나요?
편의를 위해 자주 시도하지만, 그 순간 가격 서비스가 다시 RFC 구현체에 컴파일 타임 의존성을 갖게 됩니다. 테스트 시 Mock을 주입해도 코드 경로상 lcl_rfc_rate 클래스 풀이 로드되어야 하므로, 그 의존성이 깨지면 테스트가 실패합니다. 기본값은 OPTIONAL로 두고, 호출자가 명시적으로 넘기도록 유지하세요.
FAQ 2. FOR TESTING을 붙인 Mock을 운영 코드에서 참조하려고 하면 컴파일은 되는데 동작이 이상합니다.
FOR TESTING 클래스는 테스트 실행 컨텍스트에서만 인스턴스화되도록 의도된 구조입니다. 운영 흐름에서 참조하면 의존 그래프가 꼬이고 ATC 경고가 발생합니다. Mock은 반드시 테스트 인클루드(... CCAU 테스트 클래스 풀) 또는 테스트용 로컬 클래스로 격리하세요.
FAQ 3. 인터페이스를 만들었는데 메서드가 너무 많아져서 Mock 구현이 부담스럽습니다.
이는 인터페이스가 한 가지 책임을 넘어선 신호입니다. ISP(Interface Segregation Principle)에 따라 lif_rate_provider, lif_rate_publisher처럼 역할별로 쪼개세요. ABAP에서는 한 클래스가 여러 인터페이스를 동시에 INTERFACES 구문으로 구현할 수 있으므로 비용은 거의 없습니다.
FAQ 4. 정적 메서드(CLASS-METHODS)에 의존하는 레거시는 어떻게 DI로 바꾸나요?
정적 호출은 본질적으로 주입이 불가능합니다. 1차 단계로 얇은 인스턴스 래퍼를 만들어 정적 호출을 감싸고, 그 래퍼를 인터페이스로 추상화한 뒤 주입하세요. "Sprout Class" 기법으로 알려진 점진적 리팩터링 패턴이며, 한 번에 전체를 고치려 들지 않는 것이 핵심입니다.
7. 다음 단계 / 관련 주제
- ABAP Test Double Framework(
CL_ABAP_TESTDOUBLE): 인터페이스만 있으면 런타임에 동적 Mock을 만들어 주는 프레임워크. 반복적인lcl_mock_*작성을 줄여 줍니다. - SQL Test Double Framework(
CL_OSQL_TEST_ENVIRONMENT): DB 의존성을 메모리 테이블로 대체합니다. 환율 테이블TCURR같은 핵심 데이터 검증에 유용합니다. - CDS Test Double Framework(
CL_CDS_TEST_ENVIRONMENT): CDS view 의존성을 격리하여 RAP BO 비즈니스 로직을 단위 테스트할 수 있게 합니다. - RAP Behavior Implementation 테스트: managed/unmanaged BO에서 동일한 DI 사상이 그대로 적용됩니다. Behavior 구현 안에서 외부 호출을 직접 하지 말고 인터페이스로 추상화하세요.
- Clean ABAP 가이드의 "Dependency Inversion", "Constructor Injection" 항목을 함께 읽으면 코드 리뷰 기준을 잡기 좋습니다.
8. 핵심 한 줄 요약
인터페이스로 협력자를 추상화하고 생성자로 주입하면, 같은 비즈니스 로직을 운영에서는 RFC로, 테스트에서는 Mock으로 자유롭게 갈아끼울 수 있다 — 이것이 ABAP에서 DI를 쓰는 단 하나의 이유다.
참고 자료
- SAP Help Portal — ABAP Keyword Documentation: Classes and Interfaces
- SAP Help Portal — ABAP RESTful Application Programming Model
- SAP Help Portal — ABAP Unit Testing
- Clean ABAP Style Guide — Dependency Inversion 섹션
- SAP Community Blogs — ABAP Unit & Test Double 태그
- SAP Help Portal — ABAP Test Double Framework
댓글 0
아직 댓글이 없습니다.