전략 패턴

바그다드·2023년 8월 10일
0

디자인 패턴

목록 보기
2/3

지난 포스팅에서 템플릿 메서드 패턴을 활용해 SRP를 준수함으로 코드의 변경에 유연한 로직을 작성해보았다.
하지만 템플릿 메서드 패턴은 단점이 있다.
상속을 이용하여 구현하기 때문에 컴파일 시점에 자식 클래스가 부모 클래스를 강하게 의존한다는 것이다.
자식 클래스에서는 부모 클래스의 어떠한 기능도 사용하지 않음에도 불구하고, 명확하게 부모 클래스의 코드가 명시되어 있다. 따라서 부모 클래스의 변경에 민감하다는 단점이 있다.

이러한 문제를 해결하기 위해 사용되는 것이 전략 패턴이다. 이번 포스팅에서는 전략 패턴에 대해 알아보도록 하자.

전략 패턴

변하지 않은 부분을 Context라는 곳에 두고, 변하는 부분을 Strategy라는 인터페이스를 만들어 해당 인터페이스를 구현하도록 해서 문제를 해결한다. 즉, 상속이 아닌 위임으로 문제를 해결한다.

GOF 디자인 패턴에서 정의한 전략 패턴의 의도

알고리즘 제품군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만들자. 전략을 사용하면 알고리즘을
사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.

전략 패턴을 구현하는 방법은 2가지가 있다.

  1. 생성 시점에 Strategy를 결정
    Context의 필드에 Strategy를 두고 사용하는 방법
  2. 실행 시점에 Strategy를 결정
    메서드에 Strategy를 파라미터로 받아 사용하는 방법

코드로 확인해보자

1. 생성 시점에 Strategy를 결정

1. Context 생성

@Slf4j
public class ContextV1 {

	// 인터페이스 Strategy를 의존하고 있음
    private Strategy strategy;

    public ContextV1(Strategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        long startTime = System.currentTimeMillis();
        // 비즈니스 로직 실행
        // strategy의 로직을 수행함
        strategy.call(); // 위임
        // 비즈니스 로직 종료
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("result={}",resultTime);
    }
}
  • Context가 필드로 Strategy를 가지고 있는 것을 확인할 수 있다.
    • 이때 Strategy는 인터페이스로 상황에 따라 원하는 Strategy의 구현체를 주입받아 사용할 수 있다.

2. Strategy 인터페이스 생성

public interface Strategy {
    void call();
}

3. Strategy 구현체 생성

@Slf4j
public class StrategyLogic1 implements Strategy{
    @Override
    public void call() {
      log.info("비즈니스 로직1 실행");
    }
}
  • 숫자만 1에서 2로 바꿔 구현체를 하나 더 생성해주자.(여기서는 생략했다)

4. test코드 작성

  • 그럼 테스트 코드를 이용해 직접 확인해보자.
@Slf4j
public class ContextV1Test {

    @Test
    void strategyV1() {
        StrategyLogic1 strategyLogic1 = new StrategyLogic1();
        ContextV1 context1 = new ContextV1(strategyLogic1);
        context1.execute();


        StrategyLogic2 strategyLogic2 = new StrategyLogic2();
        ContextV1 context2 = new ContextV1(strategyLogic2);
        context2.execute();
    }
	
    // 익명 내부 클래스
    // 객체를 변수로 생성후 파라미터로 넘김
    @Test
    void strategyV2() {
        Strategy strategyLogic1 = new Strategy() {
            @Override
            public void call() {
                log.info("비즈니스 로직1 실행");
            }
        };
        ContextV1 contextV1 = new ContextV1(strategyLogic1);
        log.info("strategyLogic1={}",strategyLogic1.getClass());
        contextV1.execute();

        // strategyLogic2 생략
    }

    // 익명 내부 클래스 2
    // 객체를 생성하며 파라미터로 바로 넘김
    @Test
    void strategyV3() {
        ContextV1 contextV1 = new ContextV1(new Strategy() {
            @Override
            public void call() {
                log.info("비즈니스 로직1 실행");
            }
        });
        contextV1.execute();

        // contextV2 생략
    }

