디자인 패턴/행위

전략 패턴(Strategy Pattern)

개발로 먹고 살자 2024. 1. 6. 17:36

 


개요

전략 패턴은 객체 지향 디자인 패턴 중 하나로 알고리즘군을 정의하고 각각을 캡슐화하여 사용할 수 있게 만드는 디자인 패턴입니다.

전략 패턴의 핵심 아이디어는 문제를 해결하는 다양한 전략(알고리즘)을 만들고 이를 동적으로 교환하여 사용할 수 있도록 하는 것입니다.

 

📌 헤드 퍼스트 디자인 패턴

  • 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다.
  • 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.

 

주요 구성 요소


1. 전략 인터페이스(Strategy)

전략 인터페이스는 상황에 따라 다르게 사용될 여러 알고리즘을 나타내는 인터페이스나 추상 클래스입니다.

2. 전략 인터페이스를 상속 받는 구체적인 전략 클래스(Concrete Strategy)

전략 클래스는 전략 인터페이스를 상속 받아 실제로 구현한 클래스입니다. 각각의 구체적인 전략 클래스는 특정한 알고리즘을 수행합니다.

3. 전략을 사용할 컨텍스트 클래스(Context)

콘텍스트 클래스는 전략 객체를 사용하는 역할을 합니다. 필요에 따라 전략 객체를 변경하여 사용할 수 있습니다.

 

 

전략 패턴의 장점


  1. 유연성 및 확장성: 새로운 전략을 추가하거나 기존의 전략을 변경하는 것이 쉽습니다.
  2. 코드 재사용: 전략은 독립적으로 구현되어 있으므로 다른 컨텍스트에서 재사용이 가능합니다.
  3. 유지보수 용이성: 각 전략이 독립적으로 관리되므로 수정이 필요한 경우 해당 전략 클래스만 수정하면 됩니다.

 

전략 패턴의 단점


  1. 코드 복잡성 증가: 클래스 및 인터페이스의 생성이 많아지므로 코드 복잡성이 증가합니다.

 

 

전략 패턴을 사용하는 이유


예를 들어, Animal 클래스와 그를 상속 받는 Dog와 Cat 클래스가 있습니다.

1. 상속을 이용하여 코드를 작성할 때

이 구조를 코드로 표현하면 다음과 같습니다.

// Animal 클래스
class Animal {
    void makeSound() {
        System.out.println("동물 소리를 낸다.");
    }
}

// 하위 클래스 - 강아지
class Dog extends Animal {
    void makeSound() {
        System.out.println("멍멍!");
    }

    void fetch() {
        System.out.println("공을 물어온다.");
    }
}

// 하위 클래스 - 고양이
class Cat extends Animal {
    void makeSound() {
        System.out.println("야옹~");
    }

    void scratch() {
        System.out.println("할퀴기를 한다.");
    }
}

이 상황에서 만약에 Animal 클래스에 새로운 특정 행동을 추가하려면 **Dog**와 Cat 클래스도 수정해야 합니다. 이는 새로운 동물이 추가될 때마다 전체 클래스 계층 구조를 수정해야 한다는 문제가 발생합니다.

2. 인터페이스를 이용하여 코드를 작성할 때

이 구조를 코드로 표현하면 다음과 같습니다.

// 첫 번째 특정 행동을 정의한 인터페이스
interface SoundBehavior {
    void makeSound();
}

// 두 번째 특정 행동을 정의한 인터페이스
interface ActionBehavior {
    void performAction();
}

// Animal 클래스
class Animal implements SoundBehavior, ActionBehavior {
    @Override
    public void makeSound() {
        System.out.println("동물 소리를 낸다.");
    }

    @Override
    public void performAction() {
        System.out.println("동물이 특정 행동을 한다.");
    }
}

// Dog 클래스
class Dog extends Animal {
    // Dog 클래스는 SoundBehavior와 ActionBehavior 모두 상속받음
}

// Cat 클래스
class Cat extends Animal {
    // Cat 클래스는 SoundBehavior와 ActionBehavior 모두 상속받음
}

이 경우, Dog와 Cat 클래스가 Animal 클래스를 상속하면서 SoundBehavior와 ActionBehavior 인터페이스의 모든 메서드를 상속 받게 됩니다.

이 때 발생하게 되는 여러가지 문제점입니다.

  1. 다중 상속의 제한: Java 같은 경우 클래스는 하나의 클래스만을 상속 받을 수 있기 때문에 Dog 나 Cat 클래스가 Animal 클래스를 상속 받는다면 추후 다른 클래스를 상속 받을 수 없습니다.
  2. 사용하지 않는 메소드 오버라이드: 만약 다른 Animal 클래스가 추가될 경우 SoundBehavior와 ActionBehavior 중 사용하지 않는 인터페이스가 존재할 수 있습니다. 하지만 Animal 클래스를 상속 받고 있기 때문에 사용하지 않더라도 인터페이스의 메서드를 모두 오버라이드 해야 하는 상황이 발생합니다.
  3. 인터페이스 수정으로 인한 다른 클래스 영향: 새로운 특정 행동을 추가하려면 해당 인터페이스를 구현한 모든 클래스에서 수정이 필요합니다.

따라서 다중 상속이 제한되고 인터페이스의 메서드를 수정할 때 다른 클래스에도 영향을 미치게 되면, 유연성과 확장성이 떨어지는 구조가 될 수 있습니다.

3.전략 패턴을 이용하여 코드를 작성할 때

전략 패턴과 같은 디자인 패턴을 사용하여 코드를 구조화하면, 클래스 간의 의존성을 낮추고 변경에 유연하게 대처할 수 있는 설계를 할 수 있습니다.

 

이 구조를 코드로 표현하면 다음과 같습니다.

