전략 패턴 & 템플릿 콜백 패턴

ForLearn·2022년 9월 23일
0

전략 패턴

전략 패턴은 변하지 않는 부분을 Context 라는 곳에 두고, 변하는 부분을 Strategy 라는 인터페이스를 만들어 해당 인터페이스를 구현하여 문제를 해결한다.

템플릿 메서드 패턴과의 차이점은 상속이 아니라 위임으로 문제를 해결한다는 점이다.

예제

ContextV1

변하지 않는 로직을 가진 템플릿 역할을 하는 코드이다.
컨텍스트는 변하지 않고 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);
    }
}
  • 필드에 전략을 보유하고 있다. 이 필드에 변하는 부분인 Strategy 의 구형체를 주입한다.
  • 전략 패턴의 핵심은 ContextStrategy 인테페이스에만 의존한다는 것이다.
    • 따라서 Strategy 구현체는 Strategy 구현체에 영향을 받지 않는다.
    • 스프링의 의존관계 주입이 바로 전략 패턴이다.

Strategy

전략을 인터페이스로 생성하여

public interface Strategy {
    void call();
}

StrategyLogic1

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

strategyV1 Test

    @Test
    void strategyV1() {
        StrategyLogic1 strategyLogic1 = new StrategyLogic1();
        ContextV1 context1 = new ContextV1(strategyLogic1);
        context1.execute();
    }
  • 사용할 Strategy를 구현한 다음 ContextV1에 조립한 후 메서드를 호출한다.
  • Strategy를 익명 내부 클래스 & 람다식으로 대체할 수 있다.

익명 내부 클래스 & 람다식 활용

    @Test
    void strategyV1() {
    
    // 익명 내부 클래스 활용
            ContextV1 context1 = new ContextV1(new Strategy() {
            @Override
            public void call() {
                log.info("비즈니스 로직1 실행");
            }
        });
        context1.execute();
    
    
    
       // 람다로 변환.
        ContextV1 context2 = new ContextV1(() -> log.info("비즈니스 로직1 실행"));
        context2.execute();

    }
  • 람다로 변경하려면 인터페이스에 메서드가 1개만 있어야 하는 점을 주의하자.

선조립, 후 실행

위의 예제를 보면 ContextStrategy를 조립하고 Context를 실행한다.

이는 스프링이 의존관계 주입을 통해 필요한 의존관계를 모두 맺고 실제 요청을 처리하는 것과 같은 원리이다.

이 방식의 단점은 ContextStrategy를 조립한 후 변경하기 번거롭다는 점이다 . 물론 Contextsetter를 사용하여 변경할 수 있지만, 싱글톤으로 사용하는 경우 동시성 문제등이 있다.

Strategy를 매개변수로...

ContextV2

@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(...)가 호출될 때 마다 항상 파라미터로 전략을 전달 받아 사용한다.

ContextV2Test

    @Test
    void strategyV1() {
        ContextV2 context = new ContextV2();
        context.execute(new StrategyLogic1());
        context.execute(new StrategyLogic2());
    }

    /**
     * 전략 패턴 익명 내부 클래스
     */
    @Test
    void strategyV2() {
        ContextV2 context = new ContextV2();

        context.execute(new Strategy() {
            @Override
            public void call() {
                log.info("익명 내부 클래스로 로직 실행.");
            }
        });

        context.execute(()->log.info("람다로 비즈니스 로직 실행"));

    }
  • 실행할 때마다 전략을 유연하게 변경하는 장점이 이지만, 계속 전략을 지정해 주어야 하는 것이 단점이다.

템플릿 콜백 패턴

위의 ContextV2Test 예제에서 ContextV2 는 변하지 않는 템플릿 역할을 하고 , 변하는 부분은 파라미터로 넘어오는 Strategy 의 코드를 실행해서 처리한다.

이처럼 다른 코드의 인수로서 넘겨주는 실행가능한 코드를 콜백(callback)이라고 한다.

자바에서는 실행 가능한 코드를 인수로 넘기려면 객체가 필요하다. 자바8 이전에는 익명 내부 클래스를 사용했고, 자바8 이후에는 람다를 사용한다.

템플릿 콜백 패턴

  • 사실 Context2 와 같은 방식의 전략패턴을 템플릿 콜백 패턴이라고 한다.
  • 전략패턴에서 Context가 템플릿 역할을 하고, Strategy 부분이 콜백으로 넘어온다.
  • 템플릿 콜백 패턴은 GOF 디자인 패턴은 아니고, 스프링 안에서만 불린다.
  • 스프링에서 JdbcTemplate , RestTemplate, TransactionTemplate , RedisTemplate 처럼 XxxTemplate가 있다면 템플릿 콜백 패턴으로 만들어진 것이라 생각하면 된다.

템플릿 콜백 패턴 - 예제

Callback

public interface Callback {
    void call();
}

TimeLogTemplate

@Slf4j
public class TimeLogTemplate  {

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

TemplateCallbackTest

@Slf4j
public class TemplateCallbackTest {


    @Test
    void callbackV1(){

        TimeLogTemplate template = new TimeLogTemplate();

        template.execute(new Callback() {
            @Override
            public void call() {
                log.info("비즈니스 로직 1 실행");
            }
        });

        template.execute(()->log.info("비즌니스 로직 2 실행 "));
    }
}
  • Strategy역할의 Callback인터페이스를 익명 내부 클래스 & 람다의 형태로 Template에 전달하여 사용한다.

한계

애플리케이션 개발할 때 템플릿 콜백 패턴과 같은 디자인 패턴을 사용하여 코드의 중복을 줄이는 등 장점이 있었다.
하지만 결국 부가 기능을 추가하기 위해 원본 코드를 수정해야 한다는 단점이 있다.

원본 코드를 수정하지 않고 부가기능을 추가하기 위해서는 프록시라는 개념을 이해할 필요가 있다.

Reference

스프링 핵심원리 고급편 - 김영한 https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard
스프링 입문을 위한 자바 객체지향의 원리와 이해

0개의 댓글