    // 람다 사용
    // 인터페이스에 메서드가 1개만 있을 경우 람다로 구현 가능
    @Test
    void strategyV4() {
        ContextV1 contextV1 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
        contextV1.execute();

        // contextV2 생략
    }
}
  • 템플릿 메서드 패턴과 마찬가지로 다양한 주입 방법을 사용할 수 있다.
  1. 구현 클래스를 정의하고, 구현 클래스의 객체를 생성하여 주입하는 방법
  2. 익명 클래스를 변수에 담아 주입하는 방법
  3. 익명 클래스를 파라미터로 바로 주입하는 방법
  4. 람다로 주입하는 방법
    • 자바 8부터 인터페이스에 메서드가 1개일 경우 람다로 구현할 수 있다.

흐름은 다음과 같다.

흐름

  1. Context를 생성하는 시점에 원하는 Strategy 구현체를 주입한다.
  2. 클라이언트는 context를 실행한다.
  3. context는 로직을 시작한다.
  4. 로직 중간에 strategy.call()을 호출해서 주입받은 strategy의 로직을 실행한다.
  5. context의 나머지 로직을 수행한다.

이 방식은 Context생성 시점에 strategy를 주입받아 조립을 완료하고,
그 후에 context.execute()를 호출해 context를 실행하는
선조립 후사용 방식이다.
따라서 Context를 생성한 후에는 전략을 신경쓰지 않고 실행만 하면 된다.

마치 Spring을 사용할 때처럼 빈에 미리 원하는 구현체를 주입 받은 후, 주입 받은 구현체의 로직을 실행하는 방법과 비슷하다.

2. 실행 시점에 Strategy를 결정

이번에는 실행 시점에 Strategy를 결정하도록 구현해보자.
다른 코드는 동일하며 Context만 아래와 같이 수정해주자.

Context 생성

@Slf4j
public class ContextV2 {

	// 메서드의 파라미터로 Strategy를 주입
    public void execute(Strategy strategy) {
        long startTime = System.currentTimeMillis();
        // 비즈니스 로직 실행
        strategy.call(); // 위임
        // 비즈니스 로직 종료
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("result={}",resultTime);
    }
}
  • 메서드의 파라미터로 Strategy를 주입받아 사용하고 있다.
    테스트 코드를 작성해보자.

test코드 작성

    @Test
    void strategyV3(){
        ContextV2 contextV2 = new ContextV2();
        contextV2.execute(() -> log.info("비즈니스 로직1"));
        contextV2.execute(() -> log.info("비즈니스 로직2"));
    }
  • Context의 생성 시점에 Strategy를 결정하는 것이 아니라,
    실행 시점에 Strategy를 결정하는 방식이다.

흐름은 다음과 같다.

흐름


1. Context를 생성한다.
2. 클라이언트는 Context실행 시점에 Strategy를 전달한다.
3. Context는 extecute() 로직을 실행한다.
4. 로직 수행 도중 strategy.call()을 수행한다.
5. 나머지 execute()로직을 수행한다.

context를 실행할 때 Strategy를 주입받기 때문에 실행할 때마다 Strategy를 유연하게 변경할 수 있다.
하지만 반대로 실행할 때마다 Strategy를 지정해줘야 한다는 단점이 있다.

그래서 어떤 방법을 사용할까?

위에서 알아본 두 가지 방법 모두 문제를 해결할 수 있다.
그런데 생각해보면 우리는 변하지 않는 템플릿이 있고, 템플릿 안에서 원하는 부분만 다른 코드를 실행하고 싶을 뿐이다.
따라서 우리가 고민하는 문제는 실행 시점에 유연하게 코드 조각을 넘길 수 있는 두번째 방식이 더 적합하다.

출처 : 김영한 - 스프링 핵심 원리 고급편

profile
꾸준히 하자!

1개의 댓글

comment-user-thumbnail
2023년 8월 10일

이런 유용한 정보를 나눠주셔서 감사합니다.

답글 달기