본문 바로가기
디자인 패턴

[디자인 패턴] 템플릿/콜백(Template/Callback) 패턴

by doodoom 2022. 9. 30.

0. 이 글을 쓰게 된 이유

토비의 스프링을 공부하면서 템플릿/콜백 패턴에 대해서 접하게 되었다. 스프링에서 제공하는 코드를 보니 굉장히 많이 활용되고 있었고 활용하기 좋은 패턴이어서 정리하게 되었다. 참고로 이 패턴은 GoF의 디자인 패턴에서는 나오지 않는 패턴이다.

1. 템플릿/콜백 패턴이란?

템플릿/콜백 패턴은 전략 패턴의 일종이다. 전략 패턴의 기본적인 구조에 변화되는 부분을 매번 클래스로 만들지 않고, 익명 내부 클래스를 이용해 바로 생성하여 이용하는 식이다.

1.1 용어 정리

  • 템플릿
    템플릿은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀이다. 프로그래밍에서는 고저오딘 틀 안에 바꿀 수 있는 부분을 넣어서 사용하는 경우에 템플릿이라고 부른다. JSP는 HTML이라는 고정된 틀에 EL과 스크립릿이라는 변하는 부분을 넣은 일종의 템플릿 파일이라고 볼 수 있다.
  • 콜백
    콜백은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용한다. 자바에선 메소드 자체를 파라미터로 전달할 방법은 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. 그래서 펑셔널 오브젝트(fucntional object)라고도 한다.

1.2 설명

전략 패턴은 전략이 있고 그것을 사용하는 컨텍스트 클래스가 있을 때, 그 전략을 인터페이스로 정의하고 컨텍스트 클래스에 전략 인터페이스를 구현한 클래스
를 DI 해줌으로써 다양한 전략 알고리즘을 사용하는 패턴이다. (잘 모르겠으면 전 글 참고)
여기서 클라이언트는 템플릿을 사용하는 쪽을 말한다. 템플릿은 메소드로 이루어져 있다. 그리고 템플릿은 특정 인터페이스 타입의 오브젝트(이를 Callback이라고 한다.)를 파라미터로 받아서 그 오브젝트에 있는 함수를 호출하여 템플릿 내에 변하는 부분을 넣는다. 작업의 흐름은 다음과 같다.

  1. 클라이언트는 템플릿에 특정 인터페이스 타입의 Callback 오브젝트를 생성한다. Callback 오브젝트는 익명 클래스로 만든다.
  2. 템플릿을 호출하면서 Callback 오브젝트를 전달한다.
  3. 템플릿의 workflow가 실행된다.
  4. 템플릿 중간에 바뀌는 부분에서 참조정보를 생성한다.
  5. Callback 오브젝트에 있는 메소드를 호출한다.
  6. 호출된 Callback 메소드는 Client의 final 변소를 참조한다.
  7. Callback 메소드가 작업을 수행한다.
  8. 결과를 다시 템플릿에 전달한다.
  9. 템플릿의 workflow가 계속 진행된다.
  10. workflow가 마무리 된다.
  11. template의 결과를 클라이언트에 전달한다.

2, 예시

기존 코드

public class Student {

    public void study(final Subject subject) {
        System.out.println("씻고 책상에 앉기");

        System.out.println(subject.getName() + " 공부하기");

        System.out.println("책상 정리하기");
    }

    public void shopping(final Product product) {
        System.out.println("씻고 책상에 앉기");

        System.out.println(product.getName() + " 검색하고 구매하기");

        System.out.println("책상 정리하기");
    }
}

Student라는 코드가 있고 study와 shooping이라는 메소드가 있다.
메소드들을 잘 보면 가운데 줄 빼고 나머지는 모두 겹친다. 지금은 메소드가 두개뿐이라서 괜찮지만 많아지면 굉장히 피곤할 것이다. 우리는 이 중복되는 코드를 줄이고 싶다.

2.1 Strategy(BehaviorStrategy)

