일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- resultMap
- 필드 주입
- 스프링 빈
- sqld
- 생성자 주입
- 스프링 부트 입문
- DI
- JPA
- mybatis
- assertThat
- 스프링부트
- 스프링 부트 기본
- Javascript
- db
- kafka
- DIP
- SQL
- java
- 싱글톤
- 스프링 프레임워크
- Effective Java
- @Configuration
- springboot
- assertThrows
- 스프링 컨테이너
- jdbc
- 스프링
- 스프링 부트
- spring
- thymeleaf
- Today
- Total
선 조치 후 분석
[Design Pattern] 행동패턴 - Strategy 본문
전략 (Strategy) 패턴
- 여러 알고리즘을 캡슐화하고 상호 교환 가능하게 만드는 패턴
- 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 패턴
- '전략'이란 일종의 알고리즘이나 기능, 동작이 될 수도 있는 특정판 목표를 수행하기 위한 행동 계획이다.
- 어떤 일을 수행하는 알고리즘이 여러 가지 일 때, 동작들을 미리 전략으로 정의함으로써 손쉽게 전략을 교체할 수 있는 알고리즘 변형이 빈번하게 필요한 경우 적합한 패턴
구조
- 전략 알고리즘 객체들 (Concreate Strategy) : 알고리즘, 행위, 동작을 객체로 정의한 구현체
- 전략 인터페이스 : 모든 전략 구현체에 대한 공용 인터페이스
- 컨텍스트 (Context) : 알고리즘을 실행해야 할 때마다 해당 알고리즘과 연결된 전략 객체의 메소드를 호출
- 클라이언트 : 특정 전략 객체를 컨텍스트에 전달함으로써 전략을 등록하거나 변경하여 전략 알고리즘을 실행한 결과를 누린다.

