Java/Design Pattern

[디자인 패턴] 행동 패턴 - 전략 패턴 (Strategy Pattern)

민돌v 2023. 4. 21. 09:07
728x90
(인프런) 코딩으로 학습하는 GoF의 디자인 패턴 - 백기선, 강의를 보고 정리한 글입니다.
코드는 GitHub 에 있습니다

 

#1. 객체 생성 관련 패턴

#2. 구조 관련 패턴

 

#3. 행동 관련 패턴

 

 


✔️ 전략 패턴 이란 (Strategy Patterns)

여러 알고리듬을 캡슐화하고 상호 교환 가능하게 만드는 패턴.
  • 전략 패턴이란, 어떤 업무를 수행하는 방법이 여러가지일경우, 이 여러 방법 즉 알고리즘(기능)을 캡슐화하여 하나의 인터페이스로 추상화해, 코드를 변경하지 않고 여러기능을 수해하는 패턴입니다.
  • 간단하게 정리하자면, 객체가 할 수 있는 행위들을 각각의 전략(추상의 구현) 으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략의 변경으로 행위의 수정이 가능하도록 만든 패턴입니다.

 

전략 패턴의 구조

 

→ 컨텍스트에서 사용할 알고리즘을 클라이언트가 선택 (전략을 선택)

 


✔️ 전략 패턴 예시

  • 전략패턴은, 먼저 특정 기능을 수행하는 여러 방법을 하나의 인스턴스로 추상화 하여 공통적으로 사용할 수 있도록 만듭니다.
  • 그 후, 여러 방법을 전략적으로 수행할 수 있도록 각각을 캡슐화해 기능을 구현합니다.
  • 이후 클라이언트에서 전략을 동적으로 수행할 수 있도록 주입하는 구조 방식을 가집니다.

 

전략 패턴 적용 전

조건에 따라 같은기능을 여러가지 방법으로 구현한 코드 입니다.

 예시는 단순하게 speed 속도(조건)에 따라 다른 기능 구현(전략) 방식을 가집니다.

//클라이언트 코드
public class Client {

    public static void main(String[] args) {
        BlueLightRedLight blueLightRedLight = new BlueLightRedLight(1);
        blueLightRedLight.blueRight();
        blueLightRedLight.redRight();

    }
}

//비지니스 로직 코드
public class BlueLightRedLight {

    private int speed;

    public BlueLightRedLight(int speed) {
        this.speed = speed;
    }

    //조건 (speed)에 따라 상황이 달라지는(기능 - 알고리즘) 코드
    public void blueRight() {
        if (speed == 1) {
            System.out.println("무 궁 화    꽃    이");
        }
        //조금 더 빠르게
        if (speed == 2) {
            System.out.println("무궁화  꽃  이");
        }
        //더 빠르게
        if (speed == 3) {
            System.out.println("무궁화꽃이");
        }
    }

    public void redRight() {
        if (speed == 1) {
            System.out.println("피 었 습 니 다.");
        }
        //조금 더 빠르게
        if (speed == 2) {
            System.out.println("피었습니다.");
        }
        //더 빠르게
        if (speed == 3) {
            System.out.println("피어씀다.");
        }
    }
}

 

전략패턴 적용

여기에 전략 패턴을 적용하여 조건에 따른 공통된 기능을 추상화하고 구현을 캡슐화하여 전략으로 취해보겠습니다.
public class Client {

    public static void main(String[] args) {
        //전략에 따라 어떤 기능을 행할건지 선택하면 됨
        BlueLightRedLight blueLightRedLight = new BlueLightRedLight(new Normal());
        blueLightRedLight.blueRight();
        blueLightRedLight.redRight();
    }
}

//Context Class
public class BlueLightRedLight {

    private SpeedStrategy speed;

    public BlueLightRedLight(SpeedStrategy speed) {
        this.speed = speed;
    }

