스프링 고급편 내용정리 <템플릿 메서드 패턴 / 전략 패턴>
템플릿 메서드 패턴
이름 그대로 템플릿을 이용하는 방식. 템플릿에 변하지 않는 부분을 몰아두고, 일부 변하는 부분을 별도로 호출해서 해결한다.
@Slf4j
public abstract class AbstractTemplate {
public void execute() {
long startTime = System.currentTimeMillis();
//비즈니스 로직 실행
call(); //상속
//비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
protected abstract void call();
}
다음과 같은 추상클래스 AbstractTemplate을 만든다.
@Slf4j
public class SubClassLogic1 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직1 실행"); }
}
그리고 AbstractTemplate을 상속한 subClassLogic 1,2를 만든다.
@Test
void templateMethodV1() {
AbstractTemplate template1 = new SubClassLogic1();
template1.execute();
AbstractTemplate template2 = new SubClassLogic2();
template2.execute();
}
SubClassLogic1 / 2는 모두 AbstractTemplate을 부모로 가지고 있기 때문에, template.execute()를 실행하면 SubClassLogic의 call()이 호출된다.
예제와 같이 템플릿 메서드 패턴을 이용하면 변하는 부분과 변하지 않는 부분의 코드를 다형성을 통해 분리할 수 있다.
템플릿 메서드 패턴을 통해서 부모 클래스에 골격을 정의하고 자식 클래스에는 변경되는 로직을 정의한다면, 상속과 오버라이딩을 통한 다형성으로 로직의 특정부분만 재정의 할 수 있다.
하지만, 상속을 받는다는 것은 특정 부모클래스를 의존한다는 것이다.
부모 클래스의 기능을 사용하건 사용하지않건, 부모 클래스에 강하게 의존하게 된다.
설계의 관점에서 부모 클래스의 기능을 전혀 사용하지 않는 자식클래스가 부모 클래스를 알아야 하는것은 좋은 설계가 아니다. 또한 이런 의존관계 때문에, 부모 클래스를 수정하면 자식 클래스에도 영향을 줄 수 있다.
또한 템플릿 메서드 패턴은 상속 구조를 이용하기 때문에 별도의 클래스나 익명 내부 클래스를 만들어야 한다는 부분도 복잡하다.
전략 패턴
- 선조립 후 실행 방식(필드 주입 방식)
템플릿 메서드 패턴은 부모클래스에 변하지않는 템플릿을 두고, 변하는 부분은 자식 클래스에 둬서 상속을 통해서 문제를 해결했다.
전력패턴은 변하지 않는 부분을 Conterxt라는 곳에 두고, 변하는 부분을 Strategy라는 인터페이스에 두고 구현을 통해서 문제를 해결한다.
public interface Strategy {
void call();
}
Strategy 인터페이스를 만들고 이를 구현한 StrategyLogic 1,2를 만든다.
@Slf4j
public class StrategyLogic2 implements Strategy {
@Override
public void call() {
log.info("비즈니스 로직2 실행");
}
}
그리고 Context 클래스를 만들어준다.
@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);
}
}
Context 내부에는 strategy 필드를 가지고 있다.
전략 패턴의 핵심은 Context는 Strategy 인터페이스에만 의존하고 구현체에는 의존하지 않는다는 점이다.
스프링에서 사용하는 의존관계 주입에서 사용하는 방식이 바로 전략패턴 방식이다.
@Test
void strategyV1() {
Strategy strategyLogic1 = new StrategyLogic1();
ContextV1 context1 = new ContextV1(strategyLogic1);
context1.execute();
Strategy strategyLogic2 = new StrategyLogic2();
ContextV1 context2 = new ContextV1(strategyLogic2);
context2.execute();
}
ContextV1에 Strategy 인터페이스를 구현한 구현체들을 주입하고 execute()를 호출하면,
execute()내부의 call()이 호출된다.
선 조립 후 실행 방식의 경우 위와 같은 방식이 매우 효과적이지만, 조립 이후에는 전략의 변경이 번거롭다는 단점이 있다. Context의 필드에 Strategy를 주입 받는 것이 아닌, 파라미터로 받는 방식을 사용하면 어떨까?
- 파라미터로 Strategy를 받는 방식
@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); }
}
ContextV2의 경우, Strategy를 필드로 주입받지 않고 execute()가 실행될 때 파라미터로 전달 받는다.
@Test
void strategyV1() {
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());
}
이 방식을 이용하면 선 조립을 하는 방식과 비교해서 원하는 전략을 더욱 유연하게 변경할 수 있다.
하지만 이 점은 실행할 때 마다 전략을 지정해주어야 한다는 단점으로도 작용할 수 있다.
템플릿 콜백 패턴
위의 파라미터로 Strategy를 넘겨주는 ContextV2방식을 스프링에서는 템플릿 콜백 패턴이라고 한다.
전략패턴에서 템플릿과 콜백부분이 강조된 것으로써, 스프링 내부에서 이런방식이 자주 사용되기 때문에 이렇게 불리지만 GOF 패턴은 아니다.
스프링에서 사용되는 JdbcTemplate, RestTemplate,TransferTemplate,RedisTemplate등의 xxxTemplate 이름을 가진 것들이 템플릿 콜백패턴으로 만들어졌다.
@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);
}
}
ContextV2의 이름만 TimeLogTemplate으로 변경하였다.
@Test
void callbackV1() {
TimeLogTemplate template = new TimeLogTemplate();
template.execute(new Callback() {
@Override
public void call() { log.info("비즈니스 로직1 실행");}
});
template.execute(new Callback() {
@Override
public void call() {
log.info("비즈니스 로직2 실행");}
});
}
별도의 클래스를 생성할 수도 있지만, 콜백을 사용할 경우 다음과 같이 익명 내부 클래스를 사용하거나 람다를 사용하는 것이 편리하다.
@Test
void callbackV2() {
TimeLogTemplate template = new TimeLogTemplate();
template.execute(() -> log.info("비즈니스 로직1 실행"));
template.execute(() -> log.info("비즈니스 로직2 실행"));
}
참고
김영한님 스프링 고급편 강의