전략 (Strategy) 패턴 사용 시기
- 전략 알고리즘의 여러 버전 또는 변형이 필요할 때 클래스화를 통해 관리
- 알고리즘 코드가 노출되어서는 안 되는 데이터에 접근하거나 데이터를 활용할 때 (캡슐화)
- 알고리즘의 동작이 런타임에 실시간으로 교체되어야 할 때
전략 (Strategy) 패턴 적용 전
- RPG 게임에서의 무기 전략
- state 매개변수의 값에 따라서 간접적으로 attack() 동작을 제어하도록 되어있다.
- 상수를 메소드에 넘겨 조건문으로 일일이 필터링 처리
public class TakeWeapon {
public static final int SWORD = 0;
public static final int SHIELD = 1;
public static final int CROSSBOW = 2;
private int state;
void setWeapon(int state) {
this.state = state;
}
void attack() {
if(state == SWORD) {
System.out.println("칼을 휘두르다.");
}else if(state == SHIELD) {
System.out.println("방패로 밀친다.");
}else if(state == CROSSBOW) {
System.out.println("활을 쏜다.");
}
}
}
@Test
void test1() {
//무기 착용 전략 설정
TakeWeapon hand = new TakeWeapon();
// 검을 들도록 전략 설정
hand.setWeapon(TakeWeapon.SWORD);
hand.attack();
// 방패를 들도록 전략 설정
hand.setWeapon(TakeWeapon.SHIELD);
hand.attack();
}
상태 변수를 통해 행위를 분기문으로 나누는 행위는 좋지 않은 코드이다. 잘못하면 if - else 지옥에 빠질 수 있기 때문이다.
전략 (Strategy) 패턴 적용 후
- 위의 클린 하지 않은 코드를 해결하는 가장 좋은 방법은 변경시키고자 하는 행위(전략)를 직접 넘겨주는 것
- 여러 무기들을 객체 구현체로 정의하고 이들을 Weapon이라는 인터페이스로 묶어 준다.
- 그리고 인터페이스를 컨텍스트 클래스에 합성(Composition) 시키고, setWeapon() 메소드를 통해 전략 인터페이스
객체의 상태를 바로바로 변경할 수 있도록 구성
public interface Weapon {
void offensive();
}
public class Sword implements Weapon {
@Override
public void offensive() {
System.out.println("칼을 휘두르다");
}
}
public class Shield implements Weapon {
@Override
public void offensive() {
System.out.println("방패로 밀친다");
}
}
public class CrossBow implements Weapon {
@Override
public void offensive() {
System.out.println("화살을 발사하다.");
}
}
- Context - 전략을 등록하고 실행
public class TakeWeaponStrategy {
Weapon weapon;
void setWeapon(Weapon weapon) {
this.weapon = weapon;
}
void attack() {
weapon.offensive();
}
}
- Client - 전략 제공/설정
@Test
void test2() {
// 무기착용 전략
TakeWeaponStrategy hand = new TakeWeaponStrategy();
// 검을 들도록 전략 설정
hand.setWeapon(new Sword());
hand.attack();
// 방패를 들도록 전략 설정
hand.setWeapon(new Shield());
hand.attack();
// 화살을 들도록 전략 설정
hand.setWeapon(new CrossBow());
hand.attack();
}
전략 패턴 적용 전에는 메소드에 상수값을 넘겨주었지만,
전략 패턴에서는 인스턴스를 넣어 알고리즘을 수행하도록 한 것이다.
이런 식으로 구성하면 새로운 무기를 추가한다고 했을 때, 코드의 수정 없이 기능을 확장할 수 있다.
인터페이스를 구현하는 클래스를 추가만 해주면 된다.
결과적으로 OOP 핵심인 유지보수를 용이하기 위해, 약간 복잡하더라도 이러한 패턴을 적용하여 프로그램을 구성해 나가는 게 효율적이다.
전략 (Strategy) 패턴 적용 예시 1 - 여러 기능 전략을 가진 로봇
- Robot이라는 추상클래스가 존재
- 걷는 로봇과 달리는 로봇으로 구성된 객체가 존재
public abstract class Robot {
public abstract void display();
public abstract void move();
}
public class WalkingRobot extends Robot {
@Override
public void display() {
System.out.println("걷는 로봇");
}
@Override
public void move() {
System.out.println("걸어서 배달합니다.");
}
}
public class RunningRobot extends Robot {
@Override
public void display() {
System.out.println("달리는 로봇");
}
@Override
public void move() {
System.out.println("달려서 배달합니다.");
}
}
@Test
void test3() {
Robot robot1 = new WalkingRobot();
robot1.display();
robot1.move();
Robot robot2 = new RunningRobot();
robot2.display();
robot2.move();
}
보기에는 객체지향적으로 문제없는 코드이지만, 만약 로봇의 기능 추가가 된다면 코드의 유지보수면에서 문제가 발생한다. 만약 번역 기능도 추가한다고 가정하면, 새로운 메소드를 각 걷는 로봇에 추가하여야 한다.
- 클래스가 4개로 늘어났다.
public class WalkingRobotKr extends Robot {
@Override
public void display() {
System.out.println("걷는 로봇");
}
@Override
public void move() {
System.out.println("걸어서 배달합니다.");
}
@Override
public void translate() {
System.out.println("한국어로 번역합니다.");
}
}
public class WalkingRobotJp extends Robot {
@Override
public void display() {
System.out.println("걷는 로봇");
}
@Override
public void move() {
System.out.println("걸어서 배달합니다.");
}
@Override
public void translate() {
System.out.println("일본어로 번역합니다.");
}
}
public class RunningRobotKr extends Robot {
@Override
public void display() {
System.out.println("달리는 로봇");
}
@Override
public void move() {
System.out.println("달려서 배달합니다.");
}
@Override
public void translate() {
System.out.println("한국어로 번역합니다.");
}
}
public class RunningRobotJp extends Robot {
@Override
public void display() {
System.out.println("달리는 로봇");
}
@Override
public void move() {
System.out.println("달려서 배달합니다.");
}
@Override
public void translate() {
System.out.println("일본어로 번역합니다.");
}
}
위와 같은 '클래스 폭발' 문제가 일어난 이유는 객체를 사물 / 생물 정도로 밖에 인식하지 못해서이다.
객체는 하나의 기능이나 행위, 동작으로도 표현할 수 있다.
전략 패턴은 이러한 접근으로 복잡한 문제를 해결해 나가는 방식이다.
행위를 구현체로 빼서 정의하고 관리해야 한다. 그리고 이 행위 객체들을 모아 인터페이스로 묶어 하나의 전략 묶음을
구성하고, 이것을 컨텍스트에서 합성시켜 다형성을 통해 유기적으로 전략 행위들을 사용할 수 있도록 하는 것이다.
public interface MoveStrategy {
void move();
}
public class Walk implements MoveStrategy {
@Override
public void move() {
System.out.println("걸어서 배달합니다.");
}
}
public class Run implements MoveStrategy {
@Override
public void move() {
System.out.println("달려서 배달합니다.");
}
}
public interface TranslateStrategy {
void translate();
}
public class Korean implements TranslateStrategy {
@Override
public void translate() {
System.out.println("한국어로 번역합니다.");
}
}
public class Japanese implements TranslateStrategy {
@Override
public void translate() {
System.out.println("일본어로 번역합니다.");
}
}
- Context - 전략 등록/실행
public class RobotContext {
MoveStrategy moveStrategy;
TranslateStrategy translateStrategy;
RobotContext (MoveStrategy moveStrategy, TranslateStrategy translateStrategy) {
this.moveStrategy = moveStrategy;
this.translateStrategy = translateStrategy;
}
void move() {
moveStrategy.move();
}
void translate() {
translateStrategy.translate();
}
void setMoveStrategy(MoveStrategy moveStrategy) {
this.moveStrategy = moveStrategy;
}
void setTranslateStrategy(TranslateStrategy translateStrategy) {
this.translateStrategy = translateStrategy;
}
}
- Client - 전략 교체/설정, 실행한 결과를 얻음
@Test
void test4() {
RobotStrategy robot = new RobotStrategy(new Walk(), new Korean());
robot.move();
robot.translate();
robot.setMoveStrategy(new Run());
robot.setTranslateStrategy(new Japanese());
robot.move();
robot.translate();
}
전략 (Strategy) 패턴 적용 예시 2 - 카드 결제 전략 시스템
- 1번과의 차이점은 전략 인터페이스를 클래스 필드로 합성하지 않고 Context의 메소드의 매개변수로 합성(Composition)한다.
public interface PaymentStrategy {
void pay(int amount);
}
public class KAKAOCardStrategy implements PaymentStrategy {
private String name;
private String cardNumber;
private String cvv;
private String dateOfExpiry;
public KAKAOCardStrategy(String name, String cardNumber, String cvv, String dateOfExpiry) {
this.name = name;
this.cardNumber = cardNumber;
this.cvv = cvv;
this.dateOfExpiry = dateOfExpiry;
}
@Override
public void pay(int amount) {
System.out.println(amount + "원을 카카오카드로 결제하였습니다.");
}
}
public class LUNACardStrategy implements PaymentStrategy {
private String emailId;
private String password;
public LUNACardStrategy(String emailId, String password) {
this.emailId = emailId;
this.password = password;
}
@Override
public void pay(int amount) {
System.out.println(amount + "원을 루나카드로 결제하였습니다.");
}
}
public class Item {
public String name;
public int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
}
public class ShoppingCart {
List<Item> items;
public ShoppingCart() {
this.items = new ArrayList<>();
}
public void addItem(Item item) {
this.items.add(item);
}
// 전략을 매개변수로 받는다
public void pay(PaymentStrategy paymentStrategy) {
int amount = 0;
for(Item item : items) {
amount += item.price;
}
paymentStrategy.pay(amount);
}
}
@Test
void test5() {
// 쇼핑카트 전략 Context
ShoppingCart cart = new ShoppingCart();
// 쇼핑 물품
Item a = new Item("맥북", 100000);
Item b = new Item("핸드폰", 300000);
cart.addItem(a);
cart.addItem(b);
// Luna 카드 결제 전략
cart.pay(new LUNACardStrategy("jb@abc.com", "1234"));
// Kakao 카드 결제 전략
cart.pay(new KAKAOCardStrategy("jb", "123456", "123","01/12"));
}
메소드의 입력값으로 객체를 할당하는 방식이 좋은 점은,
각 전략에 따라 초기화하는 생성자 매개변수 개수가 다를 수 있기 때문이다.
코드에서 보다시피, Luna와 Kakao에서 필요로 하는 정보가 다르기 때문이다.
전략 (Strategy) 패턴
장점
- 새로운 전략을 추가하더라도 기존 코드를 변경하지 않는다. (OCP)
- 상속대신 위임을 사용할 수 있다.
- 런타임에 전략을 변경할 수 있다.
- 기능을 호출하는 클라이언트는 내부 로직을 알 필요가 없다.
(캡슐화 - 의존성 분리 -> 테스트를 작성하기 편한 구조)
단점
- 알고리즘이 많아질수록 관리해야 할 객체의 수가 늘어난다.
- 애플리케이션 특성이 알고리즘이 많지 않고 자주 변경되지 않는다면, 새로운 클래스와 인터페이스를
만들어 프로그램을 복잡하게 만들 이유가 없다. - 개발자는 적절한 전략을 선택하기 위해 전략 간의 차이점을 파악하고 있어야 한다. (복잡도 증가)
'Language > Design Pattern' 카테고리의 다른 글
[Design Pattern] 행동패턴 - Visitor (1) | 2023.11.17 |
---|---|
[Design Pattern] 행동패턴 - Observer (1) | 2023.11.13 |
[Design Pattern] 행동패턴 - Memento (1) | 2023.11.08 |
[Design Pattern] 행동패턴 - Mediator (0) | 2023.11.07 |
[Design Pattern] 행동패턴 - Iterator (0) | 2023.11.06 |