...
Template Callback Pattern
탬플릿 콜백 패턴(Template Callback Pattern)은 스프링 프레임워크에서 DI(Dependency injection) 의존성 주입에서 사용하는 특별한 전략 패턴이다. 스프링의 JdbcTemplate, RestTemplate, TransactionTemplate, RedisTemplate과 같은곳에 사용된다.
한마디로 GOF 디자인 패턴은 아니고 전략 패턴의 확장판 정도로 보면 된다.
기존의 전략 패턴은 변화되는 전략 알고리즘 부분을 컴파일 타임에서 클래스로 만든뒤 구현체를 주입해 주어야 되지만, 템플릿 콜백 패턴은 런타임 타임에서 익명 클래스를 이용해 동적으로 전략 알고리즘을 주입한다. 용어도 그냥 전략 패턴에서의 컨텍스트(Context)를 템플릿으로 치환한 것일 뿐이며 콜백은 익명 클래스를 만들어진 메서드를 칭하는 것이다.
정리하자면 템플릿 콜백 패턴은 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부분만 자주 바꿔서 사용해야 하는 경우에 적합한 패턴이라고 보면 된다.
콜백(Callback) 이란?
프로그래밍에서 콜백(callback)은 하나의 오브젝트를 다른 오브젝트의 메소드에 매개변수로 넘겨주는 실행 가능한 코드를 말한다. 파라미터로 전달되지만 값을 넘겨주는게 아닌 특정 로직을 담은 일종의 함수를 넘겨서 실행시키기 위해 사용 된다.
즉, callback은 코드가 호출(call)은 되는데 코드를 넘겨준 곳의 뒤(back)에서 실행된다는 것이다. 콜백을 넘겨받는 함수는 이 콜백 함수를 필요에 따라 즉시 실행할 수도 있고, 아니면 나중에 실행할 수도 있다.
자바스크립트의 콜백
자바스크립트를 배운 독자분들이라면 콜백이라는 단어는 질리도록 들어봤을 것이다. 간단하게 함수를 변수에 할당하고 함수의 인자로 넘겨주면 그것이 콜백이다.
// 콜백 함수
const callback = function(num) {
return ++num;
}
// 콜백을 함수의 매개변수로 넘김
function increaseNum(num, callback) {
let result = callback(num);
console.log(result);
}
increaseNum(1, callback); // 2
자바의 콜백
본래 자바의 메소드는 일급 객체가 아니기 때문에 콜백 행위는 불가능 했다. 하지만 자바8에서 등장한 람다함수를 통해 콜백을 구현할 수 있게 되었다.
템플릿 콜백 패턴은 이름만 되게 거창해 보이지 사실 아래 코드 모습과 별반 다르지 않다. 어렵게 생각할 필요가 전혀 없다.
interface IAdd {
int add(int x, int y);
}
public class Main {
public static void main(String[] args) {
int n = result( (x, y) -> x + y ); // 메소드의 매개변수에 람다식을 전달
System.out.println(n); // 3
}
public static int result(IAdd lambda) {
return lambda.add(1,2);
}
}
템플릿 콜백 패턴 흐름
콜백은 보통 단일 메소드로 이루어진 인터페이스를 사용한다. (이를 함수형 인터페이스 라고 부른다)
템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다.
만일 하나의 템플릿에서 여러 가지 종류의 전략을 사용해야 한다면 두개 이상의 콜백 오브젝트를 사용 할 수도 있다. (메소드 구현을 여러개 해놓은 익명 클래스)
클래스 구성
// 콜백
interface Callback {
int execute(final int n);
}
// 템플릿
class Template {
int workflow(Callback cb) {
System.out.println("Workflow 시작");
int num = 100;
int result = cb.execute(num);
return result;
}
}
// 클라이언트
public class Client {
public static void main(String[] args) {
int x = 100;
int y = 20;
Template t = new Template();
int result = t.workflow(new Callback() {
@Override
public int execute(final int n) {
return n * n;
}
});
System.out.println(result); // 100 * 100 = 10000
}
}
클래스 흐름
- 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백(Callback) 객체를 만들고, 콜백이 참조할 정보를 제공한다.
- 만들어진 콜백은 클라이언트가 템플릿의 메소드를 호출할 때 파라미터로 전달된다.
- 템플릿은 정해진 흐름을 따라 작업(Workflow)을 시작한다.
- 내부에서 생성한 참조 정보(변수)를 생성한다.
- 콜백 오브젝트의 메소드 호출하면서 입력값으로 참조 정보(변수)를 전달한다.
- 콜백은 클라이언트 메소드에 있는 정보와 템플릿이 제공한 참조정보를 이용하여
- 메소드 작업을 수행한다.
- 작업 수행한 후 그 결과값을 다시 템플릿에 반환한다.
- 템플릿은 콜백이 돌려준 정보를 사용해서 작업(Workflow)을 마저 수행한다.
- 작업이 마무리되면,
- 최종 결과에 따라 클라이언트에 다시 반환한다.
템플릿 콜백 패턴 특징
- 전략 패턴과 스프링의 의존성 주입(DI)의 장점을 익명 내부 클래스 사용 전략과 결합해 독특하게 활용되는 패턴
패턴 장점
- 전략패턴은 따로 전략 알고리즘을 정해놓은 별도의 전략 클래스가 필요했지만, 템플릿-콜백 패턴은 별도의 전략 클래스 없이, 전략을 사용하는 메소드에 매개변수값으로 전략 로직을 넘겨 실행하기 때문에 전략 객체를 일일히 만들 필요가 없다.
- 외부에서 어떤 전략을 사용하는지 감추고 중요한 부분에 집중할 수 있다.
패턴 단점
- 스프링 클라이언트에서 DI를 사용하지 않게 되면, Bean으로 등록되지 않아 싱글톤 객체가 되지 않게 된다.
- 인터페이스를 사용하지만 실제 사용할 클래스를 직접 선언하기 때문에 결합도가 증가하게 된다. 다만, 그렇다고 해서 무리하게 결합도를 낮추는 행위를 할 필요는 없다.
전략 패턴 → 템플릿 콜백 패턴 변환 예제
기존 전략 패턴 코드
간단하게 사칙 연산을 전략으로 취급하여 구성한 예제 코드이다.
사직 연산을 전략 객체로 구현한 OperationStrategy 군과 이를 사용하는 OperationContext 객체가 있다.
interface OperationStrategy {
// (int x, int y) -> int
int calculate(int x, int y);
}
class Plus implements OperationStrategy {
public int calculate(int x, int y) {
return x + y;
}
}
class Sub implements OperationStrategy {
public int calculate(int x, int y) {
return x - y;
}
}
class Multi implements OperationStrategy {
public int calculate(int x, int y) {
return x * y;
}
}
class Divide implements OperationStrategy {
public int calculate(int x, int y) {
return x / y;
}
}
class OperationContext {
OperationStrategy cal;
void setOpertaion(OperationStrategy cal) {
this.cal = cal;
}
int calculate(int x, int y) {
System.out.println("연산 시작");
int result = cal.calculate(x, y);
System.out.println("연산 종료");
return result;
}
}
public class Client {
public static void main(String[] args) {
int x = 100;
int y = 30;
OperationContext cxt = new OperationContext();
cxt.setOpertaion(new Plus());
int result = cxt.calculate(x, y);
System.out.println(result); // 130
cxt.setOpertaion(new Sub());
result = cxt.calculate(x, y);
System.out.println(result); // 70
cxt.setOpertaion(new Multi());
result = cxt.calculate(x, y);
System.out.println(result); // 3000
cxt.setOpertaion(new Divide());
result = cxt.calculate(x, y);
System.out.println(result); // 3
}
}
템플릿 콜백 패턴 코드
간단한 전략 알고리즘일 경우 굳이 전략 팩토리 객체를 정의하여 코드 라인수만 차지할 필요가 전혀 없다.
템플릿 콜백 패턴으로 변환해주면, 클라이언트에서 바로 익명 클래스 혹은 람다함수로 알고리즘 로직 코드를 정의한뒤 매개변수로 주입만 시켜주면 된다.
- Context가 템플릿 역할
- Strategy 부분이 콜백(Callback)
interface OperationStrategy {
// (int x, int y) -> int
int calculate(int x, int y);
}
// Template
class OperationTemplate {
int calculate(int x, int y, OperationStrategy cal) {
System.out.println("연산 시작");
int result = cal.calculate(x, y);
System.out.println("연산 종료");
return result;
}
}
public class Client {
public static void main(String[] args) {
int x = 100;
int y = 30;
OperationTemplate cxt = new OperationTemplate();
int result = cxt.calculate(x, y, new OperationStrategy() {
// callback
@Override
public int calculate(int x, int y) {
return x + y;
}
});
System.out.println(result); // 130
result = cxt.calculate(x, y, new OperationStrategy() {
// callback
@Override
public int calculate(int x, int y) {
return x - y;
}
});
System.out.println(result); // 70
result = cxt.calculate(x, y, new OperationStrategy() {
// callback
@Override
public int calculate(int x, int y) {
return x * y;
}
});
System.out.println(result); // 3000
result = cxt.calculate(x, y, new OperationStrategy() {
// callback
@Override
public int calculate(int x, int y) {
return x / y;
}
});
System.out.println(result); // 3
}
}
중복 코드 제거하기
위의 클라이언트(main 메소드)에서 전략을 주입하는 부분을 자세히 보면 무언가 반복적으로 익명 클래스를 정의해 주입하는 모습이 보인다. 이러한 부분도 결국 쓸데없는 코드 중복이기 마련이다.
다만, 이 부분은 템플릿 콜백 패턴을 사용하는데 좀 더 최적화를 위해 리팩토링(Refactoring)을 진행할 수도 있다는 예시이지, 반드시 이행할 필요는 없으며, 전략에 따라 선택적으로 이행하면 된다.
아래 코드 역시 명답은 아니며 다양한 전략에 따라 얼마든지 변화될 수 있다는 점은 숙지하자.
class OperationContext {
int calculate(int x, String operation, int y) {
System.out.println("연산 시작");
int result = execute(operation).calculate(x, y);
System.out.println("연산 종료");
return result;
}
// 익명 클래스를 Template 클래스 내에 아예 정의함으로써 클라이언트의 전략 주입 중복 코드를 줄임
private OperationStrategy execute(final String oper) {
return new OperationStrategy() {
public int calculate(int x, int y) {
int result = 0;
switch(oper) {
case "+" : result = x + y; break;
case "-" : result = x - y; break;
case "*" : result = x * y; break;
case "/" : result = x / y; break;
}
return result;
}
};
}
}
public class Client {
public static void main(String[] args) {
int x = 100;
int y = 20;
OperationContext cxt = new OperationContext();
int result = cxt.calculate(x, "+", y);
System.out.println(result); // 120
result = cxt.calculate(x, "-", y);
System.out.println(result); // 80
result = cxt.calculate(x, "*", y);
System.out.println(result); // 2000
result = cxt.calculate(x, "/", y);
System.out.println(result); // 5
}
}
람다로 변환하기
혹은 함수형 인터페이스 구성으로도 처리되는 간단한 전략 알고리즘이라면, 람다표현식을 통해 코드를 획기적으로 짧게 줄일수 있다.
public class Client {
public static void main(String[] args) {
int x = 100;
int y = 30;
OperationContext cxt = new OperationContext();
int result = cxt.calculate(x, y, (x1, y1) -> x1 + y1);
System.out.println(result); // 130
result = cxt.calculate(x, y, (x1, y1) -> x1 - y1);
System.out.println(result); // 70
result = cxt.calculate(x, y, (x1, y1) -> x1 * y1);
System.out.println(result); // 3000
result = cxt.calculate(x, y, (x1, y1) -> x1 / y1);
System.out.println(result); // 3
}
}
템플릿 메서드 패턴 → 템플릿 콜백 패턴 변환 예제
사실 전략 패턴과 템플릿 메서드 패턴은 서로 연관은 없다. 다만 전략 알고리즘을 개별 클래스로 정의하고 때에 따라 주입한다는 관점에서 비슷하기 때문에 이 예제를 넣었다.
템플릿 메서드의 전략 알고리즘 부분은 추상 메소드로서 서브 클래스가 상속하여 개별로 정의하는데, 상속(Inheritance)는 객체 지향 프로그래밍에서 지양해야 할 부분이기 때문에 따로 전략 패턴으로 재구성을 하거나, 아니면 콜백을 이용해 상속 대신 위임으로 처리 할 수 있다.
기존 템플릿 메서드 패턴 코드
예를 들자면 위의 전략 패턴 예제와 같은 사칙 연산 예제를 그대로 가져와 템플릿 메서드 패턴으로 표현하자면 아래와 같이 된다.
abstract class OperationTemplate {
public final void templateMethod(int x, int y) {
int num1 = 100;
int num2 = calculate(x, y);
System.out.println(num1 * num2);
}
abstract int calculate(int x, int y);
}
class Plus extends OperationTemplate {
@Override
int calculate(int x, int y) {
return x + y;
}
}
class Sub extends OperationTemplate {
@Override
int calculate(int x, int y) {
return x - y;
}
}
class Multi extends OperationTemplate {
@Override
int calculate(int x, int y) {
return x * y;
}
}
public class Client {
public static void main(String[] args) {
int x = 100;
int y = 20;
// 1. 템플릿 메서드가 실행할 구현화한 하위 알고리즘 클래스 생성하고 템플릿 실행
OperationTemplate template = new Plus();
template.templateMethod(x, y); // 12000
template = new Sub();
template.templateMethod(x, y); // 8000
template = new Multi();
template.templateMethod(x, y); // 200000
}
}
추상 메서드인 calculate()를 각 서브 클래스들이 오버라이딩하여 재정의하는 형태인데, 이 역시 간단한 전략인 경우 서브 전략 클래스들을 없애고 콜백으로 바꿔버릴 수 있다.
템플릿 콜백 패턴 코드
먼저 인터페이스를 정의하고, 템플릿 메서드의 인자에 인터페이스 매개변수를 추가해주면 된다. 그리고 추상 클래스였던 템플릿 클래스를 일반 클래스로 변환 시킨다.
interface OperationStrategy {
// (int x, int y) -> int
int calculate(int x, int y);
}
class OperationTemplate {
public final void templateMethod(int x, int y, OperationStrategy oper) {
int num1 = 100;
int num2 = oper.calculate(x, y);
System.out.println(num1 * num2);
}
}
public class Client {
public static void main(String[] args) {
int x = 100;
int y = 20;
OperationTemplate template = new OperationTemplate();
template.templateMethod(x, y, (x1, y1) -> x1 + y1); // 12000
template.templateMethod(x, y, (x1, y1) -> x1 - y1); // 8000
template.templateMethod(x, y, (x1, y1) -> x1 * y1); // 200000
}
}
그리고 익명 클래스 혹은 람다함수를 인자로 주게되면, 템플릿 메서드 패턴도 따로 서브 클래스를 만들지 않고도 위임을 이용하여 로직이 문제없이 실행되게 된다.
# 참고자료
토비의 스프링3 (이일민)
https://yongdev.tistory.com/142
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.