// 전략 인터페이스 - 행동
interface Behavior {
    void perform();
}

// 구체적인 전략 클래스 - 소리 행동
class SmallSoundBehavior implements Behavior {
    @Override
    public void perform() {
        System.out.println("작은 소리를 낸다.");
    }
}

// 구체적인 전략 클래스 - 소리 행동
class BigSoundBehavior implements Behavior {
    @Override
    public void perform() {
        System.out.println("큰 소리를 낸다.");
    }
}

// 동물 클래스
class Animal {
    private Behavior soundBehavior;

    public Animal(Behavior soundBehavior) {
        this.soundBehavior = soundBehavior;
    }

    public void makeSound() {
        soundBehavior.perform();
    }

    public void setSoundBehavior(Behavior soundBehavior) {
        this.soundBehavior = soundBehavior;
    }
}

// Dog 클래스
class Dog extends Animal {
    public Dog(Behavior soundBehavior) {
        super(soundBehavior);
    }
}

// Cat 클래스
class Cat extends Animal {
    public Cat(Behavior soundBehavior) {
        super(soundBehavior);
    }
}

// 메인
public class Main {
    public static void main(String[] args) {
        // 동물 생성
        Animal dog = new Dog(new BigSoundBehavior());
        Animal cat = new Cat(new SmallSoundBehavior());

        // 소리
        dog.makeSound(); // 큰 소리를 낸다
        cat.makeSound(); // 작은 소리를 낸다

        // 전략 변경
        dog.setSoundBehavior(new SmallSoundBehavior());
        cat.setSoundBehavior(new BigSoundBehavior());

				// 전략 변경 후 동물 소리
        dog.makeSound(); // 작은 소리를 낸다
        cat.makeSound(); // 큰 소리를 낸다
    }
}

 

예제 프로젝트: 전략 패턴을 이용한 결제 시스템

전략 패턴을 통해 결제 시스템에 적용하는 예시 코드입니다.

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

// 신용카드 결제 전략
class creditCardPayment implements PaymentStrategy {
		private String cardNumber;
    private String expirationDate;
    private String cvv;

		public CreditCardPayment(String cardNumber, String expirationDate, String cvv) {
        this.cardNumber = cardNumber;
        this.expirationDate = expirationDate;
        this.cvv = cvv;
    }

		@Override
		public void pay(int amount) {
			// 카드를 통한 결제 로직
		}
}

// 무통장 입금 결제 전략
class BankTransferPayment implements PaymentStrategy {
    private String accountNumber;
    private int bankCode;

    public BankTransferPayment(String accountNumber, int bankCode) {
        this.accountNumber = accountNumber;
        this.bankCode = bankCode;
    }

    @Override
    public void pay(int amount) {
        // 무통장 입금을 통한 결제 로직
    }
}

// 카카오페이 결제 전략
class KakaoPayPayment implements PaymentStrategy {
    private String kakaoId;
    private String password;

    public KakaoPayPayment(String kakaoId, String password) {
        this.kakaoId = kakaoId;
        this.password = password;
    }

    @Override
    public void pay(int amount) {
        // 카카오페이를 통한 결제 로직
    }
}

class Payment {
		private PaymentStrategy paymentStrategy;

		public Payment(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

		public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

		public void pay(amount) {
				paymentStrategy.pay(amount);
		}
}

// 메인
public class Main {
    public static void main(String[] args) {
				// 초기 전략 : 신용 카드 결제
				Payment payment = new Payment(new creditCardPayment("1234-5678", "04/22", "377"));
		    payment.pay(10000);

				// 전략 변경 : 무통장 입금
				payment.setPaymentStrategy(new BankTransferPayment("1234-5678", 111));
				payment.pay(10000);

				// 전략 변경 : 카카오페이
				payment.setPaymentStrategy(new BankTransferPayment("testId", "testPassword"));
				payment.pay(10000);
		}
}

/* --------------- develop ----------------- */
// Enum으로 결제 전략 정의
enum PaymentMethod {
    CREDIT_CARD(CreditCardPayment::new),
    BANK_TRANSFER(BankTransferPayment::new),
    KAKAO_PAY(KakaoPayPayment::new);

    private final PaymentStrategyCreator strategyCreator;

    PaymentMethod(PaymentStrategyCreator strategyCreator) {
        this.strategyCreator = strategyCreator;
    }

    public PaymentStrategy createPaymentStrategy(String[] params) {
        return strategyCreator.create(params);
    }

    @FunctionalInterface
    interface PaymentStrategyCreator {
        PaymentStrategy create(String[] params);
    }
}

// 웹을 통해 값을 받는다고 가정 후 enum을 통해 변경
public class Main {
		
		@PostMapping("/api/pay")
		public void pay(@RequestParam String payMethod, @RequestBody payDto dto) {
				// 받은 값을 통해 enum으로 알맞은 전략 선택하여 생성
        PaymentMethod paymentMethod = PaymentMethod.valueOf(payMethod);
        PaymentStrategy paymentStrategy = paymentMethod.createPaymentStrategy(dto.getParameters());
        
				// 결제
				Payment payment = new Payment(paymentStrategy);
        payment.pay(dto.getAmount());
		}
}

// DTO 클래스
class PayDto {
    private int amount;
    private String[] parameters;
}

결론

전략 패턴은 다양한 알고리즘을 캡슐화하고 유연하게 교환 가능한 강력한 디자인 패턴 중 하나입니다.

전략 패턴의 핵심은 extends, implements 와 같은 상속이 아닌 **구성(Composition)**입니다.

전략 패턴은 일반적으로 상속에 종속되지 않으며 유연한 수정이 가능합니다.

그렇기 때문에 적절한 상황에서 사용하면 쉽게 유지 보수가 가능하며, 확장성을 확보하는 데 도움이 됩니다.