템플릿 메서드(Template Method) 패턴 알아보기

청포도봉봉이·2024년 4월 4일
3

디자인 패턴

목록 보기
1/2

템플릿 메서드 는 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 오버라이드(재정의)할 수 있도록 하는 행동 디자인 패턴입니다.

즉, 변하지 않는 기능(템플릿)은 상위 클래스에 만들어두고 자주 변경되며 확장할 기능은 하위 클래스에서 만들도록 하여, 상위의 메소드 실행 동작 순서는 고정하면서 세부 실행 내용은 다양화 될 수 있는 경우에 사용됩니다.

구조

  1. 추상 클래스는 알고리즘의 단계들의 역할을 하는 메서드들을 선언하며, 이러한 메서드를 특정 순서로 호출하는 실제 템플릿 메서드도 선언합니다. 단계들은 abstract로 선언되거나 일부 디폴트 구현을 갖습니다.

  2. 구체 클래스들은 모든 단계들을 오버라이드할 수 있지만 템플릿 메서드 자체는 오버라이드 할 수 없습니다.

디자인 패턴에서의 템플릿은 변하지 않는 것을 의미한다.

예시

만약 축구와 야구를 자바 코드로 옮기고 그거에 맞는 행동을 정의하게 된다면 아래와 같을 겁니다.

class BasketBall {
    public void run() {
        System.out.println("달리다!");
    }

    public void defense() {
        System.out.println("수비하다!");
    }

    public void shoot() {
        System.out.println("손으로 슈팅하다!");
    }

    public void pass() {
        System.out.println("손으로 패스하다!");
    }
}
class Soccer {
    public void run() {
        System.out.println("달리다!");
    }

    public void defense() {
        System.out.println("수비하다!");
    }

    public void shoot() {
        System.out.println("발로 슈팅하다!");
    }

    public void pass() {
        System.out.println("발로 패스하다!");
    }
}

위 코드를 살펴보면 run( ), defense( ) 메서드의 행위가 동일하고 shoot( ), pass( )는 다른 것을 알 수 있습니다.

만약 스포츠 종목이 1,000개가 늘어난다고 하면 행위가 같은 메서드들을 불필요하게 작성해줘야 하고 그것을 유지보수 해줘야 할 것입니다.

그렇다면 공통화할 수 있는 메서드를 합쳐주고 행위가 다른 메서드는 따로 행위를 정의하는 것이 더 효율적이지 않을까요?

abstract class Sports {
    public void run() {
        System.out.println("달리다!");
    }

    public void defense() {
        System.out.println("수비하다!");
    }

    public abstract void shoot();

    public abstract void pass();
}

Sports 라는 추상 클래스를 만들어주고 행위가 달라야할 메서드는 추상 메서드로 선언해주었습니다.

class BasketBall extends Sports {
    @Override
    public void shoot() {
        System.out.println("손으로 패스하다!");
    }

    @Override
    public void pass() {
        System.out.println("발로 패스하다!");
    }
}

class Soccer extends Sports {

    @Override
    public void shoot() {
        System.out.println("발로 슈팅하다!");
    }

    @Override
    public void pass() {
        System.out.println("발로 패스하다!");
    }
}

이렇게 코드를 작성하면 행위가 달라져야 하는 메서드만 재정의해주면 됩니다.

템플릿 메서드 패턴 특징

사용 시기

템플릿 메서드 패턴은 클라이언트들이 알고리즘의 특정 단계들만 확장할 수 있도록 하고 싶을 때, 그러나 전체 알고리즘이나 알고리즘 구조는 확장하지 못하도록 하려고 할 때 사용하면 됩니다.

장점

  • 클라이언트가 대규모 알고리즘의 특정 부분만 재정의하도록 하여, 알고리즘의 다른 부분에 발생하는 변경 사항의 영향을 덜 받도록 합니다.
  • 상위 추상클래스로 로직을 공통화 하여 코드의 중복을 줄일 수 있습니다.
  • 서브 클래스의 역할을 줄이고, 핵심 로직을 상위 클래스에서 관리하므로서 관리가 용이해집니다
    • 헐리우드 원칙 (Hollywood Principle) : 고수준 구성요소에서 저수준을 다루는 원칙 (추상화에 의존)

