본문 바로가기
Spring/spring AOP

스프링 코어2 -전략 패턴

by 킹차니 2022. 1. 5.

템플릿 메서드 패턴의 단점 

템플릿 메서드 패턴은 추상적인 템플릿(AbstractClass)과 그것을 상속한 SubClass간의 강한 결합이 필요로 하다. 이미 상속한다는 데서 강한 결합력을 가지게 된다. AbstractClass의 코드가 수정된다면 SubClass의 코드도 영향을 받기 때문이다.

하지만 전략 패턴은 상속보다 위임을 사용하기 때문에 두 클래스 간의 결합도가 낮아진다.

 

전략 패턴은 Context(변하지 않는 부분. 즉 로그 추적기) 클래스가 Strategy 타입 인터페이스를 필드에 들고 있고, 구현체인 Strategy객체를 외부에서 주입받는다. 그림으로 보면 아래와 같다.

 

전략 패턴을 간단히 구현해보고 이를 테스트 해보자

 

먼저 Context인 변하지 않는 부분은 비즈니스 로직의 수행되는 시간을 측정하는 부분이고, call 메서드가 비즈니스 로직이다.

import lombok.extern.slf4j.Slf4j;
/*
* 필드에 전략을 보관하는 방식
* */
@Slf4j
public class ContextV1 {
    private final Strategy strategy;

    public ContextV1(Strategy strategy) {
        this.strategy = strategy;
    }
    //context는 변하지 않는 로직을 뜻함.
    public void execute(){
        long startTime = System.currentTimeMillis();

        /*-----비즈니스 로직 실행-----*/
        strategy.call(); // 위임을 사용
        /*-----비즈니스 로직 종료-----*/

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }
}

Context클래스는 Strategy를 생성자에서 받는다.

excute메서드를 보면 strategy.call 메서드 위아래로 변하지 않는 코드(비즈니스 로직 시간 측정)가 존재한다. 

 

Strategy 인터페이스는 변하는 부분(비즈니스 로직)만을 위한 추상 메서드를 가지고 있다.

public interface Strategy {
    void call();
}

 

이제 각 변하는 로직에 따라 위 인터페이스를 구현하면 되는 것이다.

import lombok.extern.slf4j.Slf4j;

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


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

 

이제 이를 테스트해보자.

@Test
void strategyV1() {
    Strategy strategyLogic1 = new StrategyLogic1();
    ContextV1 contextV1 = new ContextV1(strategyLogic1);//의존성 주입
    contextV1.execute();

    Strategy strategyLogic2 = new StrategyLogic2();
    ContextV1 contextV2 = new ContextV1(strategyLogic2);//의존성 주입
    contextV2.execute();
}


실행결과:

비즈니스 로직1 실행
resultTime=3
비즈니스 로직2 실행
resultTime=0

결과가 잘 나오는 것을 확인할 수 있다.

 

물론 아래처럼 익명 내부 클래스를 사용할 수도 있다.

@Test
void strategyV2() {
    ContextV1 contextV1 = new ContextV1(new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직1 실행");
        }
    });//의존성 주입
    contextV1.execute();

    ContextV1 contextV2 = new ContextV1(new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직2 실행");
        }
    });//의존성 주입
    contextV2.execute();
}



/*람다로 변경 (물론 이때는 인터페이스에 메서드가 한개만 존재해야 한다.) */
@Test
void strategyV3() {
    ContextV1 contextV1 = new ContextV1(()->log.info("비즈니스 로직1 실행"));//의존성 주입
    contextV1.execute();

    ContextV1 contextV2 = new ContextV1(()->log.info("비즈니스 로직2 실행"));//의존성 주입
    contextV2.execute();
}

 

 

이렇게 전략 패턴을 사용하는 방법은 선 조립, 후 실행하는 방법이다. 즉 미리 Context와 어떤 Strategy를 사용할지를 조립해두고, Context의 excute메서드를 실행하기만 하면 된다.

하지만 물론 단점도 존재하는데, Context와 Strategy를 조립한 이후에는 전략을 변경하기 힘들다는 것이다.

하여 전략을 실시간으로 변경해야 한다면, 우리가 위에서 개발한 테스트 코드처럼 Context를 하나 더 생성하고 그곳에 다른 Strategy를 주입하는 것이 방법이다.

 

하지만 더 유용하게 사용하는 방법이 있다.

 


위에서는 Context가 Strategy를 필드로 가지고, 생성자를 통해 Strategy를 주입받았다면, 이번에는 Context의 execute메서드 파라미터로 Strategy를 전달받는 것이다.

import lombok.extern.slf4j.Slf4j;

/*
* 전략을 파라미터로 전달받는 방식
* */
@Slf4j
public class ContextV2 {

    //context는 변하지 않는 로직을 뜻함.
    public void execute(Strategy strategy){
        long startTime = System.currentTimeMillis();

        /*-----비즈니스 로직 실행-----*/
        strategy.call(); // 위임을 사용
        /*-----비즈니스 로직 종료-----*/

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime={}", resultTime);
    }
}

이를 테스트 해보면 아래와 같다.

@Test
void strategyV4_0() {
    ContextV2 contextV2 = new ContextV2();//의존성 주입
    contextV2.execute(new StrategyLogic1());
    contextV2.execute(new StrategyLogic2());
}

//익명 내부 클래스 사용
@Test
void strategyV4_1() {
    ContextV2 contextV2 = new ContextV2();//의존성 주입
    contextV2.execute(new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직1 실행");
        }
    });
    contextV2.execute(new Strategy() {
        @Override
        public void call() {
            log.info("비즈니스 로직2 실행");
        }
    });
}

//람다식 사용
@Test
void strategyV4_2() {
    ContextV2 contextV2 = new ContextV2();//의존성 주입
    contextV2.execute(()->log.info("비즈니스 로직1 실행"));
    contextV2.execute(()->log.info("비즈니스 로직2 실행"));
}

템플릿
지금 우리가 해결하고 싶은 문제는 변하는 부분변하지 않는 부분 분리하는 것이다. 변하지 않는 부분을 템플릿이라고 하고, 그 템플릿 안에서 변하는 부분에 약간 다른 코드 조각을 넘겨서 실행하는 것이 목적이다.
지금 우리가 원하는 것은 애플리케이션 의존 관계를 설정하는 것 처럼 선 조립, 후 실행이 아니다. 단순히 코드를 실행할 때 변하지 않는 템플릿이 있고, 그 템플릿 안에서 원하는 부분만 살짝 다른 코드를 실행하고 싶을 뿐이다. 따라서 우리가 고민하는 문제는 실행 시점에 유연하게 실행 코드 조각을 전달하는 ContextV2(메서드 파라미터로 Strategy전달)가 더 적합하다.

 

 

 

 

 

김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.