public interface BehaviorStrategy {

    void doSomething();
}

전략 패턴을 이용하기 위해 위와 같은 인터페이스를 정의한다.

2.2 Ten=mplate(BehaviorContext.hehaviorTemplate)

public class BehaviorContext {

    public void behaviorTemplate(BehaviorStrategy bs) {
        System.out.println("씻고 책상에 앉기");

        bs.doSomething();

        System.out.println("책상 정리하기");
    }

}

Template을 만들기 위해 BehaviorContext를 만들고 그 안에 template 메소드를 만든다.
template 메소드에서는 콜백함수인 doSomething을 호출한다.

2.3 Client(Student)

public class Student {

    private BehaviorContext behaviorContext;

    public void setBehaviorContext(
        BehaviorContext behaviorContext) {
        this.behaviorContext = behaviorContext;
    }

    public void study(final Subject subject) {
        behaviorContext.behaviorTemplate(new BehaviorStrategy() {
            @Override
            public void doSomething() {
                System.out.println(subject.getName() + " 공부하기");
            }
        });
    }

    public void shopping(final Product product) {
        behaviorContext.behaviorTemplate(new BehaviorStrategy() {
            @Override
            public void doSomething() {
                System.out.println(product.getName() + " 검색하고 구매하기");
            }
        });
    }
}

Student code는 다음과 같게 변화한다.
각 메소드 안에 BehaviorStrategy 인터페이스를 구현하는 익명 클래스를 만들게 해서 콜백 함수인 doSomething을 만들도록한다.

3. 템플릿/콜백 패턴의 장점

  1. 클라이언트의 메소드를 간결하게 할 수 있다. 각 메소드들은 최소한의 행동만 정의함으로서 맡은 역할에 집중할 수 있다.
  2. 확장이 쉽다. 만약 메소드를 하나 더 추가하는데 위의 메소드들과 비슷하다면 템플릿을 활용하면 빠르게 만들 수 있다.

4. 템플릿/콜백 패턴의 단점

코드의 중복되는 부분이 짧을 때 괜히 사용하면 오히려 더 길어질 수 있다. 위와 같은 예시가 대표적이라고 할 수 있다. 하지만 그 부분은 다음과 같이 해결이 가능하다.

4.1 BehaviorContext 수정

public class BehaviorContext {

    public void behaviorTemplate(BehaviorStrategy bs) {
        System.out.println("씻고 책상에 앉기");

        bs.doSomething();

        System.out.println("책상 정리하기");
    }

    public void execute(final String behavior) {
        behaviorTemplate(
            new BehaviorStrategy() {
                @Override
                public void doSomething() {
                    System.out.println(behavior);
                }
            }
        );
    }

}

BehaviorContext를 위와 같이 수정했다. 클라이언트 코드는 execute 메소드만 호출해서 behavior만 전달하면 된다.

4.2 깔끔해진 Client(Student) 코드

public class Student {

    private BehaviorContext behaviorContext;

    public void setBehaviorContext(
        BehaviorContext behaviorContext) {
        this.behaviorContext = behaviorContext;
    }

    public void study(final Subject subject) {
        behaviorContext.execute(subject.getName() + " 공부하기");
    }

    public void shopping(final Product product) {
        behaviorContext.execute(product.getName() + " 검색하고 구매하기");
    }
}

이렇게 하면 훨씬 간결하게 템플릿/콜백을 사용할 수 있다.

5. 템플릿/콜백 패턴의 활용

고정된 작업 흐름을 갖고 있으면서 자주 반복되는 코드가 있다면 다음과 같이 생각해볼 수 있다.

  1. 메소드로 추출해서 반복을 줄인다.
  2. 만약 그 중 일부 작업을 필요에 따라 바꾸어 사용해야한다면 사이에 인터페이스를 두고 전략 패턴을 고민해본다.
  3. 그런데 바뀌는 부분이 한 어플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 템플릿/콜백 패턴을 적용하는 것을 고려해볼 수 있다.