전략 패턴 Strategy Pattern
상속과 구현
- 상속(extends)와 구현(implements)는 자바가 다형성을 위한 주요 기능 중 하나이다.
- 다만, 상속과 구현은 각각 장점과 단점을 가진다.
상속의 장점과 단점
- 장점은 구현된 메서드를 재사용할 수 있다.
- 단점은 슈퍼 클래스에 종속되어 변경에 취약하다.
public abstract class Bird {
String name;
public void fly(){
System.out.println(name + "이 날고 있다!");
}
}
public class Chicken extends Bird {
}
public class Duck extends Bird {
@Override
public void fly() {
System.out.println(getName() + "이는 집오리라서 날지 않아!");
}
}
- Bird가 미리 구현한 구현한 fly() 메서드를 재사용할 수 있다. Chicken은 특정 로직 없이 fly() 메서드를 사용할 수 있다.
- 만약 해당 로직이 맘에 들지 않는 경우, Duck처럼 오버라이딩 하면 된다.
public abstract class Bird {
public void fly(){
// 아래 로직이 변경되었다.
System.out.println(name + "은 비행 중이다!");
}
}
- 다만 슈퍼 클래스인 Bird가 위와 같이 변경될 경우 문제가 심각하다. Chicken은 해당 변경에 바로 영향을 받는다.
public class Ostrich extends Bird {
@Override
public void fly(){
throw new UnsupportedOperationException("날 수 없는 새도 있다.");
}
}
- 타조처럼 날지 못하는 새가 있다. 이런 경우 해당 메서드를 사용하지 못하도록 오버라이딩 해야할 수도 있다.
구현의 장점과 단점
- 구현의 장점은 필요로한 클래스에 필요로한 행동을 명시하고 구현하는 것에 있다.
- 구현의 단점은 동일한 메서드와 동일한 코드를 공유하더라도 이를 반복하여 작성해야 한다는 점이다.
public interface Flyable {
void fly();
}
public class Chicken implements Flyable{
@Override
public void fly() {
System.out.println(name + "이 날고 있다!");
}
}
public class Ostrich /* implements Flyable */ {
// 나는 행위가 존재하지 않는다. 그러므로 Flyable을 구현하지 않는다.
// public void fly(){}
}
public class Sparrow implements Flyable{
// Chicken#fly와 동일한 로직이 반복된다.
@Override
public void fly() {
System.out.println(name + "이 날고 있다!");
}
}
상속과 구현의 한계를 넘어 - 전략패턴
- 상속과 구현의 공통점은 데이터 타입이 슈퍼 클래스나 인터페이스에 따르는 것에 있다.
- 전략패턴은 클래스의 데이터 타입을 정하지 않는다. 그보다는 특정 행위를 필드를 통해 선언하고 사용한다. 해당 필드를 전략이라 한다.
- 행위의 구현을 전략이 대리한다.
- 전략은 다형성을 통해 여러 개가 작성되고 필요한 전략을 선택할 수 있다. 같은 전략을 여러 클래스가 재사용할 수 있다.
- 함수형 인테페이스로 구현하여 주입할 수도 있다.
public interface FlyBehavior {
void fly(String name);
}
public class Chicken {
private final String name;
private final FlyBehavior flyBehavior;
public Chicken(String name, FlyBehavior flyBehavior) {
this.name = name;
this.flyBehavior = flyBehavior;
}
public void fly(){
flyBehavior.fly(name);
}
}
- FlyBehavior가 전략이며 인터페이스로 작성되었다.
- Chicken은 FlyBehavior을 전략으로 하여 필드에 선언했다.
- Chicken의 fly 메서드를 FlyBehavior가 대신 처리한다.
- FlyBehavior 구현체는 생성자를 통해 주입받는다.
public class Ostrich {
String name;
}
- 더 이상 타조는 fly 메서드를 가지고 고민하지 않는다.
public class SuperFast implements FlyBehavior{
@Override
public void fly(String name) {
System.out.println(name + "가 완전 빠르게 난다~");
}
}
public class Main {
public static void main(String[] args) {
Chicken chicken = new Chicken("닭돌이", new SuperFast());
chicken.fly();
Chicken chicken2 = new Chicken(
"닭순이"
, name -> System.out.println(name + "가 오늘은 날기 싫다네 ㅠ 힝.."));
chicken2.fly();
}
}
- 필요로한 전략을 작성할 수 있다. 그리고 재사용할 수 있다.
- 인터페이스의 메서드가 하나면 익명함수나 람다로 구현할 수 있다.
전략 패턴이란
- 전략 패턴이란 클라이언트로부터 특정 알고리즘을 캡슐화하고 분리하는 패턴이다.
- 필요로 한 알고리즘을 확장하고 선택할 수 있다.
- 상속(Inheritance)이 아닌 위임(Delegation)으로 구현한다.
- 클라이언트 객체는 전략에 해당하는 인터페이스를 소유하며, 외부는 전략의 구현체를 선택 및 주입한다.
- 런타임 시점에서 전략을 선택할 수 있다.
- OCP를 준수한다. 전략이 변경되더라도 클라이언트 코드는 변경되지 않는다.
- 스프링 빈과 주입은 전략패턴의 대표적인 사용 방법이다. 스프링의 수많은 코드가 전략 패턴을 사용한다.
참조
- 백기선, 디자인 패턴 강의(https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4)
- O’Reilly, “헤드 퍼스트 디자인 패턴”