단점

  • 일부 클라이언트들은 알고리즘의 제공된 골격에 의해 제한될 수 있습니다.
  • 당신은 자식 클래스를 통해 디폴트 단계 구현을 억제하여 리스코프 치환 원칙을 위반할 수 있습니다.
  • 템플릿 메서드들은 단계들이 더 많을수록 유지가 더 어려운 경향이 있습니다.

훅(hook) 메서드

훅(hook) 메소드부모의 템플릿 메서드의 영향이나 순서를 제어하고 싶을때 사용되는 메서드 형태를 말합니다.

위의 그림에서 볼 수 있듯이 템플릿 메서드 내에 실행되는 동작을 step2( )라는 메서드의 참, 거짓 여부에 따라 다음 스텝을 어떻게 이어갈지 정한다. 이를 통해 자식 클래스에서 좀 더 유연하게 템플릿 메서드의 알고리즘을 로직을 다양화 할 수 있다는 특징이 있다.

훅 메소드는 추상 메소드가 아닌 일반 메소드로 구현하는데, 선택적으로 오버라이드 하여 자식 클래스에서 제어하거나 아니면 놔두거나 하기 위해서입니다.

훅(hook) 활용

class Coffee {
    public void makeBeverage() {
        boilWater();
        addEspresso();
        pourInCup();
        addSugarAndMilk();
    }

    public void boilWater() {
        System.out.println("물을 끓임");
    };
    public void addEspresso() {
        System.out.println("에스프레스 추가");
    };
    public void pourInCup() {
        System.out.println("컵에 따름");
    };

    public void addSugarAndMilk() {
        System.out.println("설탕과 우유를 넣음");
    };
}

class Tea {
    public void makeBeverage() {
        boilWater();
        addTeaBag();
        pourInCup();
    }

    public void boilWater() {
        System.out.println("물을 끓임");
    };
    public void addTeaBag() {
        System.out.println("티백을 넣음");
    };
    public void pourInCup() {
        System.out.println("컵에 따름");
    };
}

위 코드를 보면 배운 바와 같이 코드의 중복이 보이며 이를 템플릿 메서드 패턴을 이용해 묶어주려고 합니다.

위 코드에서 다른 부분을 표시하면 아래와 같습니다.

  1. boilWater( ), pourInCup( ) 메서드는 행위가 같으니 공통된 메서드로 만들어줍니다.

  2. addTeaBag( ), addEspresso( ) 메서드는 행위는 다르지만 공통된 메서드로 만들어 줄 수 있을거 같습니다. 그러므로 추상 메서드 1개로 만들어봅니다.

  3. addSugarAndMilk( ) 메서드는 훅(hook)을 활용하여 만들어주겠습니다.

abstract class Drink {
    public final void makeDrink() {
        boilWater();
        addMainItem();
        pourInCup();
        if (wantOtherItem()) {
            addOtherItem();
        }
    }


	protected abstract void addMainItem();
    protected abstract void addOtherItem();
    
    private void boilWater() {
        System.out.println("물을 끓임");
    };
    private void pourInCup() {
        System.out.println("컵에 따른다.");
    };
    protected boolean wantOtherItem() {
        return false;
    };

}

Drink라는 추상 클래스를 만들어줘서 설계한 대로
1.boilWater( ), pourInCup( ) -> 공통된 행위의 메서드를 정의해주었습니다.
2. addTeaBag( ), addEspresso( )는 자손(서브)클래스에서 재정의 해줄거므로 추상 메서드 addMainItem( )로 만들었습니다.
3. 훅을 이용하기 위해서 설탕과 커피를 넣는 행위는 추상 메서드 addSugarAndMilk( )로 만들어 주었고 위의 설명처럼 훅(hook) 메소드는 부모의 템플릿 메서드의 영향이나 순서를 제어하고 싶을때 사용되는 메서드wantOtherItem( )를 만들어주었습니다.

훅 메서드를 어떻게 사용하는지 아래 코드에서 확인해보겠습니다.

class Coffee extends Drink {

    @Override
    public void addMainItem() {
        System.out.println("에스프레소 추가");
    }

    @Override
    public void addOtherItem() {
        System.out.println("설탕과 우유를 추가");
    }

    @Override
    protected boolean wantOtherItem() {
        return true;
    }
}

Coffee 클래스는 addMainItem( ), addOtherItem( ) 을 재정의 해주었습니다.

