ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 템플릿 패턴의 한계와 전략 패턴
    스프링/스프링 AOP 2023. 9. 5. 17:10

    템플릿 패턴은 부모 클래스가 바뀌면 그것을 의존하고 있는 자식 클래스도 다 바뀌어야 한다는 단점이 있다. 또한, 자식 클래스가 부모 클래스를 알고있어야 하기 때문에 객체지향 관점에서 좋지 않다

     

    전략 패턴

    이러한 문제점을 해결하기 위해서 전략 패턴이라는 설계 패턴이 있다. 전략 패턴은 변하지 않는 부분을 Context에 두고 변하는 부분을 Strategy라는 인터페이스를 선언한 다음 해당 인터페이스를 구현하는 방식이다.

     

    사진 출처 : 인프런 스프링 강의

    전략 패턴은 인터페이스에만 의존하기 때문에 구현체를 바꿔주더라도 큰 문제점이 없다.

     

     

    Strategy 인터페이스

    public interface Strategy {
        void call();
    }

     

    StrategyLogic1

    @Slf4j
    public class StrategyLogic1 implements Strategy {
        @Override
        public void call() {
            log.info("비즈니스 로직1 실행");
        }
    }

     

    StrategyLogic2

    @Slf4j
    public class StrategyLogic2 implements Strategy {
        @Override
        public void call() {
            log.info("비즈니스 로직2 실행");
        }
    }

     

     

    전략 패턴은 Strategy를 통해 구현은 하지 않고 선언만 한 다음에 구현체들을 통해 구현하는 방식이다.

    /**
     * 필드에 전략을 보관하는 방식
     */
    
    @Slf4j
    public class ContextV1 {
    
        private Strategy strategy;
    
        public ContextV1(Strategy strategy) {
            this.strategy = strategy;
        }
    
        public void execute() {
            long startTime = System.currentTimeMillis();
            //비즈니스 로직 실행
            strategy.call(); //위임
            //비즈니스 로직 종료
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("resultTime={}", resultTime);
        }
    }

    execute()라는 공통 로직 안에 strategy.call()이라는 인터페이스를 넣어둔다. 실제로 사용하는 시점에 무슨 로직을 사용할 지를 Context()의 파라미터에 넣어주면 원하는 로직을 실행시킬 수 있다.

     

     

     

    테스트에서 전략패턴을 사용하는 모습

    /**
    * 전략 패턴 적용
    */
    @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);
        contextV1.execute();
    }

    클래스를 별로도 생성하고 싶지 않으면 익명 내부클래스를 활용해서 바로 오버라이드해서 사용해주면 된다.

     

    @Test
    void strategyV2() {
        // 익명 내부 클래스
        Strategy strategyLogic1 = () -> log.info("비즈니스 로직1 실행");
        ContextV1 contextV1 = new ContextV1(strategyLogic1);
        contextV1.execute();
    }

    람다식을 사용하면 훨씬 더 간편해진다. 람다를 사용하려면 메서드가 1개만 있어야 하는데 인터페이스에 메서드가 1개만 있으므로 람다 사용이 가능하다.

     

     

    정리

    중요한 것은 선 조립, 후 실행 방식이다. execute()로 실행을 하기전에 조립을 해주는 것이다.

    스프링의 강점인 애플리케이션 로딩 시점에 의존관계 주입을 통해 의존관계를 맺어두고 실제 요청을 처리하는 것과 같은 원리이다.

     

     

    단점

    Context와 Strategy를 조립한 이후에는 전략이 번거롭다는 점이 단점이다. Context를 싱글톤으로 사용할 때는 동시성 이슈 등 고려할 점이 많다. 그렇기 때문에 더 위와 같은 방식말고 아래와 같은 방식으로 사용해야 한다.

     

     

     

     

     

    @Slf4j
    public class ContextV2 {
    
        public void execute(Strategy strategy) {
            long startTime = System.currentTimeMillis();
            //비즈니스 로직 실행
            strategy.call(); //위임
            //비즈니스 로직 종료
            long endTime = System.currentTimeMillis();
            long resultTime = endTime - startTime;
            log.info("resultTime={}", resultTime);
        }
    }

    execute() 로직은 파라미터로 Strategy를 받고, execute()로직 안에 strategy.call() 메서드가 있다.

    @Slf4j
    public class ContextV2Test {
        /**
         * 전략 패턴 적용
         */
        @Test
        void strategyV1() {
            ContextV2 context = new ContextV2();
            context.execute(new StrategyLogic1());
            context.execute(new StrategyLogic2());
        }
    }

    Context를 실행할 때마다 전략 인수를 파라미터로 전달하는 방식이다. 클라이언트는 Context를 실행하는 시점에 원하는 Strategy를 전달할 수 있다. 이렇게 설계하면 위에서 소개한 방식의 단점을 해결하며 훨씬 유연하게 설계할 수 있다.

     

     

     

    파라미터에 Strategy를 전달받는 방식으로 전략 패턴을 구사한 것이 실행할 때 마다 전략을 유연하게 변경할 수 있다는 장점이 있다. 하지만 실행할 때마다 전략을 지정해줘야 한다는 단점이 있다.

     

     

     

     

    각각 장단점이 있지만 위의 방식보다 아래의 방식으로 설계하는 것이 더 좋은 방식이다.

Designed by Tistory.