1. 개요 - 이 글에서 다룰 것
주문 금액에 따라 일반 회원은 5%, VIP는 20%, 임직원은 30% 할인을 적용한다고 가정해 봅시다. 가장 빠른 해법은 IF/CASE로 분기를 치는 것이지만, 회원 등급이 늘어날 때마다 같은 메서드를 계속 열어 수정해야 하고, 단위 테스트도 분기마다 별도로 작성해야 합니다. ABAP의 Strategy 패턴은 이 분기 덩어리를 인터페이스 + 구체 클래스 + 컨텍스트 3개로 분리해, 런타임에 알고리즘을 갈아끼울 수 있게 만듭니다.
이 글에서 다룰 것:
- Strategy 패턴이 ABAP OO 환경에서 어떤 문제를 풀어주는지
- 인터페이스 정의 -> 구체 전략 클래스 -> 컨텍스트 주입의 3단계 흐름
- 할인율 계산 예제로 IF 분기를 제거하는 실제 코드
- 실무에서 자주 마주치는 NULL 참조, 의존성 주입, 단위 테스트 작성 패턴
2. 이 글을 보기 전에
아래 개념에 익숙하면 따라오기 편합니다.
- ABAP 클래스 정의(
CLASS ... DEFINITION) 및 인터페이스(INTERFACE) 문법 - 참조 변수(
TYPE REF TO)와 객체 생성(NEW) 연산자 사용 경험 - 다형성(polymorphism)의 기본 개념 - 같은 메서드 호출이 객체에 따라 다르게 동작하는 원리
- ABAP Unit 테스트 클래스의 기본 골격(선택 사항이지만 3단계 예제에 등장)
3. 환경 / 버전 / 준비물
예제 코드는 다음 환경에서 동작하도록 작성됐습니다.
- ABAP 릴리스: ABAP 7.40 SP08 이상 권장 (
NEW인스턴스 연산자, 인라인 선언DATA(...)사용) - 플랫폼: SAP NetWeaver AS ABAP, S/4HANA 온프레미스, 또는 ABAP Cloud / SAP BTP ABAP Environment
- IDE: ADT(ABAP Development Tools for Eclipse) 또는 SE80 - ADT가 일반적으로 더 권장됩니다
- 준비물: 개발 가능한 패키지, 사용자 권한(S_DEVELOP), 테스트용 클래스 풀 또는 로컬 클래스를 담을 리포트 1개
예제는 모두 로컬 클래스(lcl_*) 기준으로 작성합니다. 글로벌 클래스로 옮길 때도 클래스명만 ZCL_*로 바꾸면 동일하게 동작합니다.
4. 핵심 개념
Strategy 패턴을 한 줄로 요약하면 "같은 목적을 가진 알고리즘 여러 개를 인터페이스 뒤에 숨기고, 사용하는 쪽은 인터페이스만 본다"입니다. 비유하자면 노트북에 USB-C 포트가 있고, 충전기/외장 SSD/모니터 케이블이 모두 같은 포트에 꽂힌다고 생각하면 됩니다. 노트북(컨텍스트)은 무엇이 꽂혔는지 신경 쓰지 않고 "전원 공급" 또는 "데이터 전송"이라는 표준 동작만 호출합니다.
구성 요소 3가지:
- Strategy (인터페이스): 알고리즘이 지켜야 할 약속. 예제에서는
lif_discount_strategy가calc_discount메서드 시그니처를 정의합니다. - Concrete Strategy (구체 전략 클래스): 약속을 실제로 구현.
lcl_normal_discount,lcl_vip_discount처럼 알고리즘별로 1개씩 만듭니다. - Context (컨텍스트): 전략을 주입받아 사용. 어떤 전략이 들어왔는지는 모르고, 인터페이스 메서드만 호출합니다.
도식:Client->Context(전략 인터페이스 보유)->Concrete Strategy A / B / C
의존성 방향이 항상 인터페이스를 향하기 때문에, 새 알고리즘이 추가돼도 Context는 손대지 않습니다.
IF 분기 방식과 비교하면 차이가 명확합니다. IF 방식은 한 메서드 안에 분기가 누적되며 OCP(개방-폐쇄 원칙)를 위반합니다. 등급이 하나 늘어날 때마다 메서드를 열어 수정해야 하고, 회귀 테스트 범위가 넓어집니다. Strategy 방식은 새 클래스 하나만 추가하면 끝이고, 기존 클래스는 건드리지 않으므로 변경의 영향 범위가 격리됩니다.
또한 런타임에 전략을 바꿀 수 있다는 점이 큰 장점입니다. 예를 들어 프로모션 기간에는 lcl_event_discount를 주입하고, 종료 후에는 다시 lcl_normal_discount로 돌릴 수 있습니다. 같은 클래스 인스턴스를 유지한 채 동작만 교체할 수 있다는 점에서 단순 상속보다 유연합니다.
5. 실전 코드 3단계
1단계 - 인터페이스 정의 (알고리즘 약속 만들기)
먼저 모든 할인 전략이 따라야 할 계약을 인터페이스로 선언합니다. 입력은 정가(iv_price), 출력은 할인 금액(rv_discount)이며, 어떤 비율로 어떻게 계산할지는 구현 클래스의 자유입니다.
INTERFACE lif_discount_strategy.
METHODS calc_discount
IMPORTING iv_price TYPE p LENGTH 9 DECIMALS 2
RETURNING VALUE(rv_discount) TYPE p LENGTH 9 DECIMALS 2.
ENDINTERFACE.
여기서 두 가지 결정이 중요합니다. 첫째, 반환 타입을 p LENGTH 9 DECIMALS 2로 명시해 금액 계산의 정밀도를 통일했습니다. 둘째, 메서드를 하나만 두어 ISP(인터페이스 분리 원칙)를 지켰습니다. 인터페이스에 메서드가 너무 많아지면 구현 클래스가 빈 메서드(RAISE EXCEPTION TYPE cx_sy_not_implemented)로 채워지는 안티패턴이 발생합니다.
2단계 - 구체 전략 클래스 (알고리즘 구현)
일반 회원 5%, VIP 20% 두 가지 전략을 만들어 봅니다. 각 클래스는 인터페이스 메서드만 구현하면 됩니다.
CLASS lcl_normal_discount DEFINITION.
PUBLIC SECTION.
INTERFACES lif_discount_strategy.
ENDCLASS.
CLASS lcl_normal_discount IMPLEMENTATION.
METHOD lif_discount_strategy~calc_discount.
rv_discount = iv_price * '0.05'. " 5%
ENDMETHOD.
ENDCLASS.
CLASS lcl_vip_discount DEFINITION.
PUBLIC SECTION.
INTERFACES lif_discount_strategy.
ENDCLASS.
CLASS lcl_vip_discount IMPLEMENTATION.
METHOD lif_discount_strategy~calc_discount.
rv_discount = iv_price * '0.20'. " 20%
ENDMETHOD.
ENDCLASS.
실무에서는 단순 계산만 하지 않고 로깅, 한도 체크, 예외 상황도 다뤄야 합니다. VIP 할인을 좀 더 현실적으로 만들어 보겠습니다. 최대 할인액 한도를 두고, 한도를 넘으면 한도값으로 클램프하며, 잘못된 입력은 예외로 막습니다.
CLASS lcl_vip_discount_v2 DEFINITION.
PUBLIC SECTION.
INTERFACES lif_discount_strategy.
CONSTANTS c_max_discount TYPE p LENGTH 9 DECIMALS 2 VALUE '50000.00'.
ENDCLASS.
CLASS lcl_vip_discount_v2 IMPLEMENTATION.
METHOD lif_discount_strategy~calc_discount.
IF iv_price <= 0.
RAISE EXCEPTION TYPE cx_sy_arithmetic_error
EXPORTING textid = cx_sy_arithmetic_error=>cx_sy_arithmetic_error.
ENDIF.
DATA(lv_calc) = CONV p( iv_price * '0.20' ).
rv_discount = COND #( WHEN lv_calc > c_max_discount
THEN c_max_discount
ELSE lv_calc ).
" 로깅 (Application Log 또는 BAL_LOG_CREATE)
MESSAGE i001(zdiscount) WITH iv_price rv_discount INTO DATA(lv_msg).
ENDMETHOD.
ENDCLASS.
이렇게 분리해 두면 "VIP 한도는 5만원" 같은 비즈니스 정책 변경이 발생해도 다른 등급의 코드는 손대지 않습니다. 또한 각 전략을 독립적으로 ABAP Unit 테스트할 수 있어 회귀 위험이 줄어듭니다.
3단계 - 컨텍스트 주입 (프로덕션 패턴)
컨텍스트는 전략을 주입받아 보관하고, 외부에는 단일 메서드(get_final_price)만 노출합니다. 호출자는 어떤 전략이 들어 있는지 알 필요가 없습니다.
CLASS lcl_price_context DEFINITION FINAL.
PUBLIC SECTION.
METHODS constructor
IMPORTING io_strategy TYPE REF TO lif_discount_strategy
RAISING cx_parameter_invalid_range.
METHODS get_final_price
IMPORTING iv_price TYPE p LENGTH 9 DECIMALS 2
RETURNING VALUE(rv_final) TYPE p LENGTH 9 DECIMALS 2.
METHODS set_strategy
IMPORTING io_strategy TYPE REF TO lif_discount_strategy
RAISING cx_parameter_invalid_range.
PRIVATE SECTION.
DATA mo_strategy TYPE REF TO lif_discount_strategy.
ENDCLASS.
CLASS lcl_price_context IMPLEMENTATION.
METHOD constructor.
IF io_strategy IS NOT BOUND.
RAISE EXCEPTION TYPE cx_parameter_invalid_range.
ENDIF.
mo_strategy = io_strategy.
ENDMETHOD.
METHOD set_strategy.
IF io_strategy IS NOT BOUND.
RAISE EXCEPTION TYPE cx_parameter_invalid_range.
ENDIF.
mo_strategy = io_strategy.
ENDMETHOD.
METHOD get_final_price.
rv_final = iv_price - mo_strategy->calc_discount( iv_price ).
ENDMETHOD.
ENDCLASS.
사용 예시는 다음과 같습니다. 동일한 컨텍스트 인스턴스로 전략만 갈아끼우는 모습을 확인해 보세요.
START-OF-SELECTION.
" VIP 가격 계산
DATA(lo_ctx) = NEW lcl_price_context( NEW lcl_vip_discount( ) ).
DATA(lv_vip_price) = lo_ctx->get_final_price( '100.00' ).
WRITE: / 'VIP:', lv_vip_price. " -> 80.00
" 런타임에 일반 회원 전략으로 교체
lo_ctx->set_strategy( NEW lcl_normal_discount( ) ).
DATA(lv_normal_price) = lo_ctx->get_final_price( '100.00' ).
WRITE: / 'Normal:', lv_normal_price. " -> 95.00
프로덕션 환경에서는 전략 선택 로직을 팩토리에 위임하는 것이 일반적으로 권장됩니다. 회원 등급 문자열을 받아 적절한 전략 인스턴스를 돌려주는 정적 팩토리를 두면, 호출 코드가 더 깔끔해집니다.
CLASS lcl_discount_factory DEFINITION FINAL.
PUBLIC SECTION.
CLASS-METHODS create
IMPORTING iv_grade TYPE string
RETURNING VALUE(ro_strategy) TYPE REF TO lif_discount_strategy.
ENDCLASS.
CLASS lcl_discount_factory IMPLEMENTATION.
METHOD create.
ro_strategy = SWITCH #( iv_grade
WHEN 'VIP' THEN NEW lcl_vip_discount( )
WHEN 'NORMAL' THEN NEW lcl_normal_discount( )
ELSE NEW lcl_normal_discount( ) ).
ENDMETHOD.
ENDCLASS.
"팩토리에도 결국 분기가 있지 않은가?"라는 질문이 자주 나옵니다. 맞습니다. 다만 분기가 객체 생성 단 한 곳으로 격리되고, 비즈니스 로직(계산식)은 분기에서 완전히 분리됩니다. 더 나아가면 등급-클래스 매핑을 커스터마이징 테이블에 두고 CREATE OBJECT 동적 생성으로 팩토리 자체에서도 분기를 제거할 수 있습니다.
마지막으로 ABAP Unit 테스트의 핵심 패턴은 "가짜 전략을 만들어 주입"하는 것입니다. 컨텍스트가 인터페이스에만 의존하므로 테스트용 더블을 쉽게 끼워 넣을 수 있습니다.
CLASS ltcl_fake_strategy DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES lif_discount_strategy.
ENDCLASS.
CLASS ltcl_fake_strategy IMPLEMENTATION.
METHOD lif_discount_strategy~calc_discount.
rv_discount = '10.00'. " 항상 10원 할인
ENDMETHOD.
ENDCLASS.
CLASS ltcl_price_context DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS final_price_uses_strategy FOR TESTING.
ENDCLASS.
CLASS ltcl_price_context IMPLEMENTATION.
METHOD final_price_uses_strategy.
DATA(lo_ctx) = NEW lcl_price_context( NEW ltcl_fake_strategy( ) ).
cl_abap_unit_assert=>assert_equals(
exp = CONV decfloat34( '90.00' )
act = lo_ctx->get_final_price( '100.00' ) ).
ENDMETHOD.
ENDCLASS.
6. 흔한 실수 / 트러블슈팅
Q1. 컨텍스트에서 CX_SY_REF_IS_INITIAL 예외가 발생합니다.
전략 참조가 바인딩되지 않은 상태에서 메서드를 호출하면 발생합니다. 컨스트럭터에서 IS NOT BOUND 체크로 가드를 두거나, 기본 전략을 한 개 미리 생성해 두는 패턴을 권장합니다. set_strategy에도 같은 가드를 넣어야 합니다.
Q2. 새 등급이 추가될 때마다 팩토리의 SWITCH를 계속 수정해야 합니다. 이것도 OCP 위반 아닌가요?
맞습니다. 비즈니스 로직(계산)은 OCP를 지키지만, 객체 생성은 여전히 분기가 남습니다. 더 엄격하게 가려면 등급 코드와 클래스명을 매핑한 커스터마이징 테이블(ZDISCOUNT_MAP)을 두고 CREATE OBJECT lo_strategy TYPE (lv_classname)로 동적 생성하면 됩니다. 다만 동적 생성은 ATC 정적 분석에서 경고가 뜨고 ABAP Cloud의 릴리스 API 제약을 받으므로, 일반적으로 SWITCH 팩토리 정도가 가독성과 유연성의 균형점입니다.
Q3. 인터페이스 메서드에 새 파라미터를 추가해야 합니다. 기존 구현 클래스가 다 깨지는데 어떻게 해야 하나요?
이런 변경은 인터페이스가 너무 좁게 설계됐다는 신호입니다. 옵션 파라미터(OPTIONAL)로 추가하거나, 컨텍스트 정보를 담는 구조체 파라미터(is_context TYPE zdiscount_ctx_s)를 받도록 시그니처를 처음부터 설계하면 변화에 강해집니다. 이미 운영 중이라면 새 인터페이스 lif_discount_strategy_v2를 만들고 기존 인터페이스도 한동안 유지하는 점진적 마이그레이션 전략이 안전합니다.
Q4. 전략 클래스 안에서 DB SELECT를 해도 되나요?
가능은 하지만 단위 테스트가 어려워집니다. DB 접근은 별도 리포지토리 클래스에 두고, 전략 컨스트럭터로 주입받는 형태가 일반적으로 권장됩니다. 이렇게 하면 테스트에서 가짜 리포지토리를 주입해 SELECT 없이 검증할 수 있습니다.
7. 다음 단계 / 관련 주제
Strategy 패턴이 손에 익었다면 다음 주제로 확장해 보세요.
- Factory Method / Abstract Factory: 전략 생성 자체를 객체화. 등급-전략 매핑이 복잡해질 때 유용합니다.
- State 패턴: 구조는 Strategy와 거의 같지만, 객체의 "상태"가 동작을 바꾼다는 의미론적 차이가 있습니다.
- Decorator 패턴: 전략 위에 로깅/캐싱/권한 체크를 덧씌울 때 사용합니다. 같은 인터페이스를 구현한 래퍼를 만들면 됩니다.
- 의존성 주입 컨테이너: BAdI, Enhancement Spot, 또는 ABAP Cloud의
RAP BO Behavior Implementation에서 전략을 외부에서 주입하는 패턴. - RAP(RESTful Application Programming): Behavior Pool에서 가격 계산 로직을 Strategy로 분리하면 시나리오별 동작 교체가 쉬워집니다.
8. 핵심 한 줄
IF 분기는 분기의 수만큼 수정 지점을 늘리지만, Strategy 패턴은 분기를 클래스로 바꿔 "새 등급 = 새 클래스 하나"로 변경 비용을 일정하게 유지합니다.
댓글 0
아직 댓글이 없습니다.