본문 바로가기
디자인패턴

디자인패턴 : 전략 패턴

by 이상한나라의개발자 2024. 5. 9.

디자인 원칙 1

"바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다."

 

이 개념은 매우 간단하지만 모든 디자인 패턴의 기반을 이루는 원칙이다. 

 

  • 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화한다.
  • 그러면 코드를 변경하는 과정에서 의도치 않게 발생하는 일을 줄이면서 시스템의 유연성을 향상시킬 수 있다.
  • 이것이 바로 디자인 원칙 중 하나인 '변하는 부분과 변하지 않는 부분을 분리하라' 는 원칙이다.
// 지불 방법을 위한 인터페이스 정의
interface PaymentStrategy {
    void pay(int amount);
}

// 신용카드로 지불하는 전략
class CreditCardPayment implements PaymentStrategy {
    private String name;
    private String cardNumber;

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

    @Override
    public void pay(int amount) {
        System.out.println(amount + " is paid with credit card.");
    }
}

// 페이팔로 지불하는 전략
class PaypalPayment implements PaymentStrategy {
    private String email;

    public PaypalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + " is paid using PayPal.");
    }
}

// 결제를 처리하는 클라이언트 클래스
class ShoppingCart {
    PaymentStrategy paymentMethod;

    public void setPaymentMethod(PaymentStrategy paymentMethod) {
        this.paymentMethod = paymentMethod;
    }

    public void checkout(int amount) {
        paymentMethod.pay(amount);
    }
}

// 메인 클래스
public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        // 신용카드로 결제
        cart.setPaymentMethod(new CreditCardPayment("John Doe", "1234567890"));
        cart.checkout(100);

        // 페이팔로 결제
        cart.setPaymentMethod(new PaypalPayment("john@example.com"));
        cart.checkout(200);
    }
}

 

위 코드에서는 특정 기능이나 동작의 구체적인 구현 세부사항을 숨기고 있습니다. PaymentStrategy 인터페이스를 통해 다양한 지불 방식의 구체적인 로직을 클라이언트 코드로 부터 분리하고 클라이언트는 이 인터페이스를 통해 결제 방식을 다룹니다. 실제로 어떤 결제 방식이 사용되었는지 ( CreditCardPayment, PaypalPayment ) 클라이언트 코드는 알 필요가 없습니다.

// 결제를 처리하는 클라이언트 클래스
class ShoppingCart {
    PaymentStrategy paymentMethod;

    public void setPaymentMethod(PaymentStrategy paymentMethod) {
        this.paymentMethod = paymentMethod;
    }

    public void checkout(int amount) {
        paymentMethod.pay(amount);
    }
}

 

디자인 원칙 2

"구현보다는 인터페이스에 맞춰서 프로그래밍한다."

 

이 개념의 핵심은 실제 실행시에 쓰이는 객체가 코드에 고정되지 않도록 상위 형식(supertype)에 맞춰 프로그래밍해서 다형성을 활용한다는 점에 있다. 그리고 보통 "상위 형식에 맞춰서 프로그래밍하라"는 원칙은 "변수를 선언할 때 보통 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 한다. 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다. 그러면 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다" 라는 뜻으로 생각하면 된다.

// 인터페이스 정의
interface Vehicle {
    void drive();
    void stop();
}

// 인터페이스를 구현하는 자동차 클래스
class Car implements Vehicle {
    public void drive() {
        System.out.println("Car is driving.");
    }
    public void stop() {
        System.out.println("Car has stopped.");
    }
}

// 인터페이스를 구현하는 오토바이 클래스
class Motorcycle implements Vehicle {
    public void drive() {
        System.out.println("Motorcycle is driving.");
    }
    public void stop() {
        System.out.println("Motorcycle has stopped.");
    }
}

// 메인 클래스
public class Main {
    public static void main(String[] args) {
        Vehicle myVehicle = new Car(); // 인터페이스 타입으로 자동차 객체 생성
        operateVehicle(myVehicle);

        myVehicle = new Motorcycle(); // 같은 인터페이스 타입으로 오토바이 객체 생성
        operateVehicle(myVehicle);
    }

    // 인터페이스 타입을 매개변수로 받는 메소드
    public static void operateVehicle(Vehicle vehicle) {
        vehicle.drive();
        vehicle.stop();
    }
}

 

 

 

디자인 원칙 3

"상속 보다는 구성을 활용한다"

 

이 원칙은 객체 지향 프로그래밍에서 "구성 우선(Composition over inheritance)" 이라 고도 하며, 코드 재사용성을 높이기 위해 상속 대신 객체 구성을 사용하는 것을 권장합니다. 이 원칙은 클래스 계층이 복잡해지고 취약해지는 문제를 피하기 위해 사용 됩니다.

 

// 엔진 기능을 정의하는 인터페이스
interface Engine {
    void start();
    void stop();
}

// 가솔린 엔진 구현
class GasolineEngine implements Engine {
    public void start() {
        System.out.println("Gasoline engine starts.");
    }
    
    public void stop() {
        System.out.println("Gasoline engine stops.");
    }
}

// 전기 엔진 구현
class ElectricEngine implements Engine {
    public void start() {
        System.out.println("Electric engine starts.");
    }
    