그리고 wantOtherItem( ) 을 보면 부모(슈퍼)클래스에서의 리턴값 truefalse 바꾸었습니다. 왜나하면 Coffee 클래스에서 음료를 만들라는 명령을 할때 설탕과 우유를 추가하는 행위도 추가해야 되기 때문입니다.

addOtherItem( )이 실행된다면 메서드 행위도 재정의 해주었으므로 Coffee 클래스에 맞게 실행될 것입니다.

class Tea extends Drink {

    @Override
    public void addMainItem() {
        System.out.println("티백 추가");
    }

    @Override
    public void addOtherItem() {

    }
}

Tea 클래스는 addMainItem( )의 행위를 재정의하고 addOtherItem( )은 재정의할 필요가 없으므로 남겨두었습니다.

public class Client {
    public static void main(String[] args) {
        Drink drinkCoffee = new Coffee();
        drinkCoffee.makeDrink();

        Drink drinkTea = new Tea();
        drinkTea.makeDrink();
    }
}

클라이언트에서 음료를 제조한다면 결과는 아래와 같을 것 입니다.

Java에서 사용된 사례

자바에서 템플릿 메서드 패턴 적용 예시:

java.io.InputStream 클래스는 템플릿 메서드 패턴을 따릅니다. InputStream은 추상 클래스이며, read() 메서드는 템플릿 메서드입니다. 이 메서드는 데이터를 읽어 오는 과정을 정의하고 있습니다. 하위 클래스인 FileInputStream, ByteArrayInputStream 등에서는 read() 메서드의 일부 단계를 구현하고 있습니다.

public abstract class InputStream {
    public int read() throws IOException {
        // 템플릿 메서드
        // 1. 데이터를 읽어올 준비 작업
        // ...
        
        // 2. 추상 메서드 호출하여 실제 데이터 읽기
        int b = read0();
        
        // 3. 데이터 읽기 후 작업
        // ...
        return b;
    }
    
    // 하위 클래스에서 구현해야 하는 추상 메서드
    protected abstract int read0() throws IOException;
}

Spring 사용된 사례

Spring의 JdbcTemplate 클래스는 앞서 말씀드린 대로 템플릿 메서드 패턴을 따르고 있습니다. JdbcTemplate은 템플릿 메서드인 execute(), query(), update() 등을 제공하고 있습니다. 이 메서드들은 JDBC 코드 실행 과정의 골격을 정의하고 있으며, 개발자는 콜백 인터페이스를 구현하여 실제 코드 로직을 제공합니다.

public class JdbcTemplate extends JdbcAccessor {
    public <T> T execute(StatementCallback<T> action) throws DataAccessException {
        // 템플릿 메서드
        // 1. Connection 가져오기
        // 2. Statement 생성
        // 3. 콜백 실행 (action.doInStatement(stmt))
        // 4. Statement, Connection 닫기
        // ...
    }
    
    // 개발자가 구현해야 하는 콜백 인터페이스
    public interface StatementCallback<T> {
        T doInStatement(Statement stmt) throws SQLException, DataAccessException;
    }
}

이처럼 템플릿 메서드 패턴은 알고리즘의 구조를 정의하고, 일부 단계는 하위 클래스나 콜백으로 구현하도록 합니다. 이를 통해 코드 재사용성과 유지보수성이 높아집니다.

다른 패턴과의 관계

  1. 팩토리 메서드템플릿 메서드의 특수화라고 생각할 수 있습니다. 동시에 대규모 템플릿 메서드의 한 단계의 역할을 팩토리 메서드가 할 수 있습니다.

  2. 템플릿 메서드상속을 기반으로 합니다. 이 메서드는 자식 클래스들에서 알고리즘의 부분들을 확장하여 변경할 수 있도록 합니다.

  3. 전략 패턴은 합성을 기반으로 합니다. 전략 팬턴은 객체 행동의 일부분들을 이러한 행동에 해당하는 다양한 전략들을 제공하여 변경할 수 있습니다.

템플릿 메서드는 클래스 수준에서 작동하므로 정적입니다. 전략 패턴은 객체 수준에서 작동하므로 런타임에 행동들을 전환할 수 있도록 합니다.


참고
💠 템플릿 메소드(Template Method) 패턴 - 완벽 마스터하기
https://refactoring.guru/ko/design-patterns/template-method

profile
서버 백엔드 개발자

0개의 댓글