본문 바로가기
TIL

템플릿 메소드 패턴 vs. 전략 패턴

by wch_t 2024. 7. 14.

1. 개요

매달 동작하는 스케줄러를 개발하게 되면서 DB 테이블을 어떻게 수정해야 될 지에 대한 고민을 하게 됐다.

 

1) 기존 테이블과 비교하면서 update 하는 방식

2) 기존 row 데이터를 전부 delete하고, 새롭게 update 하는 방식

 

위 두 가지 방식에 대해 생각하여 제안을 드렸는데, 각 방식에 대한 성능 분석을 해달라는 코멘트를 받고 코드 작성에 들어갔다!

 

DB 수정 전략에 대해 추상화 해 놓으면 좋겠다는 생각에

"템플릿 메소드 패턴과 전략 패턴 중에 어떤 것을 적용하는 것이 좋을까?" 하는 물음이 들었다.

(물음에 답은 아래 '5. 정리' 에서)

 

 


 

2. 템플릿 메소드 패턴

 

기본적인 정의에 대해서 먼저 알아보자.

 

1) 정의

 

: 여러 클래스에서 공통으로 사용하는 메서드를 템플릿화 하여 부모 클래스에서 정의하고, 자식 클래스마다 세부 동작 사항을 다르게 구현하는 패턴이다.

 

 

 

2) 특징

 

사용 시기

- 공통 구조를 상위 클래스에서 정의하고,  확장 및 변화가 필요한 부분만 하위 클래스에서 구현할 때

 

장점

- 알고리즘의 공통 구조를 상위 클래스에서 정의하므로 코드 중복을 줄일 수 있다.

- 상위 클래스의 코드 변경 없이도 하위 클래스에서 기능을 확장하거나 변경할 수 있다.

- 상위 클래스에서 핵심 로직을 관리하므로, 관리가 용이해진다.

   (할리우드 원칙: 고수준 객체에서 저수준 객체를 다루는 원칙)

 

단점

- 추상 메소드가 많아지면, 하위 클래스의 생성 및 관리가 어려워질 수 있다.

 

 

 

3) 구조

 

AbstractClass (추상 클래스)

: 템플릿을 담당하는 상위 클래스를 추상 클래스로 정의하고,

템플릿 메소드를 정의하고, 템플릿 메소드에서 실행되는 추상 메소드를 정의한다.

 

ConcreateClass (구현 클래스)

: AbstractClass를 상속하여, 추상 메소드를 구체적으로 구현한다.

 

hook 메소드

: 부모의 템플릿 메소드의 영향이나 순서를 제어하고 싶을 때 사용되는 일반 메소드이다. (추상 메소드 x)

자식 클래스에서 선택적으로 Override 하여 제어할 수 있다.

 

 

 

4) 코드

abstract class AbstractClass {
    // 템플릿 메소드 : 메서드 앞에 final 키워드를 붙이면 자식 클래스에서 오버라이딩이 불가능함.
    // 자식 클래스에서 상위 템플릿을 오버라이딩해서 자기마음대로 바꾸도록 하는 행위를 원천 봉쇄
    public final void templateMethod() {
        // 상속하여 구현되면 실행될 메소드들
        step1();
        step2();

        if(hook()) { // 안의 로직을 실행하거나 실행하지 않음
            // ...
        }

        step3();
    }

    boolean hook() {
        return true;
    }

    // 상속하여 사용할 것이기 때문에 protected 접근제어자 설정
    protected abstract void step1();
    protected abstract void step2();
    protected abstract void step3();
}

 

class ConcreteClassA extends AbstractClass {

    @Override
    protected void step1() {}

    @Override
    protected void step2() {}

    @Override
    protected void step3() {}
}

class ConcreteClassB extends AbstractClass {

    @Override
    protected void step1() {}

    @Override
    protected void step2() {}

    @Override
    protected void step3() {}

    // hook 메소드를 오버라이드 해서 false로 하여 템플릿에서 마지막 로직이 실행되지 않도록 설정
    @Override
    protected boolean hook() {
        return false;
    }
}

 

class Client {
   public static void main(String[] args) {
       // 1. 템플릿 메서드가 실행할 구현화한 하위 알고리즘 클래스 생성
       AbstractTemplate templateA = new ImplementationA();

       // 2. 템플릿 실행
       templateA.templateMethod();
   }
}

 

 

5) 라이브러리 적용 예시

 

java.io 패키지의 InputStream / OutputStream / Reader / Writer

: byte[]에서 지정된 offset부터 length까지의 byte들을 하나씩 write(int b) 추상 메소드를 통해 stream에 쓰는 동작이다.

public abstract class OutputStream implements Closeable, Flushable {

    public abstract void write(int b) throws IOException;

    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }

    public void write(byte[] b, int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }

        for (int i = 0 ; i < len ; i++) {
            write(b[off + i]);
        }
    }

 

 

 

javax.servlet 패키지의 HttpServlet

: service 메소드는 템플릿 메소드로서, HTTP 요청 타입을 확인하고 이에 따라 적절한 hook 메소드를 호출한다.

하위 클레스에서 doGet, doPost ... 등의 메소드를 재정의하여, 각 HTTP 요청 메소드에 대한 구체적인 처리를 구현한다.

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();

    if (method.equals("GET")) {
        doGet(req, resp); // hook 메소드
    } else if (method.equals("POST")) {
        doPost(req, resp);
    } else if (method.equals("PUT")) {
        doPut(req, resp);
    } else if (method.equals("DELETE")) {
        doDelete(req, resp);
    } else {
        // 기타 HTTP 메소드에 대한 처리
        ...
    }
}

 

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;

