전략 패턴
전략 패턴은 정책 패턴이라고하며, 객체의 행위를 정의하는 알고리즘을 각각의 클래스 즉 "전략"으로 분리하고, 그 행위를 동적으로 변경할 수 있도록 해주는 디자인 패턴으로써, 해당 패턴을 사용한다면 코드의 유연성을 높이고 유지보수성을 개선할 수 있습니다. 특히 다양한 알고리즘을 사용하여야 하는 상황에서 조건문을 사용하는 복잡한 코드 대신, 전략 패턴을 사용하면 알고리즘을 쉽게 교체할 수 있습니다.
정리하자면 객체의 행위를 바꾸고 싶은 경우 '직접' 수정하지 않고 전략이라고 부르는 캡슐화한 알고리즘을 컨텍스트 안에서 바꿔주면서 상호교체가 가능하게 만드는 패턴입니다.
컨테스트란? 전략을 사용하는 객체를 의미합니다. 즉, 실제로 알고리즘을 실행하는 주체가 되는 클래스

https://refactoring.guru/ko 해당 사이트에서는 많은 디자인 패턴과 SOLID 개념들을 이해하게 하는 데 큰 도움을 받았습니다. 여기서 이해한 내용을 바탕으로 정리를 해보자면,
만일 여러분들이 여행자들을 위한 내비게이션 앱을 개발할 때, 앱에서 다양한 경로 계획을 지원하고 싶다고 가정합시다. 차로 이동하는 경로, 도보 경로, 자전거 경로, 대중교통 경로 등등 여러 옵션을 제공하려면 각 경로 계획 알고리즘을 전략 패턴을 사용하여 별도로 구현할 수 있습니다. 사용자는 필요에 따라 경로 선택을 다르게 할 수 있으며, 알고리즘 자체는 전략으로 분리되어 앱 내비게이션 클래스는 각 알고리즘의 구체적인 내용에 의존하지 않게 됩니다.
여기서 전략 패턴의 정의를 하자면 결국 객체의 행위를 동적으로 변경할 수 있게 해주는 디자인 패턴입니다. 이 패턴은 비즈니스 로직에서 알고리즘을 캡슐화하여, 해당 알고리즘을 구체저인 클래스에서 독립적으로 구현한 후 동적으로 변경할 수 있도록 하는 것인데요, 이를 통해 조건문이나 상속으로 해결하던 복잡한 알고리즘 처리 문제를 해결할 수 있습니다
무슨 말인지 이해가 잘 안 가실텐데, 코드로 보면 훨씬 이해가 편합니다.
1) 전략 인터페이스 구성
public interface RouteStrategy {
void buildRoute(String startPoint, String endPoint);
}
2) 해당 전략에 맞는 각 알고리즘 전략 구성
// 자동차 경로 전략
public class CarRouteStrategy implements RouteStrategy {
@Override
public void buildRoute(String startPoint, String endPoint) {
System.out.println("차량을 이용한 경로를 계산합니다: " + startPoint + " -> " + endPoint);
}
}
// 도보 경로 전략
public class WalkingRouteStrategy implements RouteStrategy {
@Override
public void buildRoute(String startPoint, String endPoint) {
System.out.println("도보로 이동하는 경로를 계산합니다: " + startPoint + " -> " + endPoint);
}
}
// 자전거 경로 전략
public class BicycleRouteStrategy implements RouteStrategy {
@Override
public void buildRoute(String startPoint, String endPoint) {
System.out.println("자전거를 이용한 경로를 계산합니다: " + startPoint + " -> " + endPoint);
}
}
3) 컨텍스트 클래스 구성, 해당 컨텍스트는 위에서 말했듯이 전략을 사용하는 객체를 의미합니다. 그러니 클라이언트 입장에서는 전략들을 바라보는 것이 아니라, 해당 전략을 이용하고 주무르는 컨텍스트를 이용하여서 객체를 생성하겠죠?
public class Navigator {
private RouteStrategy routeStrategy;
// 전략 설정
public void setRouteStrategy(RouteStrategy routeStrategy) {
this.routeStrategy = routeStrategy;
}
// 경로 계산 요청
public void calculateRoute(String startPoint, String endPoint) {
routeStrategy.buildRoute(startPoint, endPoint);
}
}
4) 클라이언트 입장에서의 코드 - 결국 클라이언트는 원하는 전략에 맞게 계산을 알맞게 원하는 결과를 얻었습니다.
public class Main {
public static void main(String[] args) {
Navigator navigator = new Navigator();
// 차로 이동하는 경로
navigator.setRouteStrategy(new CarRouteStrategy());
navigator.calculateRoute("서울", "부산");
// 도보로 이동하는 경로
navigator.setRouteStrategy(new WalkingRouteStrategy());
navigator.calculateRoute("서울", "부산");
// 자전거로 이동하는 경로
navigator.setRouteStrategy(new BicycleRouteStrategy());
navigator.calculateRoute("서울", "부산");
}
}
전략 패턴의 장점
행위의 독립성: 알고리즘을 별도의 클래스로 캡슐화하여, 행위에 대한 독립성을 보장합니다
유연한 확장성: 새로운 전략을 추가할 때 기존 코드를 수정할 필요 없이 새로운 알고리즘을 쉽게 추가할 수 있습니다
유지보수성 향상: 코드 수정이 최소화되어 유지보수가 용이하며 특히, 알고리즘을 자주 변경하는 상황에서 효과적입니다
OCP(개방-폐쇄 원칙) 준수: 코드의 수정 없이 새로운 알고리즘을 추가할 수 있어 개방-폐쇄 원칙을 준수할 수 있습니다.
전략 패턴의 단점
복잡성 증가: 알고리즘마다 별도의 클래스를 만들어야 하므로, 클래스와 인터페이스가 많아지면서 복잡도가 증가합니다
전략 선택의 책임: 클라이언트 코드에서 적절한 전략을 선택하고 주입해야 하므로, 클라이언트가 알고리즘에 대한 세부적인 차이를 알고 있어야 합니다.
자 그렇다면 전략 패턴을 언제 사용하는 것이 좋을까요? 이렇게 분류하면 됩니다.
| 동일한 작업을 여러 가지 방법으로 처리해야 할 때 |
| 객체의 행위를 동적으로 변경해야 할 때. |
| 조건문이나 분기문으로 알고리즘을 선택하는 복잡한 구조를 제거하고 싶을 때. |
| 알고리즘이 자주 변경되거나 추가될 가능성이 있을 때. |
이렇게 말하면 잘 와닿지 않을 수가 있는데요, 전략 패턴의 실제 적용 예시로는 다양한 이동 수단에 맞게 제작되어야 하는 내비게이션 앱, 여러 결제 수단에 맞추어야 하는 결제 시스템, 게임 캐릭터의 행동을 전략 패턴으로 구현하여 상황에 따라 행동 방식을 달리하게 하는 등 여러 상황에서 사용이 될 수 있습니다
옵저버 패턴

