공부해봅시당
[디자인패턴] 행동 패턴 - 전략 패턴 본문
의도
전략 패턴은 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환할 수 있도록 하는 행동 디자인 패턴
문제
여행자들을 위한 내비 앱을 만들기로 했다고 가정
사용자들이 어느 도시에서든 빠르게 방향을 잡을 수 있도록 도와주는 아름다운 지도가 앱의 중심 기능
앱에서 가장 많이 요청된 기능 중 하나는 자동 경로 계획 기능
사용자가 주소를 입력하면 지도에 표시된 해당 목적지로 가는 가장 빠른 경로를 볼 수 있는 기능
앱의 첫 번째 버전에서는 도로로 된 경로만을 만들 수 있었음
차를 타고 여행하는 사용자들은 만족
모든 사용자가 여가 중에 운전하는 걸 좋아하진 않았음
따라서 도보 경로를 만드는 옵션을 추가
바로 그다음에는 사람들이 경로에서 대중교통의 사용을 계획할 수 있도록 옵션을 추가
나중에는 자전거를 타는 사용자들을 위한 경로를 만들 계획을 세웠음
그다음에는 도시의 모든 관광 명소들을 지나는 경로를 만들 수 있는 또 다른 옵션을 추가할 계획을 세움
사업적인 측면에서 앱은 성공했지만, 기술적인 문제들이 많은 골칫거리를 야기했음
새 경로 구축 알고리즘을 추가할 때마다 내비게이터의 메인 클래스의 크기가 두 배로 늘어났으며, 어느 시점이 되자 내비 앱은 유지하기가 너무 어려워졌음
간단한 버그를 수정하거나 주행거리 점수를 살짝 조정하기 위해 알고리즘 중 하나를 변경하면 전체 클래스에 영향이 미쳐 이미 작동하는 코드에서 오류가 발생할 가능성이 높아졌음
팀워크가 비효율적으로 변함
앱 출시 직후 고용된 팀원들은 병합 충돌을 해결하는 데 너무 많은 시간을 할애해야 한다고 불평함
새로운 기능을 구현하려면 거대한 동일 클래스를 변경해야 했는데, 이렇게 바꾼 내용들이 다른 팀원들이 생성한 코드와 충돌하곤 했음
해결책
전략 패턴은 특정 작업을 다양한 방식으로 수행하는 클래스를 선택한 후 모든 알고리즘을 전략들(strategies)이라는 별도의 클래스들로 추출할 것을 제안
콘텍스트(context)라는 원래 클래스에는 전략 중 하나에 대한 참조를 저장하기 위한 필드가 있어야 함
콘텍스트는 작업을 자체적으로 실행하는 대신 연결된 전략 객체에 위임
콘텍스트는 작업에 적합한 알고리즘을 선택할 책임이 없음
대신 클라이언트가 원하는 전략을 콘텍스트에 전달
사실, 콘텍스트는 전략들에 대해 많이 알지 못함
콘텍스트는 같은 일반 인터페이스를 통해 모든 전략과 함께 작동하며, 이 일반 인터페이스는 선택된 전략 내에 캡슐화된 알고리즘을 작동시킬 단일 메서드만 노출
이렇게 하면 콘텍스트가 구상 전략들에 의존하지 않게 되므로 콘텍스트 또는 다른 전략들의 코드를 변경하지 않고도 새 알고리즘들을 추가하거나 기존 알고리즘들을 수정할 수 있음
내비 앱에서 각 경로 구축 알고리즘을 단일 buildRoute 메서드를 사용하여 자체 클래스로 추출할 수 있음
이 메서드는 출발지와 목적지를 받은 후 경로의 체크포인트들의 컬렉션을 반환함
같은 인수가 주어졌더라도 각 경로 구축 클래스는 다른 경로를 구축할 수 있지만 주 내비게이터 클래스는 어떤 알고리즘이 선택되었는지 별로 신경 쓰지 않음
주 내비게이터 클래스의 주요 작업은 지도에 체크포인트들의 집합을 렌더링하는 것이기 때문
이 클래스에는 활성 경로 구축 전략을 전환하는 메서드가 있어, 클래스의 클라이언트들이 (예: 사용자 인터페이스의 버튼들) 현재 선택된 경로 구축 행동들을 다른 행동으로 대체할 수 있음
실제상황 적용
공항에 가야 한다고 상상
버스를 탈 수도 있고, 택시나 자전거를 탈 수도 있음
이것들이 바로 운송 전략들
예산이나 시간 제약 등을 고려하여 이러한 전략 중 하나를 선택할 수 있음
구조
위 사진의 번호와 대응되도록 아래 설명 작성함
- 콘텍스트: 구상 전략 중 하나에 대한 참조를 유지하고 전략 인터페이스를 통해서만 이 객체와 통신
- 전략 인터페이스: 모든 구상 전략에 공통이며, 콘텍스트가 전략을 실행하는 데 사용하는 메서드를 선언
- 구상 전략들: 콘텍스트가 사용하는 알고리즘의 다양한 변형들을 구현
- 콘텍스트는 알고리즘을 실행해야 할 때마다 연결된 전략 객체의 실행 메서드를 호출. 콘텍스트는 알고리즘이 어떻게 실행되는지와 자신이 어떤 유형의 전략과 함께 작동하는지를 모름
- 클라이언트: 특정 전략 객체를 만들어 콘텍스트에 전달. 콘텍스트는 클라이언트들이 런타임에 콘텍스트와 관련된 전략을 대체할 수 있도록 하는 세터(setter)를 노출
의사코드
이 예시에서의 콘텍스트는 여러 전략들을 사용하여 다양한 산술 연산들을 실행
// 전략 인터페이스는 어떤 알고리즘의 모든 지원 버전에 공통적인 작업을 선언합니다.
// 콘텍스트는 이 인터페이스를 사용하여 구상 전략들에 의해 정의된 알고리즘을
// 호출합니다.
interface Strategy is
method execute(a, b)
// 구상 전략들은 기초 전략 인터페이스를 따르면서 알고리즘을 구현합니다. 인터페이스는
// 그들이 콘텍스트에서 상호교환할 수 있게 만듭니다.
class ConcreteStrategyAdd implements Strategy is
method execute(a, b) is
return a + b
class ConcreteStrategySubtract implements Strategy is
method execute(a, b) is
return a - b
class ConcreteStrategyMultiply implements Strategy is
method execute(a, b) is
return a * b
// 콘텍스트는 클라이언트들이 관심을 갖는 인터페이스를 정의합니다.
class Context is
// 콘텍스트는 전략 객체 중 하나에 대한 참조를 유지합니다. 콘텍스트는 전략의
// 구상 클래스를 알지 못하며, 전략 인터페이스를 통해 모든 전략과 함께
// 작동해야 합니다.
private strategy: Strategy
// 일반적으로 콘텍스트는 생성자를 통해 전략을 수락하고 런타임에 전략이 전환될
// 수 있도록 세터도 제공합니다.
method setStrategy(Strategy strategy) is
this.strategy = strategy
// 콘텍스트는 자체적으로 여러 버전의 알고리즘을 구현하는 대신 일부 작업을 전략
// 객체에 위임합니다.
method executeStrategy(int a, int b) is
return strategy.execute(a, b)
// 클라이언트 코드는 구상 전략을 선택하고 콘텍스트에 전달합니다. 클라이언트는 올바른
// 선택을 하기 위해 전략 간의 차이점을 알고 있어야 합니다.
class ExampleApplication is
method main() is
Create context object.
Read first number.
Read last number.
Read the desired action from user input.
if (action == addition) then
context.setStrategy(new ConcreteStrategyAdd())
if (action == subtraction) then
context.setStrategy(new ConcreteStrategySubtract())
if (action == multiplication) then
context.setStrategy(new ConcreteStrategyMultiply())
result = context.executeStrategy(First number, Second number)
Print result.
적용
- 전략 패턴은 객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶을 때, 그리고 런타임 중에 한 알고리즘에서 다른 알고리즘으로 전환하고 싶을 때 추천
- 또 전략 패턴은 객체의 행동들을 특정 하위 행동들을 다양한 방식으로 수행할 수 있는 다른 하위 객체들과 연관시켜 객체의 행동들을 런타임에 간접적으로 변경할 수 있게 해줌
- 전략 패턴은 일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스들이 많은 경우에 추천
- 전략 패턴은 다양한 행동들을 별도의 클래스 계층구조로 추출하고 원래 클래스들을 하나로 결합하여 중복 코드를 줄일 수 있게 해줌
- 해당 로직의 콘텍스트에서 중요하지 않은 알고리즘의 구현 세부사항과 클래스의 비즈니스 로직을 분리하고 싶을 때 추천
- 전략 패턴은 코드의 나머지 부분에서 해당 코드, 내부 데이터, 그리고 다양한 알고리즘들의 의존 관계들 분리 가능.
- 다양한 클라이언트들이 알고리즘들을 실행하고 런타임에 전환하기 위한 간단한 인터페이스를 얻을 수 있음
- 클래스에 대규모 조건문이 있는 경우, 해당 알고리즘의 여러 조건 사이를 전환하는 패턴을 사용할 때 추천
- 전략 패턴을 사용하면 모든 알고리즘을 같은 인터페이스를 구현하는 별도의 클래스들로 추출하여 이러한 조건문을 제거할 수 있음
- 원래 객체는 알고리즘의 모든 변형들을 구현하는 대신 이러한 객체들 중 하나에 실행을 위임
구현방법
- 콘텍스트 클래스에서 자주 변경되는 알고리즘 식별. 런타임에 같은 알고리즘의 변형을 선택한 후 실행하는 대규모 조건문일 수도 있음.
- 알고리즘의 모든 변형에 공통인 전략 인터페이스 선언
- 하나씩 모든 알고리즘을 자체 클래스들로 추출한 후 모두 전략 인터페이스 구현
- 콘텍스트 클래스에서 전략 객체에 대한 참조를 저장하기 위한 필드를 추가한 후, 해당 필드의 값을 대체하기 위한 세터 제공. 콘텍스트는 전략 인터페이스를 통해서만 전략 객체와 작동해야 함. 콘텍스트는 인터페이스를 정의할 수 있으며, 이 인터페이스는 전략이 콘텍스트의 데이터에 접근할 수 있도록 함.
- 콘텍스트의 클라이언트들은 콘텍스트를 적절한 전략과 연관시켜야 함. 이러한 전략은 클라이언트들이 기대하는 콘텍스트가 주 작업을 수행하는 방식과 일치해야 함
장단점
장점
- 런타임에 한 객체 내부에서 사용되는 알고리즘들을 교환할 수 있음
- 알고리즘의 구현 세부사항을 사용하는 코드로부터 분리할 수 있음
- 상속을 합성으로 대체할 수 있음
- 합성(Composition, 링크 참고)
- 여러 객체를 합하여 다른 하나로 만드는 것
- 어떤 객체가 다른 객체의 일부분인 경우
- 자동차가 엔진, 트랜스미션, 헤드라이트 등으로 구성되는 것 같은 경우
- 합성(Composition, 링크 참고)
- 개방/폐쇄 원칙. 콘텍스트를 변경하지 않고도 새로운 전략들을 도입할 수 있음
단점
- 알고리즘이 몇 개밖에 되지 않고 거의 변하지 않는다면, 패턴과 함께 사용되는 새로운 클래스들과 인터페이스들로 프로그램을 지나치게 복잡하게 만들 이유가 없음
- 클라이언트들은 적절한 전략을 선택할 수 있도록 전략 간의 차이점들을 알고 있어야 함
- 현대의 많은 프로그래밍 언어에는 익명 함수들의 집합 내에서 알고리즘의 다양한 버전들을 구현할 수 있는 함수형 지원이 있으며, 클래스들과 인터페이스들을 추가하여 코드의 부피를 늘리지 않으면서도 전략 객체를 사용했을 때와 똑같이 사용할 수 있음
다른 패턴과의 관계
- 브리지, 상태, 전략 패턴은 매우 유사한 구조로 되어 있으며, 어댑터 패턴도 이들과 어느 정도 유사한 구조로 되어 있음
- 위 모든 패턴은 다른 객체에 작업을 위임하는 합성을 기반으로 함
- 하지만 이 패턴들은 모두 다른 문제들을 해결
- 패턴은 특정 방식으로 코드의 구조를 짜는 레시피가 아님
- 왜냐하면 패턴은 해결하는 문제를 다른 개발자들에게 전달할 수도 있기 때문
- 커맨드와 전략 패턴
- 공통점
- 둘 다 어떤 작업으로 객체를 매개변수화하는 데 사용할 수 있음
- 차이점: 의도가 매우 다름
- 커맨드
- 모든 작업을 객체로 변환할 수 있음
- 작업의 매개변수들은 해당 객체의 필드들이 됨
- 이 변환은 작업의 실행을 연기하고, 해당 작업을 대기열에 넣고, 커맨드들의 기록을 저장한 후 해당 커맨드들을 원격 서비스에 보내는 등의 작업을 가능하게 함
- 전략 패턴
- 일반적으로 같은 작업을 수행하는 다양한 방법을 설명
- 단일 콘텍스트 클래스 내에서 이러한 알고리즘들을 교환할 수 있도록 함
- 커맨드
- 공통점
- 데코레이터와 전략 패턴 비유
- 템플릿 메소드와 전략 패턴
- 상태와 전략
코드 예시 - Java
전략 패턴은 자바 코드에서 매우 일반적
이 패턴은 다양한 프레임워크에서 사용자들이 클래스를 확장하지 않고 클래스의 행동을 변경할 수 있도록 자주 사용됨
자바 8은 람다 함수를 지원하기 시작했으며, 해당 함수는 전략 패턴의 더 간단한 대안으로 사용될 수 있음
코어 자바 라이브러리로부터 가져온 전략 패턴의 몇 가지 예시들
- Collections#sort()로부터 호출된 java.util.Comparator#compare().
- javax.servlet.http.HttpServlet: service() 메서드, 또 HttpServletRequest 와 HttpServletResponse 객체들을 매개변수로 받는 doXXX()의 모든 메서드.
- javax.servlet.Filter#doFilter()
전략 패턴은 중첩된 객체가 실제 작업을 수행할 수 있도록 하는 메서드가 있으며 또 해당 객체를 다른 객체로 대체할 수 있는 세터가 있음
전자상거래 앱의 결제 메소드
전략 패턴은 전자 상거래 앱의 다양한 지불 메서드들을 구현하는 데 사용
구매할 제품을 선택한 후 고객은 페이팔 또는 신용 카드 지불 메서드를 선택
구상 전략들은 실제 지불을 수행할 뿐만 아니라 체크아웃 양식의 행동을 변경하여 지불 세부 정보들을 기록하는 적절한 필드들을 제공
strategies
strategies/PayStrategy.java: 결제 메서드들의 공통 인터페이스
package refactoring_guru.strategy.example.strategies;
/**
* Common interface for all strategies.
*/
public interface PayStrategy {
boolean pay(int paymentAmount);
void collectPaymentDetails();
}
strategies/PayByPayPal.java: 페이팔을 통한 결제
package refactoring_guru.strategy.example.strategies;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
/**
* Concrete strategy. Implements PayPal payment method.
*/
public class PayByPayPal implements PayStrategy {
private static final Map<String, String> DATA_BASE = new HashMap<>();
private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in));
private String email;
private String password;
private boolean signedIn;
static {
DATA_BASE.put("amanda1985", "amanda@ya.com");
DATA_BASE.put("qwerty", "john@amazon.eu");
}
/**
* Collect customer's data.
*/
@Override
public void collectPaymentDetails() {
try {
while (!signedIn) {
System.out.print("Enter the user's email: ");
email = READER.readLine();
System.out.print("Enter the password: ");
password = READER.readLine();
if (verify()) {
System.out.println("Data verification has been successful.");
} else {
System.out.println("Wrong email or password!");
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
private boolean verify() {
setSignedIn(email.equals(DATA_BASE.get(password)));
return signedIn;
}
/**
* Save customer data for future shopping attempts.
*/
@Override
public boolean pay(int paymentAmount) {
if (signedIn) {
System.out.println("Paying " + paymentAmount + " using PayPal.");
return true;
} else {
return false;
}
}
private void setSignedIn(boolean signedIn) {
this.signedIn = signedIn;
}
}
strategies/PayByCreditCard.java: 크레딧 카드를 통한 결제
package refactoring_guru.strategy.example.strategies;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* Concrete strategy. Implements credit card payment method.
*/
public class PayByCreditCard implements PayStrategy {
private final BufferedReader READER = new BufferedReader(new InputStreamReader(System.in));
private CreditCard card;
/**
* Collect credit card data.
*/
@Override
public void collectPaymentDetails() {
try {
System.out.print("Enter the card number: ");
String number = READER.readLine();
System.out.print("Enter the card expiration date 'mm/yy': ");
String date = READER.readLine();
System.out.print("Enter the CVV code: ");
String cvv = READER.readLine();
card = new CreditCard(number, date, cvv);
// Validate credit card number...
} catch (IOException ex) {
ex.printStackTrace();
}
}
/**
* After card validation we can charge customer's credit card.
*/
@Override
public boolean pay(int paymentAmount) {
if (cardIsPresent()) {
System.out.println("Paying " + paymentAmount + " using Credit Card.");
card.setAmount(card.getAmount() - paymentAmount);
return true;
} else {
return false;
}
}
private boolean cardIsPresent() {
return card != null;
}
}
strategies/CreditCard.java: 크레딧 카드 클래스
package refactoring_guru.strategy.example.strategies;
/**
* Dummy credit card class.
*/
public class CreditCard {
private int amount;
private String number;
private String date;
private String cvv;
CreditCard(String number, String date, String cvv) {
this.amount = 100_000;
this.number = number;
this.date = date;
this.cvv = cvv;
}
public void setAmount(int amount) {
this.amount = amount;
}
public int getAmount() {
return amount;
}
}
order
order/Order.java: 주문 클래스
package refactoring_guru.strategy.example.order;
import refactoring_guru.strategy.example.strategies.PayStrategy;
/**
* Order class. Doesn't know the concrete payment method (strategy) user has
* picked. It uses common strategy interface to delegate collecting payment data
* to strategy object. It can be used to save order to database.
*/
public class Order {
private int totalCost = 0;
private boolean isClosed = false;
public void processOrder(PayStrategy strategy) {
strategy.collectPaymentDetails();
// Here we could collect and store payment data from the strategy.
}
public void setTotalCost(int cost) {
this.totalCost += cost;
}
public int getTotalCost() {
return totalCost;
}
public boolean isClosed() {
return isClosed;
}
public void setClosed() {
isClosed = true;
}
}
Demo.java: 클라이언트 코드
package refactoring_guru.strategy.example;
import refactoring_guru.strategy.example.order.Order;
import refactoring_guru.strategy.example.strategies.PayByCreditCard;
import refactoring_guru.strategy.example.strategies.PayByPayPal;
import refactoring_guru.strategy.example.strategies.PayStrategy;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
/**
* World first console e-commerce application.
*/
public class Demo {
private static Map<Integer, Integer> priceOnProducts = new HashMap<>();
private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
private static Order order = new Order();
private static PayStrategy strategy;
static {
priceOnProducts.put(1, 2200);
priceOnProducts.put(2, 1850);
priceOnProducts.put(3, 1100);
priceOnProducts.put(4, 890);
}
public static void main(String[] args) throws IOException {
while (!order.isClosed()) {
int cost;
String continueChoice;
do {
System.out.print("Please, select a product:" + "\n" +
"1 - Mother board" + "\n" +
"2 - CPU" + "\n" +
"3 - HDD" + "\n" +
"4 - Memory" + "\n");
int choice = Integer.parseInt(reader.readLine());
cost = priceOnProducts.get(choice);
System.out.print("Count: ");
int count = Integer.parseInt(reader.readLine());
order.setTotalCost(cost * count);
System.out.print("Do you wish to continue selecting products? Y/N: ");
continueChoice = reader.readLine();
} while (continueChoice.equalsIgnoreCase("Y"));
if (strategy == null) {
System.out.println("Please, select a payment method:" + "\n" +
"1 - PalPay" + "\n" +
"2 - Credit Card");
String paymentMethod = reader.readLine();
// Client creates different strategies based on input from user,
// application configuration, etc.
if (paymentMethod.equals("1")) {
strategy = new PayByPayPal();
} else {
strategy = new PayByCreditCard();
}
}
// Order object delegates gathering payment data to strategy object,
// since only strategies know what data they need to process a
// payment.
order.processOrder(strategy);
System.out.print("Pay " + order.getTotalCost() + " units or Continue shopping? P/C: ");
String proceed = reader.readLine();
if (proceed.equalsIgnoreCase("P")) {
// Finally, strategy handles the payment.
if (strategy.pay(order.getTotalCost())) {
System.out.println("Payment has been successful.");
} else {
System.out.println("FAIL! Please, check your data.");
}
order.setClosed();
}
}
}
}
OutputDemo.txt: 실행 결과
Please, select a product:
1 - Mother board
2 - CPU
3 - HDD
4 - Memory
1
Count: 2
Do you wish to continue selecting products? Y/N: y
Please, select a product:
1 - Mother board
2 - CPU
3 - HDD
4 - Memory
2
Count: 1
Do you wish to continue selecting products? Y/N: n
Please, select a payment method:
1 - PalPay
2 - Credit Card
1
Enter the user's email: user@example.com
Enter the password: qwerty
Wrong email or password!
Enter user email: amanda@ya.com
Enter password: amanda1985
Data verification has been successful.
Pay 6250 units or Continue shopping? P/C: p
Paying 6250 using PayPal.
Payment has been successful.
출처
https://refactoring.guru/ko/design-patterns/strategy
전략 패턴
/ 디자인 패턴들 / 행동 패턴 전략 패턴 다음 이름으로도 불립니다: Strategy 의도 전략 패턴은 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환
refactoring.guru
https://refactoring.guru/ko/design-patterns/strategy/java/example
자바로 작성된 전략 / 디자인 패턴들
/ 디자인 패턴들 / 전략 / 자바 자바로 작성된 전략 전략은 행동들의 객체들을 객체들로 변환하며 이들이 원래 콘텍스트 객체 내에서 상호 교환이 가능하게 만드는 행동 디자인 패턴입니다. 원래
refactoring.guru
https://actruce.com/copy-object-oriented-programming-2/
객체지향 프로그래밍(OOP) – 2 : Composition and Inheritance – actruce's Blog
흔하디 흔한 제목이고 많은 소개 자료가 있어 굳이 블로그에 포스트할 필요가 있나 싶었지만, 최근 보게 된 책에서 내용을 잘 설명해 주어서 책의 흐름을 따라가면서 소개해 보고자 한다. 내가
actruce.com
'STUDY > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] MVC와 MVP 패턴 비교 (0) | 2023.06.09 |
---|---|
[디자인 패턴] MVC 패턴 (0) | 2023.06.09 |
[디자인 패턴] 생성 패턴 - 팩토리 메소드 패턴 (0) | 2023.06.05 |
[디자인 패턴] 생성 패턴 - 싱글톤 패턴 (2) | 2023.06.05 |
[디자인패턴] 디자인 패턴이란? (0) | 2023.06.05 |