본문 바로가기
Spring

[스프링 핵심 원리 - 고급편] 4. 전략 패턴으로 유연하게 동작 변경하기

by doogfoot 2024. 1. 3.

[스프링 핵심 원리 - 고급편] 4. 전략 패턴

 

 

 

 

전략 패턴 - 정의

다수의 책과 문서에서는 전략 패턴을 다양하게 정의하고 있습니다

 

1. 행위에 대한 전략 클래스를 각각 생성하고, 유사한 행위들을 캡슐화하는 인터페이스를 정의한다. 객체의 행위를 변경할 때 직접 행위를 수정하지 않고 전략을 변경함으로써 행위를 유연하게 확장하는 방법을 말한다.

 

2. 변하지 않는 부분을 "Context" 라는 곳에 두고, 변하는 부분은 "Strategy"라는 인터페이스를 만들고 해당 인터페이스를 구현하여 위임으로 문제를 해결하는 것이다.

 

3. 알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만들자. 전략을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.

 

4. 런타임 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 패턴이다.

 

위의 정의들을 정리하면 인터페이스를 사용해 동작(전략) 별로 각 클래스를 구현하고 상호 교환함으로써, 객체의 동작을 동적으로 변경가능 하게 하는 패턴입니다.

 

변경이 빈번한 동작 부분을 인터페이스를 통해 따로 분리했기 때문에 이를 호출하는 클라이언트와 상관없이 독립적으로 동작을 변경할 수 있습니다.

 

 

전략 패턴 - 예제

 

전략 패턴은 보통 두 가지 방식으로 구현할 수 있는데 두가지 모두 예시로 설명드리겠습니다.

 

첫 번째 방식은 1. 전략을 컨텍스트의 멤버 변수로 가지는 선 설정, 후 실행 예제입니다.

 

// 전략 인터페이스
public interface PaymentStrategy {
    void pay(int amount);
}


// 신용 카드 결제 전략 구현체
public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Credit Card Payment: " + amount);       
    }
}

// 페이팔 결제 전략 구현체
public class PayPalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("PayPal Payment: " + amount);        
    }
}

 

위의 코드에서 먼저 전략들의 인터페이스가 되는 PaymentStrategy 인터페이스 클래스를 만들고 전략 별로 각각 구현체를 만들었습니다.

 

// 결제 컨텍스트
public class PaymentContext {
    private PaymentStrategy paymentStrategy;

    // 전략 설정 메서드
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    // 결제 수행 메서드
    public void performPayment(int amount) {
        if (paymentStrategy != null) {
            paymentStrategy.pay(amount);
        } else {
            System.out.println("No payment strategy set.");
        }
    }
}

 

위에 코드는 전략을 사용할 컨텍스트 클래스를 만드는데 멤버변수로 PaymentStrategy라는 타입을 가지고 있습니다.

Setter 메서드(setPaymentStrategy)를 통해 전략을 설정하고, 전략을  perfromPayment 메서드로 수행하도록 되어있습니다.

 

예시만 봐도 아시겠지만 Setter 메서드를 이용해서 자유롭게 전략을 변경할 수 있으며, 전략이 변경돼도 수행 메서드는 변경 없이 그대로 사용할 수 있다는 것을 알 수 있습니다.

PaymentContext paymentContext = new PaymentContext();

// 신용 카드 결제 전략 설정 후 수행
paymentContext.setPaymentStrategy(new CreditCardPayment());
paymentContext.performPayment(100);

// 페이팔 결제 전략으로 설정 후 수행
paymentContext.setPaymentStrategy(new PayPalPayment());
paymentContext.performPayment(50);

 

위의 코드는 전략을 변경하면서 결제를 수행하는 클라이언트 코드 예시입니다. 

 

전략을 변경하더라도 기존의 전략 코드들에 if 문 등으로 전략을 추가 및 수정하지 않아도 되며 클라이언트 코드를 많이 수정하지 않고 전략을 변경할 수 있다는 것을 알 수 있습니다.

 

