1. 개요 및 목표
ABAP 객체지향(ABAP OO) 환경에서 Factory 패턴은 객체 생성 로직을 한 곳에 모아 호출자가 구체 클래스를 직접 알지 않아도 되도록 만드는 대표적인 생성 패턴(Creational Pattern)입니다. SAP S/4HANA 및 ABAP Cloud 시대에 들어서면서 Clean ABAP 가이드라인과 의존성 역전 원칙(DIP)이 강조되면서, Factory 패턴은 RAP(RESTful ABAP Programming Model) 핸들러, BAdI 구현 분기, 단위 테스트용 Test Double 주입에서 빠지지 않고 등장합니다.
이 글에서 다룰 것:
- Factory 패턴의 개념과 ABAP에서의 활용 맥락 이해
interface기반 설계와 의존성 역전 원칙 적용- 운송업체(Shipper) 시나리오로 인터페이스/구현/Factory 3단 구조 직접 구현
- 예외 처리, 싱글톤 캐싱, ABAP Unit Test Double 연계까지 확장
- 현업에서 자주 겪는 트러블슈팅 포인트 정리
2. 사전 가정
이 글은 다음 배경 지식을 가정합니다.
- ABAP OO 기본 문법:
CLASS ... DEFINITION,METHODS,NEW연산자 INTERFACE선언과 구현 클래스에서의INTERFACES사용- 예외 클래스(
CX_STATIC_CHECK,CX_NO_CHECK) 차이 - SE80/ADT(Eclipse) 중 하나로 클래스 풀을 생성해 본 경험
3. 환경 / 버전 / 준비물
아래 코드는 SAP NetWeaver 7.52 이상 또는 S/4HANA 2022(ABAP Platform 2022) 이상에서 검증된 신택스를 사용합니다. NEW 연산자, 인라인 선언(DATA(...)), 문자열 템플릿(|...|)이 동작하는 7.40 SP08 이후 환경이면 대부분 그대로 사용 가능합니다.
- 릴리스: ABAP Platform 7.52 / S/4HANA 2022 / ABAP Cloud(BTP ABAP Environment) 권장
- IDE: ABAP Development Tools(ADT) for Eclipse 또는 SE80
- 패키지: 로컬 객체($TMP) 혹은 개발 패키지 (Z네임스페이스)
- 오브젝트: 클래스 풀 1개 또는 로컬 클래스용 인클루드 1개
- 테스트: ABAP Unit이 활성화된 시스템 (대부분 기본 활성)
아래 예제는 학습 편의를 위해 로컬 클래스(lcl_*)로 작성하지만, 실무에서는 글로벌 클래스(ZCL_*)로 옮기고 패키지/네이밍 규칙을 적용하는 것이 권장됩니다.
4. 핵심 개념
Factory 패턴이란?
Factory 패턴은 객체 생성 책임을 호출자(Client)에서 분리하여 별도의 클래스(Factory)에게 위임하는 패턴입니다. 호출자는 "무엇이 필요한지"만 알려주고, 어떤 구체 클래스가 인스턴스화되는지는 신경 쓰지 않습니다.
비유: 카페에서 "아메리카노"를 주문하면 바리스타가 어떤 머신/원두/컵을 쓰는지 손님이 몰라도 결과물이 나옵니다. 손님 = 호출자, 바리스타 = Factory, 머신/원두 = 구체 클래스입니다.
의존성 역전 원칙(DIP)과의 관계
SOLID의 D에 해당하는 DIP는 "고수준 모듈은 저수준 모듈에 의존하지 않는다. 둘 다 추상(인터페이스)에 의존한다"는 원칙입니다. Factory는 호출자에게 인터페이스 타입의 참조를 돌려주므로, 호출자는 구체 클래스에 의존하지 않고 인터페이스에만 의존합니다.
" 의존 방향 도식
" [Client] ---uses---> [lif_shipper] (interface)
" ^ ^
" | |
" [lcl_express] [lcl_standard] <-- 구현체
" ^
" | creates
" [lcl_shipper_factory]캡슐화 효과
- 변경 비용 감소: 새로운 운송업체(예: lcl_overnight) 추가 시 Factory의 분기만 수정.
- 테스트 용이성: 호출자가 인터페이스에만 의존하므로 Test Double 주입이 쉬움.
- 생성 로직 일원화: 환경 설정, 로깅, 캐싱 등 공통 처리를 Factory에 모음.
5. 실전 코드 3단계
1단계: 인터페이스 정의
운송업체가 제공해야 할 두 가지 기능을 인터페이스로 추상화합니다. calculate_cost는 배송비를 계산하고, get_eta는 예상 도착일을 돌려줍니다.
INTERFACE lif_shipper.
TYPES:
BEGIN OF ty_quote,
cost TYPE p LENGTH 10 DECIMALS 2,
currency TYPE waers,
eta TYPE d,
END OF ty_quote.
METHODS calculate_cost
IMPORTING
iv_weight_kg TYPE p LENGTH 5 DECIMALS 2
iv_destination TYPE land1
RETURNING
VALUE(rv_cost) TYPE p LENGTH 10 DECIMALS 2.
METHODS get_eta
IMPORTING
iv_destination TYPE land1
RETURNING
VALUE(rv_eta) TYPE d.
METHODS quote
IMPORTING
iv_weight_kg TYPE p LENGTH 5 DECIMALS 2
iv_destination TYPE land1
RETURNING
VALUE(rs_quote) TYPE ty_quote.
ENDINTERFACE.인터페이스에 TYPES를 함께 두면 호출자가 lif_shipper=>ty_quote로 참조할 수 있어 응집도가 높아집니다.
2단계: 구현 클래스 작성
두 구현체를 만듭니다. lcl_express는 빠르지만 비싼 옵션, lcl_standard는 저렴하지만 느린 옵션입니다.
CLASS lcl_express DEFINITION FINAL.
PUBLIC SECTION.
INTERFACES lif_shipper.
PRIVATE SECTION.
CONSTANTS c_base_rate TYPE p LENGTH 5 DECIMALS 2 VALUE '15.00'.
CONSTANTS c_per_kg TYPE p LENGTH 5 DECIMALS 2 VALUE '3.50'.
ENDCLASS.
CLASS lcl_express IMPLEMENTATION.
METHOD lif_shipper~calculate_cost.
rv_cost = c_base_rate + ( iv_weight_kg * c_per_kg ).
IF iv_destination <> 'KR'.
rv_cost = rv_cost * '1.8'. " 해외 할증
ENDIF.
ENDMETHOD.
METHOD lif_shipper~get_eta.
rv_eta = sy-datum + COND i( WHEN iv_destination = 'KR' THEN 1 ELSE 3 ).
ENDMETHOD.
METHOD lif_shipper~quote.
rs_quote-cost = lif_shipper~calculate_cost( iv_weight_kg = iv_weight_kg
iv_destination = iv_destination ).
rs_quote-currency = 'USD'.
rs_quote-eta = lif_shipper~get_eta( iv_destination ).
ENDMETHOD.
ENDCLASS.
CLASS lcl_standard DEFINITION FINAL.
PUBLIC SECTION.
INTERFACES lif_shipper.
PRIVATE SECTION.
CONSTANTS c_base_rate TYPE p LENGTH 5 DECIMALS 2 VALUE '5.00'.
CONSTANTS c_per_kg TYPE p LENGTH 5 DECIMALS 2 VALUE '1.20'.
ENDCLASS.
CLASS lcl_standard IMPLEMENTATION.
METHOD lif_shipper~calculate_cost.
rv_cost = c_base_rate + ( iv_weight_kg * c_per_kg ).
ENDMETHOD.
METHOD lif_shipper~get_eta.
rv_eta = sy-datum + COND i( WHEN iv_destination = 'KR' THEN 5 ELSE 14 ).
ENDMETHOD.
METHOD lif_shipper~quote.
rs_quote-cost = lif_shipper~calculate_cost( iv_weight_kg = iv_weight_kg
iv_destination = iv_destination ).
rs_quote-currency = 'USD'.
rs_quote-eta = lif_shipper~get_eta( iv_destination ).
ENDMETHOD.
ENDCLASS.두 클래스 모두 FINAL로 선언해 의도치 않은 상속을 차단하고, 내부 상수는 PRIVATE로 캡슐화했습니다.
3단계: Factory 클래스와 호출부
이제 핵심인 Factory를 작성합니다. create 메서드는 운송 유형(iv_type)에 따라 적절한 구현체를 만들어 인터페이스 참조로 반환합니다. 동일 타입은 캐싱하여 불필요한 인스턴스 생성도 피합니다.
CLASS lcx_shipper_factory DEFINITION
INHERITING FROM cx_static_check FINAL.
PUBLIC SECTION.
INTERFACES if_t100_dyn_msg.
METHODS constructor IMPORTING iv_type TYPE string OPTIONAL.
DATA mv_type TYPE string READ-ONLY.
ENDCLASS.
CLASS lcx_shipper_factory IMPLEMENTATION.
METHOD constructor.
super->constructor( ).
mv_type = iv_type.
ENDMETHOD.
ENDCLASS.
CLASS lcl_shipper_factory DEFINITION FINAL CREATE PRIVATE.
PUBLIC SECTION.
TYPES:
BEGIN OF ty_cache,
type TYPE string,
instance TYPE REF TO lif_shipper,
END OF ty_cache.
CLASS-METHODS create
IMPORTING iv_type TYPE string
RETURNING VALUE(ro_shipper) TYPE REF TO lif_shipper
RAISING lcx_shipper_factory.
CLASS-METHODS reset.
PRIVATE SECTION.
CLASS-DATA gt_cache TYPE HASHED TABLE OF ty_cache
WITH UNIQUE KEY type.
ENDCLASS.
CLASS lcl_shipper_factory IMPLEMENTATION.
METHOD create.
DATA(lv_key) = to_upper( iv_type ).
" 1) 캐시 조회
READ TABLE gt_cache WITH KEY type = lv_key
ASSIGNING FIELD-SYMBOL(<ls_hit>).
IF sy-subrc = 0.
ro_shipper = <ls_hit>-instance.
RETURN.
ENDIF.
" 2) 분기 생성
CASE lv_key.
WHEN 'EXPRESS'.
ro_shipper = NEW lcl_express( ).
WHEN 'STANDARD'.
ro_shipper = NEW lcl_standard( ).
WHEN OTHERS.
RAISE EXCEPTION NEW lcx_shipper_factory( iv_type = lv_key ).
ENDCASE.
" 3) 캐시 저장
INSERT VALUE #( type = lv_key instance = ro_shipper )
INTO TABLE gt_cache.
ENDMETHOD.
METHOD reset.
CLEAR gt_cache.
ENDMETHOD.
ENDCLASS.CREATE PRIVATE로 외부에서 직접 NEW를 호출하지 못하게 막고, CLASS-METHODS로 정적 진입점만 노출했습니다. 캐시는 HASHED TABLE이라 O(1)로 조회됩니다.
호출부 사용 예시
REPORT z_demo_factory.
START-OF-SELECTION.
TRY.
DATA(lo_shipper) = lcl_shipper_factory=>create( 'EXPRESS' ).
DATA(ls_quote) = lo_shipper->quote(
iv_weight_kg = '2.5'
iv_destination = 'KR' ).
WRITE: / |비용: { ls_quote-cost } { ls_quote-currency }|,
/ |도착: { ls_quote-eta DATE = USER }|.
CATCH lcx_shipper_factory INTO DATA(lo_ex).
WRITE: / |지원하지 않는 운송 유형: { lo_ex->mv_type }|.
ENDTRY.호출자는 lcl_express나 lcl_standard를 전혀 알 필요가 없습니다. 새로운 업체가 추가돼도 호출부 코드는 그대로입니다. ABAP Unit에서는 lcl_shipper_factory=>reset( )으로 캐시를 비우고, lif_shipper를 구현하는 Test Double을 주입하는 별도 setter를 두면 격리된 테스트가 가능합니다.
6. 흔한 실수 / 트러블슈팅
Q1. CREATE PRIVATE를 빼먹어 외부에서 NEW로 직접 인스턴스가 생성됩니다.
Factory를 만들었더라도 구현 클래스에 CREATE PUBLIC(기본값)이 남아 있으면 호출자가 NEW lcl_express( )로 우회할 수 있습니다. 구현 클래스에 CREATE PRIVATE를 두고 Factory를 FRIENDS로 선언하거나, 글로벌 클래스 사용 시 패키지 검사(Package Interface)를 활용해 의도된 생성 경로만 허용하는 것이 권장됩니다.
Q2. CASE 분기가 끝없이 늘어납니다.
운송 유형이 수십 개로 늘어나면 CASE가 비대해집니다. 이 경우 레지스트리(Registry) 방식으로 전환하면 좋습니다. register( iv_type, io_factory ) 같은 메서드로 런타임에 등록하거나, 커스터마이징 테이블(SM30 유지보수)에서 type => 클래스명 매핑을 읽어 CREATE OBJECT ... TYPE (lv_classname) 동적 생성으로 처리합니다.
Q3. 단위 테스트에서 실제 구현체가 호출돼 외부 시스템에 영향을 줍니다.
Factory가 정적 메서드만 노출하면 테스트 시 Mock 주입이 어렵습니다. 해결책은 seam을 만드는 것입니다. Factory에 CLASS-METHODS inject IMPORTING io_double TYPE REF TO lif_shipper를 추가해 테스트 setup에서 Test Double을 주입하고, teardown에서 reset( )을 호출합니다. ABAP Unit의 cl_abap_testdouble로 인터페이스 더블을 만들면 됩니다.
Q4. 캐싱한 인스턴스에 상태가 남아 다음 호출에 영향을 줍니다.
Factory가 인스턴스를 캐싱할 때 구현체가 상태(state)를 가지면 위험합니다. 구현체는 가능한 한 stateless로 설계하고, 사용자별/세션별 상태가 필요하다면 캐싱을 끄거나 키에 사용자 ID를 포함시키세요. 또는 인스턴스 대신 방법(method) 자체를 재사용하는 플라이웨이트(Flyweight) 접근도 고려할 만합니다.
7. 다음 단계 / 관련 주제
- Abstract Factory: 여러 종류의 관련 객체군(예: 운송업체 + 송장 양식 + 추적 서비스)을 묶어서 생성할 때 사용합니다.
- Strategy 패턴: Factory가 만들어준 인터페이스 참조를 런타임에 교체하면 자연스럽게 Strategy 패턴이 됩니다. 두 패턴은 자주 함께 등장합니다.
- Dependency Injection: Factory를 한 단계 더 추상화한 형태. RAP 핸들러나 BAdI 구현에서 의존성을 외부에서 주입하는 패턴을 익혀두면 좋습니다.
- ABAP Test Double Framework:
cl_abap_testdouble로 인터페이스 기반 Mock을 만들어 Factory와 결합해 격리 테스트를 구축합니다. - Clean ABAP: Factory, DI, 인터페이스 분리 원칙(ISP) 관련 권장 사항을 참고해 코드 리뷰 기준으로 활용합니다.
8. 참고 자료
- ABAP Keyword Documentation (help.sap.com) -
CLASS,INTERFACE,NEW연산자 레퍼런스 - ABAP Platform Documentation (help.sap.com) - ABAP OO 및 예외 처리 가이드
- SAP BTP, ABAP Environment (help.sap.com) - ABAP Cloud에서 권장되는 객체 생성 패턴
- Clean ABAP Style Guide (SAP GitHub) - 생성/주입 관련 권장 사항
- SAP Community - ABAP OO Design Patterns - 커뮤니티 사례 모음
- ABAP Unit Test Framework (help.sap.com) - Test Double 활용
핵심 한 줄 요약
Factory 패턴은 "무엇을 만들지"만 알려주면 "어떻게 만들지"를 캡슐화해 주는 장치이며, ABAP에서는 인터페이스 + CREATE PRIVATE + 정적 create 메서드의 조합으로 구현해 의존성 역전과 테스트 용이성을 동시에 확보합니다.
댓글 0
아직 댓글이 없습니다.