    public void stop() {
        System.out.println("Electric engine stops.");
    }
}

// 차량 클래스
class Car {
    private Engine engine; // 엔진을 구성 요소로 포함

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void startCar() {
        engine.start();
    }

    public void stopCar() {
        engine.stop();
    }
}

// 메인 클래스
public class Main {
    public static void main(String[] args) {
        Engine gasolineEngine = new GasolineEngine();
        Car gasCar = new Car(gasolineEngine);
        gasCar.startCar();
        gasCar.stopCar();

        Engine electricEngine = new ElectricEngine();
        Car electricCar = new Car(electricEngine);
        electricCar.startCar();
        electricCar.stopCar();
    }
}

 

이 예제에서 Car 클래스는 구체적인 엔진 타입을 상속 받는 대신, Engine 인터페이스 타입의 객체를 멤버 변수로 가지고 있습니다. 이를 통해, 차량이 어떤 종류의 엔진을 사용할지 결정하는 것은 인스턴스 생성 시 주입된 엔진 객체에 의존하게 됩니다. 이로써 Car 클래스는 엔진의 구체적인 구현으로 부터 독립적이며, 필요에 따라 다른 유형의 엔진으로 쉽게 교체할 수 있습니다.

 

 

이제 위 세 가지 방법을 통합하여 코드를 작성해 보겠습니다.

오리들은 모두 Duck를 확장해서 만들고, 나는 행동은 FlyBehavior를, 꽥꽥거리는 행동은 QuackBehavior를 구현해서 만듭니다.

위처럼 구현하게 되면 달라지는 부분은 나머지 코드에 영향을 주지 않도록 캡슐화 하고 변하지 않는 부분은 그대로 두어 사용하게끔 구현 되었습니다.  새로운 오리를 구현할 경우 Duck를 상속 받고 해당 오리의 FlyBehavior, QuachBehavior을 구현하면됩니다. 이렇게 구현된 행동은 클라이언트 코드에서 Duck 에 포함된 set메서드를 통해서 객체를 주입하여 사용할 수 있습니다.

 

Duck 구현

public abstract class Duck {

    QuackBehavior quackBehavior;
    FlyBehavior flyBehavior;
    
    public void swim() {
        System.out.println("Swim");
    }

    abstract public void display();

    public void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

    public void performQuack() {
        quackBehavior.quack();
    }

    public void performFly() {
        flyBehavior.fly();
    }
}

 

나는 행동 구현

public interface FlyBehavior {

    public void fly();
}

public class FlyWithWings implements FlyBehavior {

    @Override
    public void fly() {
        System.out.println("FlyWithWings");
    }
}


public class FlyNoWay implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("FlyNoWay");
    }
}

 

소리 내는 행동 구현

public interface QuackBehavior {

    public void quack();
}

public class Quack implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("Quack");
    }
}

public class Squeak implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("Squeak");
    }
}

 

테스트

public class DuckInterfaceMain2 {
    public static void main(String[] args) {
        Duck mallardDuck = new MallardDuck();
        mallardDuck.setFlyBehavior(new FlyWithWings());
        mallardDuck.setQuackBehavior(new Quack());
        mallardDuck.performQuack();
        mallardDuck.performFly();

        Duck modelDuck = new DecoDuck();
        modelDuck.setFlyBehavior(new FlyRocketPowered());
        modelDuck.setQuackBehavior(new MuteQuack());
        modelDuck.performQuack();
        modelDuck.performFly();
    }
}

 

위 코드와 클래스 다이어그램에서 나오는 내용을 간략히 요약하면 아래와 같습니다.

  • 디자인 원칙 1 : 바뀌는 부분과 바뀌지 않는 부분을 분리하라 
    • 변하는 부분은 따로 뽑아 캡슐화하고 변하지 않는 부분은 그대로 유지
    • 변하는 부분 "나는 행동", "소리 내는 행동" 을 따로 때어 인터페이스로 만들고 구현체를 만듭니다.
      • 이렇게 하면 확장에는 열려있고 변경에는 닫혀있게 됩니다.
      • FlyBehavior flyBehavior = new FlyNoWay()  실제 코드는 인터페이스를 받으므로 구현체는 몰라도 됩니다.
      • 그러므로 구체적인 구현은 각 전략 클래스 내에 캡슐화 되어 있습니다.
  • 디자인 원칙 2 : 구현 보다는 인터페이스에 맞춰 프로그래밍 한다.
    • 이 원칙의 핵심은 "상위 형식(supertype)에 맞춰 프로그래밍해서 다형성을 활용한다." 입니다.
    • FlyBehavior flyBehavior = new FlyNoWay() 이렇게 디자인 원칙 1과 매우 유사합니다.
  • 디자인 원칙 3 : 상속보다는 구성을 활용한다.
    • 쉽게 설명하면 멤버 변수를 두어 객체를 주입 받는 형식입니다.
    • mallardDuck.setFlyBehavior(new FlyWithWings());
    • 위 코드처럼 duck에 상위 타입(인터페이스)를 두어 해당 클래스의 set 메소드 또는 생성자를 통해 주입 하는 형식입니다.

 

지금 까지 다룬 내용은 디자인 패턴중의 하나인 전략 패턴에 해당합니다. 전략 패턴이란 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.