    //조건 (speed)에 따라 상황이 달라지는(기능 - 알고리즘) 코드
    public void blueRight() {
        speed.blueRight();
    }

    public void redRight() {
        speed.redRight();
    }
}
  • 이렇게 기능(알고리즘) 을 캡슐화하고 추상화함으로써, 클라이언트에서 기능을 전략적으로 선택할 수 있는 구조가 되었습니다.
public interface SpeedStrategy {

    void blueRight();

    void redRight();
}

//speed 가 1일때
public class Normal implements SpeedStrategy{

    @Override
    public void blueRight() {
        System.out.println("무 궁 화    꽃    이");
    }

    @Override
    public void redRight() {
        System.out.println("피 었 습 니 다.");
    }
}

//speed 가 2일때
public class Faster  implements SpeedStrategy{

    @Override
    public void blueRight() {
        System.out.println("무궁화  꽃  이");
    }

    @Override
    public void redRight() {
        System.out.println("피었습니다.");
    }
}

//speed가 3일 때
public class Fastest implements SpeedStrategy{

    @Override
    public void blueRight() {
        System.out.println("무궁화꽃이");
    }

    @Override
    public void redRight() {
        System.out.println("피어씀다.");
    }
}

 

  • 클라이언트는 여러 구조로 전략패턴을 적용하여 구조를 설계할 수 있습니다.
  • 이는 즉, 클라이언트는 유동적이고 전략적이게 기능을 선택할 수 있는 구조가 되었음을 말합니다.
public class Client {

    public static void main(String[] args) {
        //전략에 따라 어떤 기능을 행할건지 선택하면 됨
        BlueLightRedLight blueLightRedLight = new BlueLightRedLight(new Normal());
        blueLightRedLight.blueRight();
        blueLightRedLight.redRight();

        //매소드 파라미터에서 전략을 매개변수로 받아서 실행해도 됨 → 중요한건 전략을 유연하게 선택한다는 것
        BlueLightRedLight_2 context = new BlueLightRedLight_2();
        context.blueRight(new Normal());
        context.redRight(new Fastest());

        //기능을 추상화했기 때문에 익명함수로 그때의 전략을 구현해서 주입해도 됨 (마치 JAVA Comparator)
        BlueLightRedLight anonymityStrategy = new BlueLightRedLight(new SpeedStrategy() {
            @Override
            public void blueRight() {

            }

            @Override
            public void redRight() {

            }
        });
    }
}

 


✔️ 전략 패턴 장점, 단점

• 장점

  1. 새로운 전략을 추가하더라도 기존 코드를 변경하지 않는다. (OCP)
  2. 상속 대신 위임을 사용할 수 있다.
  3. 런타임에 전략을 변경할 수 있다.
  4. 기능을 호출하는 클라이언트는 내부 로직을 알 필요가 없다 (캡슐화 - 의존성 분리 → 테스트를 작성하기 편한 구조)

 

• 단점

  1. 레이어 구조 복잡도가 증가한다. (파일이 많아져서)
  2. 클라이언트 코드가 어떤 전략이 있는지 알아야 한다.

 


✔️ 전략 패턴과 테스트코드

사소한 부분이지만, next-step 과제에서 테스트코드 작성에 대해 전략패턴을 언급한적이 있음 기록으로 남겨보고자 합니다.

전략패턴을 사용하면 기능을 캡슐화하고 → 더 작게 기능을 분리하기 때문에 테스트 코드를 작성하기 더 쉬운 구조가 됩니다.

 

1. 전략패턴 적용전 

  • 예시는 "0~10 까지 랜덤한 값이 4이상의 값이 나올때 Car 객체는 1의 거리만큼 이동한다." 라는 기능을 수행합니다. 
  • 단순하게 랜덤함수를 이용해서 Car 객체내부에서 이동 가능 여부를 판단하는 코드를 먼저 작성해 보았습니다.
  • 이것도 저는,, 나쁘지 않은 코드라고 생각하긴 합니다 - 결국 Car 객체에서 이동의 책임을 가져가게되니까
