아래 교재를 바탕으로 디자인 패턴을 정리한 글입니다
헤드 퍼스트 디자인 패턴 | 에릭 프리먼 - 교보문고
헤드 퍼스트 디자인 패턴 | 유지관리가 편리한 객체지향 소프트웨어 만들기! 『헤드 퍼스트 디자인 패턴(개정판)』 한 권이면 충분합니다!이유 1. 흥미로운 이야기와 재치 넘치는 구성이 담긴 〈
product.kyobobook.co.kr
전략 패턴 정의
동일한 계열의 알고리즘들(알고리즘 군)을 정의하고 캡슐화해서 각각의 알고리즘 군을 수정해서 쓸 수 있게 한다.
클라이언트는 독립적으로 원하는 알고리즘을 선택하여 사용할 수 있으며,
클라이언트에 영향 없이 알고리즘의 변경이 가능하다.
알고리즘 군 : 동일한 목적을 달성하기 위한 다양한 방식들의 집합
예시 : 데이터를 암호화하는 목적을 가진 암호화 방식들(알고리즘 군: AES 암호화, RSA 암호화, SHA 해시)
참고로 GoF의 디자인 패턴의 생성/구조/행위 패턴 중 행위(행동) 패턴에 해당된다.
전략 패턴의 정의를 한눈에 이해하기 위해,
교재에 나오는 오리 시뮬레이션 게임 예제를 기반으로 전략 패턴을 아래 UML 다이어그램으로 나타낼 수 있다.
여기서, 나는 행동과 꽥꽥거리는 행동 각 집합을 알고리즘 군으로 볼 수 있다.
해당 내용을 코드로 살펴보면 아래와 같다
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() { }
public abstract void display();
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void setFlyBehavior(FlyBehavior fb) {
this.flyBehavior fb;
}
public void setFlyBehavior(QuackBehavior qb) {
this.quackBehavior = qb;
}
public void swim() {
System.out.println("모든 오리는 물에 뜹니다.");
}
}
public interface FlyBehavior {
public void fly();
}
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("날고 있어요");
}
}
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("못 날아요");
}
}
public interface QuackBehavior {
public void quack();
}
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("꽥");
}
}
public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("조용");
}
}
public class Squeak implements QuackBehavior {
public void quack() {
System.out.println("삑");
}
}
예시로 Duck 추상 클래스를 상속 받은 몇몇 클래스들을 구현해보면,
날 수 있고, 꽥꽥 우는 행동을 하는 청둥오리(MallardDuck) 클래스를 아래와 같이 구현할 수 있다.
만약 날지 못하고, 삑삑 우는 고무 오리(RubberDuck)라면, 조건에 맞게 구현할 수 있다.
public class MallardDuck extends Duck {
public MallardDuck() {
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
public void display() {
System.out.println("저는 청둥 오리입니다");
}
}
public class RubberDuck extends Duck {
public RubberDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new Squeak();
}
public void display() {
System.out.println("저는 고무 오리입니다");
}
}
이처럼 행동을 인터페이스로 분리하고, 구체적인 구현을 주입할 수 있어
클라이언트는 독립적으로 각 조건에 맞는 알고리즘을 선택하여 사용할 수 있다.
또한 상속 받는 다양한 클래스들에 대해서 코드 중복 제거와 재사용성이 향상됨을 알 수 있다.
전략 패턴의 장점
교재를 통해 전략 패턴의 장점을 정리해보면 다음과 같다
장점 | 설명 |
행동을 캡슐화하여 변경에 유연함 | 알고리즘(행동)을 객체로 분리했기 때문에, 런타임에 전략을 바꿀 수 있음 (ex. setBehavior()) |
코드 재사용성 증가 | 공통되는 알고리즘을 별도의 전략 클래스로 만들어, 여러 객체에서 재사용 가능 |
OCP(Open-Closed Principle) 준수 |
기존 코드를 수정하지 않고 새로운 전략을 추가하여 확장 가능함 |
클라이언트 코드 단순화 | 클라이언트는 어떤 전략이 사용되는지만 알면 되고, 내부 알고리즘 구현을 몰라도 됨 |
상속 대신 구성(composition)을 활용 | 행동을 상속 대신 구성으로 위임하여 더 유연한 구조를 만듦 (A에는 B가 있다) |
전략 패턴의 단점
단점 | 설명 |
클래스 수 증가 | 각 전략을 별도의 클래스로 만들기 때문에 클래스가 많아짐 (전략이 많을수록 복잡도 증가) |
전략 간 데이터 공유 어려움 | 전략이 객체와 별도로 존재하므로, 필요한 상태(ex. 멤버 변수)에 접근하기 어렵거나 번거로움 |
지나치가 분리하면 오히려 over-engineering |
간단한 경우에도 무조건 전략 패턴을 사용하면 코드가 오히려 복잡해질 수 있음 |
실제 전략 패턴의 예시
그렇다면 전략 패턴은 실제로 어디에서 사용되고 있을까? 라는 궁금증에 예시를 찾아보게되었다.
Java의 Comparator<T> Interface
Comparator<T>는 객체의 정렬 기준을 외부에서 주입할 수 있도록 설계된 전략 인터페이스이다.
예를 들어, Collections.sort(List<T>, Comparator<T>) 메서드는 다양한 Comparator 구현체를 받아 정렬 방식을 유연하게 변경할 수 있다. 아래와 같이 Comparator를 사용한 예시 코드를 작성해보았다.
import java.util.*;
// 적용할 대상 클래스
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// 전략 1: 이름 기준 정렬
class NameComparator implements Comparator<Person> {
public int compare(Person p1, Person p2) {
return p1.name.compareTo(p2.name);
}
}
// 전략 2: 나이 기준 정렬
class AgeComparator implements Comparator<Person> {
public int compare(Person p1, Person p2) {
return Integer.compare(p1.age, p2.age);
}
}
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 32));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 28));
System.out.println("원본 리스트:");
System.out.println(people);
// 이름 기준 정렬 전략 적용
Collections.sort(people, new NameComparator());
System.out.println("\n이름 기준 정렬:");
System.out.println(people);
// 나이 기준 정렬 전략 적용
Collections.sort(people, new AgeComparator());
System.out.println("\n나이 기준 정렬:");
System.out.println(people);
}
}
원본 리스트:
[Alice (32), Bob (25), Charlie (28)]
이름 기준 정렬:
[Alice (32), Bob (25), Charlie (28)]
나이 기준 정렬:
[Bob (25), Charlie (28), Alice (32)]
그렇다면 좀 더 나아가서 백엔드 단에서 API를 만든다고 할 때, 어떻게 전략 패턴을 적용할 수 있을까 고민해보았는데
요즘 간편 결제나 신용카드와 같이 '결제 방식'에 대한 방법들이 정말 다양하게 존재한다.
이러한 점을 토대로 전략 패턴을 적용해볼 수 있지 않을까? 라는 생각이 들었다.
정말 간단하게 큰 틀로만 보면 아래와 비슷하지 않을까 싶다! (진짜 그냥 단순한 내 생각)
공통 인터페이스
public interface PaymentStrategy {
void pay(int amount);
}
구체적인 결제 전략 구현체들
public class CardPaymentStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("신용카드로 " + amount + "원을 결제했습니다.");
}
}
public class KakaoPayStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("카카오페이로 " + amount + "원을 결제했습니다.");
}
}
public class NaverPayStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("네이버페이로 " + amount + "원을 결제했습니다.");
}
}