이 글은 김영한님의 스프링 고급편 강의중 제목과 관련된 부분을 블로그장의 취향대로 요약한 것이며 강의 자료 및 출처는 가장 아래에서 확인할 수 있습니다.
템플릿 메서드 패턴(Template Method Pattern)
1) 정의
이름 그대로 템플릿을 사용하는 방식이다. 템플릿 메서드 패턴에서 템플릿은 기준이 되는 거대한 틀을 의미하고 여기에 변하지 않는 부분을 몰아둔 후, 일부 변하는 부분을 별도로 호출해서 해결한다.
코드로는 부모 클래스에 변하지 않는 템플릿 코드를 두고, 변하는 부분은 자식 클래스에 두고 상속과 오버라이딩을 사용하여 처리한다.
2) 예제
템플릿 클래스
@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();
}
변하지 않는 부분인 시간 측정 로직을 몰아두고,템플릿 안에서 변하는 부분은 call()메서드를 호출하여 처리한다.
서브 클래스
@Slf4j
public class SubClassLogic1 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직1 실행");
}
}
@Slf4j
public class SubClassLogic2 extends AbstractTemplate {
@Override
protected void call() {
log.info("비즈니스 로직2 실행");
}
}
변하는 부분, 즉 비즈니스 로직이 call()내부에 들어가있다.
호출
@Test
void templateMethodV1(){
AbstractTemplate template1 = new SubClassLogic1();
template1.execute();
AbstractTemplate template2 = new SubClassLogic2();
template2.execute();
}
구조
3) 장점 및 단점
장점: 부모 클래스에 템플릿을 정의하고, 일부 변경되는 로직을 자식 클래스에 정의함으로써 자식 클래스가 알고리즘의 전체 구조를 변경하지 않고, 특정 부분만 재정의할 수 있어 상속과 오버라이딩을 통한 다형성으로 문제를 해결할 수 있다.
단점: 상속에서 오는 단점들을 그대로 안고간다. 부모 클래스와 컴파일 시점에 강하게 결합되는 문제가 있고, 자식 클래스 입장에선 부모 클래스의 기능을 전혀 사용하지 않는다.
📌한계점
자식 클래스 입장에서는 부모 클래스의 기능을 전혀 사용하지 않는데 부모 클래스를 알아야하는것은 좋은 설계가 아니며, 이런 잘못된 의존관계 때문에 부모 클래스를 수정하면 추후 자식 클래스에도 영향을 줄 수 있다.
따라서 이후에 나올 전략 패턴은 템플릿 메서드 패턴과 비슷한 역할을 하면서 상속의 단점을 제거할 수 있다.
전략 패턴(Strategy Pattern)
1) 정의
변하지 않는 부분을 Context라는 곳에 두고, 변하는 부분을 Strategy 인터페이스를 만들어 구현하도록 하여 문제를 해결한다. 위 템플릿 메서드 패턴은 상속으로 문제를 해결했지만 전략 패턴은 위임으로 문제를 해결한다.
2) 예제
Strategy 인터페이스
public interface Strategy { // 이 인터페이스는 변하는 알고리즘 역할을 한다.
void call();
}
Context 클래스
@Slf4j
public class ContextV1 { // 변하지 않는 로직을 가지고 있는 템플릿 역할을 하는 코드. 전략 패턴에서는 이것을 컨텍스트(문맥)라고 얘기한다.
// 쉽게 말해 컨텍스트(문맥)은 크게 변하지 않지만, 그 문맥 속에서 strategy를 통해 일부 전략이 변경된다 생각하면 된다.
private Strategy strategy;
public ContextV1(Strategy strategy){ // Strategy 인터페이스에 의존하고있다. 따라서 Strategy의 구현체를 변경하거나 새로 만들어도 Context 코드에 영향을 주지 않는다.
// 어디서 많이 본 것 같다면 정상이다. 스프링에서 의존관계 주입에서 사용하는 방식이 바로 이 전략패턴이기 때문이다.
this.strategy = strategy;
}
public void execute(){
long startTime = System.currentTimeMillis();
// 비즈니스 로직 실행
strategy.call(); // 위임
// 비즈니스 로직 종료
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("resultTime={}", resultTime);
}
}
Logic이 포함된 변하는 클래스
@Slf4j
public class StrategyLogic1 implements Strategy{ // 비즈니스 로직1을 구현한 클래스
@Override
public void call() {
log.info("비즈니스 로직1 실행");
}
}
호출
@Test
void strategyV1(){
Strategy strategyLogic1 = new StrategyLogic1();
ContextV1 contextV1 = new ContextV1(strategyLogic1);
contextV1.execute();
Strategy strategyLogic2 = new StrategyLogic2();
ContextV1 context2 = new ContextV1(strategyLogic2);
context2.execute();
}
Context 클래스는 내부에 Strategy 필드를 가지고 있고, 이 필드에 변하지 않는 부분인 Strategy 구현체를 주입하면 된다.
전략 패턴의 핵심은 Context는 Strategy 인터페이스에만 의존한다는 점이고, 이 덕분에 Strategy의 구현체를 변경하거나 새로 만들어도 Context코드에는 영향을 주지 않는다.
4.구조
번외로 스프링에서 의존관계 주입에서 사용하는 방식이 바로 위 전략 패턴이다.
3) 전략 패턴의 또 다른 방식
위에서 살펴본 방식은, Context 클래스 생성시 전략을 함께 주입하는 "선 조립, 후 실행" 방식이다.
스프링에서 어플리케이션을 개발할 때, 어플리케이션 로딩 시점에 의존관계 주입을 통해 필요한 의존관계를 모두 맺어두고 난 다음에 실제 요청을 처리하는 것과 같은 원리이다.
그러나 Context와 Strategy를 실행 전에 원하는 모양으로 조립해두고, 그 다음 Context를 실행하는 방식에서 매우 유용하지만 Context와 Strategy를 조립한 이후에는 전략을 변경하기가 번거롭다.
Context에 setter를 제공해서 Strategy를 넘겨 받아 변경한다면 해결될 것 같지만, Context를 싱글톤으로 사용할 경우 동시서 이슈등 고려할 점이 많아지게 된다.
따라서 전략을 실시간으로 변경해야 한다면 선 조립,후 실행이 아닌 아래와 같이 Context를 실행할 때 마다 전략을 인수로 전달하는 방식을 사용할수도 있다.
4) 두 번째 전략 패턴 예제
@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);
}
}
Context클래스만 변경하였다.
변경된 클래스를 보면 전략을 필드로 가지는것이 아닌, 파라미터로 넘겨받아 사용하는 것을 확인할 수 있다.
호출한다면 아래와 같다.
@Test
void strategyV1(){
ContextV2 context = new ContextV2();
context.execute(new StrategyLogic1());
context.execute(new StrategyLogic2());
}
전략을 먼저 생성하고 Context와 함께 생성하는것이 아닌, Context를 먼저 생성하고 실행할 때마다 전략을 인수로 전달하는 것을 확인할 수 있다.
이전 방식과 비교하면 원하는 전략을 더욱 유연하게 변경할 수 있다는 점이다.
5) 위 두가지 방식의 정리
첫 번째 방식은 선 조립,후 실행 방법에 적합하며 Context를 실행하는 시점에 이미 조립이 끝났기 때문에 전략을 신경쓰지 않고 단순히 실행만 하지만 조립 이후 변경이 번거롭다.
두 번째 방식은 실행할 때 마다 전략을 유연하게 변경할 수 있지만 실행할 때 마다 전략을 계속 지정해주어야 한다.
따라서 상황에 맞게 위 두가지중 하나를 선택하여 사용하면 된다.
6)템플릿 콜백 패턴(Template Callback Pattern)
스프링에서는 위 두번째 방식의 전략 패턴을 템플릿 콜백 패턴이라고 한다.
📌콜백(callback) 이란?
프로그래밍에서 콜백(callback)은 "다른 코드의 인수로서 넘겨주는 실행 가능한 코드"를 의미한다.
즉, 코드가 호출(call)은 되는데 코드를 넘겨준 뒤(back)에서 실행된다는 뜻이다.
결과적으로 위 예제에서 콜백은 Strategy가 된다. 클라이언트가 Context.execute(..)를 실행할 때 Strategy를 넘겨주고, Context의 뒤에서 Strategy가 실행되기 때문이다.
템플릿 콜백 패턴은 GOF은 아니지만, 스프링 내부에서 자주 사용하기 때문에 스프링내부에서만 이렇게 부른다.
JdbcTemplate, RestTemplate, TransactionTemplate, RedisTemplate 등등 스프링에서 이름에 XxxTemplate가 있다면 템플릿 콜백 패턴으로 만들어져 있다고 생각하면 된다.
* 출처 자료
'Spring' 카테고리의 다른 글
Spring Advanced 정리 4) 스프링의 프록시 등록 방식 (0) | 2024.03.28 |
---|---|
Spring Advanced 정리 3) 프록시 패턴과 데코레이터 패턴 (0) | 2024.03.26 |
Spring Advanced 정리 1) 쓰레드 로컬 (0) | 2024.03.14 |
Spring DB 정리 4) 트랜잭션 전파 (0) | 2024.03.13 |
Spring DB 정리 3) 트랜잭션AOP 주의사항과 예외 처리 (0) | 2024.03.06 |