https://refactoring.guru/ko/design-patterns/observer
옵저버 패턴은 주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 디자인 패턴입니다. 쉽게 스타크래프트 저그 유닛인 옵저버가 허구한날 훔쳐보고 커멘더에게 알려주는 걸 떠오르시면 됩니다
옵저버 패턴의 관계는 "1 대 다" 인 관계로 알 수 있는데요, 한 객체의 상태가 변경될 때, 그 객체에 의존하는 다른 객체들에게 자동으로 알림이 전달되며, 모든 관련 객체들이 그 상태에 따라 자동으로 갱신될 수 있습니다. 주로 이벤트 기반 시스템에서 많이 사용되며, 이벤트 발생 시 여러 객체들에게 변화를 통지하여 데이터를 동기화하는 역할을 합니다
옵저버 패턴의 개념에는 두 가지 주요 역할이 있습니다.
1) 주체: 상태를 관리하는 객체로, 해당 객체는 상태가 변경되면 관련된 옵저버들에게 알림을 보내며, 각 옵저버는 알림을 받아 적절히 대응합니다.
2) 옵저버: 주체의 상태를 감지하고 반응하는 객체로, 옵저버는 주체에 등록된 후, 주제의 상태가 변경되면 주체로부터 상태 변경에 대한 알림을 받아 자신의 상태를 갱신합니다
이것을 보면 주체와 옵저버는 강한 결합을 갖는 것이 아니라, 느슨한 결합을 갖는다는 것을 알 수 있습니다. 왜냐하면 주체는 옵저버가 구체적으로 어떤 행동을 하는 지 모르며, 단순히 상태 변화만을 통지하기 때문이죠
옵저버 패턴의 구조
옵저버 패턴의 코드를 보기에 앞서, 어떠한 요소로 이루어져서 해당 패턴이 완성되는 지를 먼저 살펴볼 필요가 있습니다.
주체의 상태 변경을 알리기 위해 옵저버들을 관리하는 인터페이스를 정의합니다.
옵저버 등록 메서드 - addObserver(Observer observer)
옵저버 제거 메서드 - removeObserver(Observer observer)
상태 변경 시 알람을 보내는 메서드 - notifyObservers()
옵저버는 주체 객체의 상태가 변경되면 이를 감지하고 자신을 업데이트하는 인터페이스를 정의합니다
상태 업데이트 메서드 - update()
구체적인 주체 : 주체 인터페이스를 구현하며 상태 변경 시 옵저버들에게 알림을 보냅니다
구체적인 옵저버 : 옵저버 인터페이스를 구현하며 주제로부터 상태 변경 알림을 받고 자신의 상태를 갱신합니다
옵저버 패턴의 구현
1) 옵저버 인터페이스 구현
public interface Observer {
void update(String message);
}
2) 주체 인터페이스 구현
import java.util.ArrayList;
import java.util.List;
public class Subject {
private List<Observer> observers = new ArrayList<>();
// 옵저버 등록
public void addObserver(Observer observer) {
observers.add(observer);
}
// 옵저버 제거
public void removeObserver(Observer observer) {
observers.remove(observer);
}
// 모든 옵저버에게 알림
public void notifyObservers(String message) {
for (Observer observer : observers) {
observer.update(message);
}
}
}
3) 구체적인 주체 클래스
public class NewsAgency extends Subject {
private String news;
public void setNews(String news) {
this.news = news;
notifyObservers(news);
}
}
4) 구체적인 옵저버 클래스
public class NewsSubscriber implements Observer {
private String name;
public NewsSubscriber(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + "님, 새로운 뉴스 알림: " + message);
}
}
5) 클라이언트 코드
public class Main {
public static void main(String[] args) {
NewsAgency agency = new NewsAgency();
NewsSubscriber subscriber1 = new NewsSubscriber("지코");
NewsSubscriber subscriber2 = new NewsSubscriber("기리보이");
agency.addObserver(subscriber1);
agency.addObserver(subscriber2);
agency.setNews("아무말이나 해 say SomeThing 뉴스");
agency.removeObserver(subscriber1); // 구독자 제거 가능
}
}
옵저버 패턴의 장점
느슨한 결합: 주체와 옵저버는 서로 독립적이기에 주체는 옵저버의 구체적인 구현을 알 필요가 없고, 옵저버도 주체의 내부 상태를 알 필요가 없습니다. 이를 통해 두 객체 간의 결합도를 줄일 수 있습니다
확장성: 새로운 옵저버를 추가하더라도 기존의 주체나 다른 옵저버들을 수정할 필요가 없습니다. 새로운 옵저버만 주제에 등록하면 자동으로 알림을 받을 수 있습니다
자동화: 주체의 상태가 변경되면, 모든 옵저버에게 자동으로 알림이 전달되어 자동으로 상태가 갱신됩니다. 수동으로 상태를 확인할 필요 없이 실시간으로 변화에 대응할 수 있습니다.
옵저버 패턴의 단점
성능 문제: 옵저버가 너무 많아지면, 주체의 상태가 변경될 때마다 모든 옵저버에게 알림을 보내는 작업이 성능에 영향을 줄 수 있습니다. 많은 옵저버가 동시에 갱신되면 성능 저하가 발생할 수 있습니다.
디버깅이 어렵다: 옵저버 패턴은 다양한 객체가 서로 독립적으로 상호작용하므로, 디버깅 과정에서 어떤 객체가 상태를 변경하고 있는지 추적하기 어려울 수 있습니다.
옵저버 패턴의 실제 사용 사례를 보면, 옵저버 패턴이 언제 어디에서 쓰이는 지 쉽게 파악이 가능할 것 같은데용, 옵저버 패턴은 주로 "이벤트 리스터", " 실시간 데이터 스트리밍 ", " SNS 알림 시스템" 등의 시스템을 구현하는 데 있어, 많이 쓰입니다
프록시 객체와 결합할 수 있겠는데?
옵저버 패턴의 변형 중 하나로 프로식 객체를 사용할 수 있습니다. 프로식 객체는 상태의 변화를 자동으로 감지하고 옵저버에게 그 상태 변화를 프로퍼티 체인지 이벤트로 통지하는 방법입니다.
이 접근 방식은 주로 객체의 프로퍼티 값이 변경될 때 자동으로 통지하고, 구독자가 해당 변화를 감지할 수 있도록 하는 데 사용됩니다. 이 방법은 옵저버 패턴을 보다 효율적으로 구현할 수 있게 해주며, 상태가 변경될 때 객체의 필드에 대한 구체적인 변화를 통지하는 구조입니다.
자바에서는 PropertyChangeSupport 클래스를 통해 프로퍼티의 변화를 옵저버에게 자동으로 알리도록 구성할 수 있죠 그렇다면 구조를 알아보자면
프로식 객체를 사용하는 옵저버 패턴의 구조는 다음과 같습니다:
PropertyChangeSupport: 주체가 상태를 변경할 때마다 변경 사항을 자동으로 통지하는 역할을 합니다
PropertyChangeListener: 옵저버 역할을 하며, 주체에서 발생한 프로퍼티 변경 사항을 수신합니다
1) 주체 클래스
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
public class NewsAgency {
private String news;
private final PropertyChangeSupport support;
public NewsAgency() {
this.support = new PropertyChangeSupport(this);
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
public void setNews(String news) {
String oldNews = this.news;
this.news = news;
support.firePropertyChange("news", oldNews, news);
} // 이전 값과 새 값을 모두 전달하여, 옵저버들이 이전 상태와 변경된 상태를 구분할 수 있게 하기 위함
}
2) 옵저버 클래스
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
public class NewsSubscriber implements PropertyChangeListener {
private String name;
public NewsSubscriber(String name) {
this.name = name;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.println(name + "에게 오는 새로운 이벤트 뉴스: " + evt.getNewValue());
}
}
3) 클라이언트 코드
public class Main {
public static void main(String[] args) {
NewsAgency agency = new NewsAgency();
NewsSubscriber subscriber1 = new NewsSubscriber("지코");
NewsSubscriber subscriber2 = new NewsSubscriber("기리보이");
agency.addPropertyChangeListener(subscriber1);
agency.addPropertyChangeListener(subscriber2);
agency.setNews("티켓 10월달 예매 가능~!");
}
}
프로식 객체를 이용한 옵저버 패턴은 Java에서 제공하는 PropertyChangeSupport와 PropertyChangeListener를 사용하여 프로퍼티 변화에 따른 자동화된 알림 시스템을 구현하는 방식입니다. 특히, 상태 변경을 보다 세밀하게 통제하고 자동으로 옵저버에게 알림을 보내는 기능을 통해 이벤트 기반 시스템에 매우 유용하게 활용될 수 있습니다.
기본 옵저버 패턴에 비해 복잡할 수 있지만, 상태 변경에 대한 관리가 중요한 경우나 프로퍼티 변경에 민감한 시스템에서 강력한 도구로 사용할 수 있습니다.
또한 Vue.js 3.0에서 옵저버 패턴을 이용을 하는 데 ref나 reactive로 정의하면 해당 값이 변경되었을 대 자동으로 DOM에 있는 값이 변경되는 데 , 앞서 설명한 프록시 객체를 이용한 옵저버 패턴을 이용하여 구현한 것입니다
'CS' 카테고리의 다른 글
| Redis는 싱글 스레드인데 동시성 관리가 필요할까? (1) | 2024.09.17 |
|---|---|
| 객체 지향 설계를 위한 디자인 패턴 정리 (프록시 패턴, 이터레이터 패턴) (0) | 2024.09.15 |
| 객체 지향 설계를 위한 디자인 패턴 정리 (싱글톤, 팩토리 패턴) (1) | 2024.09.14 |
| Redis란 무엇일까? - Redis의 개념과 자료구조 및 아키텍쳐 정리 (0) | 2024.09.13 |
| 오브젝트 (코드로 이해하는 객체지향 설계) 1장 정리본 (0) | 2024.07.06 |