public class Car {

    private int movementDistance;

    public void go() {
        if (RandomUtil.getNumber() > 4) {
            movementDistance += 1;
        }
    }

    public int getNowMovementResult() {
        return this.movementDistance;
    }
}

 

  • 랜덤한 값을 내려주는 RandomUtil 함수도 만들어 주었습니다.
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RandomUtil {

    private static final Random random = new Random();

    public static int getNumber() {
        return random.nextInt(10);
    }
}

 

Test 코드 작성

  • 이제 간단하게 테스트해보고자 하지만 아래와 같은 에러가 발생합니다.
  • 결론은, Random함수는 static 함수이기 때문에 Mockito 만으로는 mocking 할 수 없다입니다.
  • 즉, 결과로만 보았을 때는 Car 객체의 go() 기능이 RandomUtil 클래스에 강한 종속성을 가지고 있다고 볼 수 있습니다. 
@Test
@DisplayName("랜덤한 숫자로 나오는 값이 4이상일 경우 차는 이동한다.")
void test() {
    Car car = new Car();

    when(RandomUtil.getNumber()).thenReturn(5);
    car.go();

    assertThat(car.getNowMovementResult()).isEqualTo(1);
}

 

2. 전략패턴 적용 후 

  • 그럼 이제, 이 문제를 해결해봅시다.
  • 문제는 Car 의 go() 기능과 RandomUtil 의 결합도 분리입니다.
  • 지금은 단순하지만, go() 메소드도 어떻게 보면 주요한 비지니스 로직이고 현업에서는 더 중요한 비지니스로직도 이렇게 알게모르게 통제할 수 없는 무엇인가에 강한 종속성을 가질 수도 있겠죠
  • 이를 전략패턴을 사용해서 행위를 추상화하고 기능을 캡슐화하여 결합도를 낮추겠습니다.

 

1. 먼저 move, 즉 이동한다 라는 공통된 기능을 추상화하였습니다.

2. 그리고 Car 의 이동에 대한 기능을 구현하였습니다. → move 에 대한 캡슐화

public interface MoveStrategy {

    //얼마나 이동할지
    int move();
}

public class CarMove implements MoveStrategy{

    @Override
    public int move() {
        if (RandomUtil.getNumber() > 4) {
            return 1;
        }
        return 0;
    }
}

 

3. 이렇게 되면, Car는 move 에대한 전략을 외부에서 주입받아 "주입된 거리만큼 - 이동한다" 를 수행합니다.

public class Car {

    private int movementDistance;

    public void go(MoveStrategy strategy) {
        movementDistance += strategy.move();
    }
    
    public int getNowMovementResult() {
        return this.movementDistance;
    }
}

 

👏🏻 이렇게 함으로써, "랜덤한 숫자 4 이상이 나왔을 때 1만큼 이동한다" 가 아래처럼 책임이 분리가 되어집니다.

  • Car - 이동한다.
  • MoveStrategy - 얼마만큼
  • CarMoveStrategy - 랜덤한 숫자 4이상이 나왔을 때 

이로인해 Car 도메인의 비지니스 로직은 Random 함수에 대한 종속성을 가지고 가지 않아 테스트에 유려한 코드가 됩니다.

결국 RandomUtil 에대한 종속성은 전략패턴의 한가지 전략이 가져가긴하지만, 중요한 도메인의 비지니스 로직 자체에 종속되는 것 보다는 났다고 생각합니다.

또한, Car 객체만 보았을 때 객체 자체의 확장성도 전략패턴을 적용함으로써 더 좋아졌다고 생각됩니다.


 

✔️ 자바와 스프링에서 전략 패턴 예시

• 자바

  1. Comparator

 

•  스프링

  1. ApplicationContext
  2. PlatformTransactionManager

 

 

 

 

그럼 전략패턴도 끝!

 

 

 

 

반응형