앞 포스팅에서 설명한 템플릿 메서드 패턴처럼 전략이 추가되거나 변경되어도 컨텍스트 클래스 자체의 수정이 필요하지 않기 때문에 유지 보수에도 좋을 것 같습니다. (컨텍스트 클래스는 변하지 않음, 전략 클래스만 추가 및 수정)

 

 

위의 첫 번째 예시와 비슷하지만 조금 다른 방법도 소개해 드리자면 2. 수행 시 전략을 파라미터로 전달하는 예제입니다.

 

// 결제 서비스 컨텍스트
public class PaymentContext {
    // 결제 수행 메서드에 파라미터로 직접 전략을 받아 사용
    public void performPayment(PaymentStrategy paymentStrategy, int amount) {
        if (paymentStrategy != null) {
            paymentStrategy.pay(amount);
        } else {
            System.out.println("No payment strategy set.");
        }
    }
}

 

기존 인터페이스와 전략 구현체는 동일하고 위의 코드처럼 컨텍스트 클래스가 달라지게 됩니다.

전략을 컨텍스트 클래스의 멤버변수로 가지고 있지 않고 수행 메서드의 파라미터로 직접 전달받아 바로 전략을 수행합니다

 

// 결제 서비스 컨텍스트 생성
PaymentContext paymentContext = new PaymentContext();

// 신용 카드 결제 전략을 직접 전달하면서 수행
paymentContext.performPayment(new CreditCardPayment(), 100);

// 페이팔 결제 전략 직접을 전달하면서 수행
paymentContext.performPayment(new PayPalPayment(), 50);

 

클라이언트 코드도 달라지게 되는데 첫 번째 예제와 다르게 전략을 선 설정하는 메서드를 사용하지 않고 파라미터로 전략을 전달하면서 바로 수행 메서드를 호출하도록 되어있습니다.

 

 

두 가지 예제를 정리하면 다음과 같습니다.

 

1. 전략을 컨텍스트의 멤버 변수로 가지는 선 설정, 후 실행

2. 수행 시 전략을 파라미터로 전달

 

 

둘 다 전략패턴의 구현방식이며 우리가 원하는 유연하게 객체의 동작을 변경할 수 있습니다.

큰 차이는 없지만 장단점이 있어서 상황에 맞게 사용하면 될 것 같습니다

 

 개인적인 생각으로는 두 번째 예제가 더 좋다고 생각하는데 이유는 다음과 같습니다.

 

1) 전략을 두 번에 나눠서 실행하는 것보다는 한 번에 수행하는 장점 (코드라인도 줄이고 직관적임)

2) 컨텍스트 클래스에 불필요한 Setter 메서드, 멤버변수(전략)를 제거할 수 있음 (동시성 문제 등..)

3) 컨텍스트 클래스와 전략 클래스 간에 의존성을 조금이라도 분리(수행 전에 미리 설정된 전략을 알고 있어야 되는 불편함)

 

위의 몇 가지 이유가 바로 생각났기 때문에 첫 번째보다는 두 번째 예제인 수행 시 전략을 파라미터로 전달하는 방식이 일반적으로는 좋다고 생각됩니다. (하지만 이것도 상황에 따라 다를 것 같음) 

 

 

전략 패턴 - 장점

 

컨텍스트 클래스(변하지 않는 공통의 코드)의 변경 없이 새로운 전략을 추가하고 교체할 수 있다는 점이 장점이고 패턴의 사용 이유입니다.

 

템플릿 메서드 패턴과 달리 인터페이스를 구현하는 방식이기 때문에 좀 더 결합도를 낮출 수 있습니다.

 

 

 

전략 패턴 - 정리

코드를 개발할 때 변하지 않는 부분과 변하는 부분을 구분한 뒤 유지 보수에 좋은 코드를 위해 여러 가지 패턴을 공부하고 있습니다. 

 

그중 전략 패턴은 인터페이스와 전략 별로 각각 구현한 구현체를 사용하여 결합도를 낮추고 쉽게 변경할 수 있도록 하는 패턴입니다.

 

변하지 않는 부분은 컨텍스트 클래스에, 변하는 로직은 전략 클래스로 각각 나눔으로써 유지보수를 효율적으로 할수 있기 때문에 잘 알고 사용하면 좋을 것 같습니다.

 

댓글