public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.getWriter().println("<h1>Hello, GET request!</h1>");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.getWriter().println("<h1>Hello, POST request!</h1>");
    }
}

 

 


 

3. 전략 패턴

 

1) 정의

 

: 실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 변경할 수 있게 하는 패턴이다.

 

 

 

2) 특징

 

사용 시기

- 여러 버전의 전략 알고리즘을 구현해야 할 때 (ex. 정렬, 경로탐색 알고리즘)

- 알고리즘의 동작이 런타임에 동적으로 변경해야 할 때

 

장점

- 다양한 상황에 맞게 행동 방식을 쉽게 변경할 수 있다.

- 새로운 전략을 추가할 때, 기존의 코드를 수정할 필요가 없다.

   (새로운 전략 클래스를 작성하고 Context에서 사용하도록 설정하기만 하면 됨)

 

단점

- 간단한 알고리즘의 경우, 전략 패턴을 적용하면 오히려 코드가 복잡해질 수 있다. (over engineering)

- 클라이언트가 직접 전략을 설정해야 하므로, 클라이언트 코드가 복잡해질 수 있다.

 

 

3) 구조

 

 

 

 

4) 코드

// 전략(추상화된 알고리즘)
interface Strategy {
    void doSomething();
}

// 전략 알고리즘 A
class ConcreteStrateyA implements IStrategy {
    public void doSomething() {}
}

// 전략 알고리즘 B
class ConcreteStrateyB implements IStrategy {
    public void doSomething() {}
}

 

// 컨텍스트(전략 등록/실행)
class Context {
    Strategy strategy;
	
    // 전략 교체 메소드
    void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
	
    // 전략 실행 메소드
    void doSomething() {
        this.strategy.doSomething();
    }
}

 

// Strategy interface
interface SortingStrategy {
    void sort(int[] array);
}

// Concrete strategy: Bubble sort
class BubbleSort implements SortingStrategy {
    public void sort(int[] array) {

    }
}

// Concrete strategy: Quick sort
class QuickSort implements SortingStrategy {
    public void sort(int[] array) {

    }
}

// Context
class SortContext {
    private SortingStrategy strategy;

    public void setStrategy(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void sortArray(int[] array) {
        strategy.sort(array);
    }
}

// Client code
public class Main {
    public static void main(String[] args) {
        SortContext context = new SortContext();

        int[] array = {5, 2, 9, 1, 5, 6};

        // Bubble sort
        context.setStrategy(new BubbleSort());
        context.sortArray(array);
        System.out.println("Bubble Sorted: " + Arrays.toString(array));

        // Quick sort
        int[] array2 = {3, 7, 8, 5, 2, 1, 9, 5};
        context.setStrategy(new QuickSort());
        context.sortArray(array2);
        System.out.println("Quick Sorted: " + Arrays.toString(array2));
    }
}

 


 

4. 전략 패턴 vs. 템플릿 메소드 패턴

두 패턴 모두 "기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록" 하는 OCP 원칙을 잘 지켜내는 디자인 패턴이다.

 

  전략 패턴 템플릿 메소드 패턴
확장 방식 합성(compositon)
- 필드에서 클래스의 인스턴스를 참조
상속(inheritance)
- 하위 추상 메소드 변경
모듈 간의 결합도 낮음 높음
- 템플릿에 종속됨
상위 모듈 interface abstract class
종속성 새로운 전략 알고리즘으로 변경 가능 알고리즘의 일부만 변경되고,
나머지는 템플릿에 종속되어 변경되지 않음.

 

 


 

5. 정리

동적으로 전략을 수정해서 적용하는 것이 아니기도 하고, 
추상화를 사용해 update / recreate 2가지 방식에 대한 기본적인 골격을 잡기 위해, 임시로 적용하는 템플릿을 만들고자 했다. 따라서 템플릿 메소드 패턴이 더 맞을 것 같다고 생각했고 이를 적용하게 되었다.

 

사용한 템플릿 메소드 패턴에 대해 간략히 설명해보자면

공통으로 사용하는 메소드(템플릿 메소드)에서 구현 메소드를 실행하고 실행 시간을 측정하는 기능이라고 생각하고, runTest()를 정의했다.

그리고 하위 클래스에서 DB update / recreate 방식에 맞추어,  execute를 구현하는 방식으로 접근했다.

 

public abstract class AreaService {

    public void runTest(-) {
        long startTime = System.currentTimeMillis();

        // 실제로 구현할 메소드 호출
        execute(-);

        long endTime = System.currentTimeMillis();
        System.out.println(getMethodName() + (endTime - startTime) + "ms");
    }

    public abstract void execute(-);
    
}

 

@Component
@RequiredArgsConstructor
public class BeopjeongdongWorker {
    // @RequiredArgsConstructor와 @Quailfier 같이 사용 시, @Quailfier 어노테이션을 인식하지 못하고 여러 개의 Bean을 조회하는 에러가 발생함.
    // -> 변수 이름을 해당 클래스명으로 명시적으로 적어주면 해당 Bean을 자동으로 주입함.
    private final AreaService updateAreaService;
}

 

 


 

참고문헌

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%85%9C%ED%94%8C%EB%A6%BF-%EB%A9%94%EC%86%8C%EB%93%9CTemplate-Method-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90


https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%A0%84%EB%9E%B5Strategy-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90