Template Method 패턴은 객체지향 설계에서 제공되는 행동형 패턴 중 하나로, 알고리즘의 구조를 정의하고, 그 세부 구현은 서브클래스에서 정의하는 방식입니다. 즉, 어떤 처리 과정의 전체 흐름을 정의해 두고, 그 세부적인 단계만을 자식 클래스에서 구체화하는 방식으로 동작합니다. 이 패턴은 주로 중복된 코드의 재사용성을 높이고, 알고리즘을 일정하게 유지하기 위해 활용됩니다.
Template Method 패턴의 주요 구성 요소
Template Method 패턴은 주로 두 가지 주요 요소로 나눌 수 있습니다:
추상 클래스(Abstract Class): 알고리즘의 기본 뼈대를 정의합니다. 알고리즘의 흐름을 설정하는 "템플릿 메소드"를 포함하며, 구체적인 단계는 서브클래스에서 구현하도록 되어 있습니다.
구체적인 클래스(Concrete Class): 추상 클래스에서 정의한 템플릿 메소드에 포함된 구체적인 알고리즘 단계를 구현합니다.
Template Method 패턴의 동작 원리
Template Method 패턴에서 가장 중요한 요소는 바로 템플릿 메소드입니다. 이 메소드는 알고리즘의 기본적인 흐름을 정의하고, 특정 단계의 실행을 서브클래스에 위임합니다. 예를 들어, 어떤 데이터를 처리하는 알고리즘에서 '데이터 읽기', '처리', '출력' 단계를 거친다고 할 때, 이 흐름을 템플릿 메소드에서 정의하고, 각 단계별 세부 사항은 서브클래스에서 구현하는 방식입니다.
예시
간단한 예시로, 커피를 만드는 알고리즘을 고려해봅시다. 커피를 만드는 과정은 비슷하지만, 커피의 종류에 따라 세부적인 과정이 달라질 수 있습니다.
abstract class CoffeeTemplate {
// 템플릿 메소드
public final void makeCoffee() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addCondiments();
}
// 기본 알고리즘 단계
public void boilWater() {
System.out.println("Boiling water");
}
public void brewCoffeeGrinds() {
System.out.println("Brewing coffee grounds");
}
public void pourInCup() {
System.out.println("Pouring coffee into cup");
}
// 자식 클래스에서 구현해야 할 부분
public abstract void addCondiments();
}
class Tea extends CoffeeTemplate {
@Override
public void addCondiments() {
System.out.println("Adding lemon to tea");
}
}
class Coffee extends CoffeeTemplate {
@Override
public void addCondiments() {
System.out.println("Adding sugar and milk to coffee");
}
}
위 예시에서 makeCoffee() 메소드는 커피를 만드는 기본 흐름을 정의합니다. addCondiments() 메소드는 추상 메소드로, 커피와 차에 따라 다른 양념을 추가하는 세부 구현을 서브클래스에서 담당합니다.
Template Method 패턴의 장점
코드 재사용성: 알고리즘의 구조는 상위 클래스에 정의되고, 세부 구현만 자식 클래스에서 수정하므로 코드 중복을 줄일 수 있습니다.
일관성 유지: 알고리즘의 구조가 고정되어 있기 때문에, 알고리즘의 흐름이 일관되게 유지됩니다.
유연성 제공: 알고리즘의 일부 단계만 수정할 수 있어 유연하게 확장할 수 있습니다.
Template Method 패턴의 단점
상속 의존성: 추상 클래스에 의존해야 하므로, 상속 관계가 복잡해질 수 있습니다.
세부 구현에 대한 제한: 기본적인 알고리즘은 상위 클래스에 정의되지만, 세부 구현에 대한 유연성이 떨어질 수 있습니다.
사용 시기
Template Method 패턴은 다음과 같은 상황에서 유용하게 사용됩니다:
알고리즘의 구조가 거의 비슷하고, 일부 단계만 달라지는 경우.
코드의 재사용성을 높이고, 알고리즘 흐름을 일관성 있게 유지하고자 할 때.
여러 서브클래스에서 공통된 알고리즘 흐름을 따르되, 세부적인 구현만 달리 하고자 할 때.
결론
Template Method 패턴은 알고리즘의 구조를 상위 클래스에서 정의하고, 세부적인 구현을 자식 클래스에서 다르게 구현할 수 있도록 함으로써 코드의 중복을 줄이고, 알고리즘 흐름을 일관성 있게 유지할 수 있게 해줍니다. 이 패턴은 특히 알고리즘의 구조가 동일하지만, 세부 구현만 다른 경우에 매우 유용하게 사용됩니다.
public interface ValidationStrategy {
boolean execute(String s);
}
public class IsAllLowerCase implements ValidationStrategy {
@Override
public boolean execute(String s) {
return s.matches("[a-z]+");
}
}
public class IsNumeric implements ValidationStrategy {
@Override
public boolean execute(String s) {
return s.matches("\\d+");
}
}
public class Validator {
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v) {
this.strategy = v;
}
public boolean validate(String s){
return strategy.execute(s);
}
}
public class LegacyMain {
public static void main(String[] args) {
Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaa");
System.out.println(b1);
Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
boolean b2 = lowerCaseValidator.validate("hello");
System.out.println(b2);
}
}
Lambda로 리팩터링 예제
public class LambdaMain {
public static void main(String[] args) {
Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.validate("aaaaa");
System.out.println(b1);
Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");
System.out.println(b2);
}
}
ValidationStrategy 는 함수형 인터페이스며 Predicate과 같은 함수 디스크립터를 갖고 있음을 파악할 수 있음.
public class Kbank implements OnlineBanking{
@Override
public void makeCustomerHappy(Customer c) {
System.out.println(c.getName() + "케이뱅크 오버라이딩");
}
}
public class NH implements OnlineBanking{
@Override
public void makeCustomerHappy(Customer c) {
System.out.println(c.getName() + "농협 오버라이딩");
}
}
public class LagacyMain {
public static void main(String[] args) throws Exception {
OnlineBanking kBanking = new Kbank();
kBanking.processCustomer(1);
kBanking.processCustomer(2);
OnlineBanking nhBanking = new NH();
nhBanking.processCustomer(1);
nhBanking.processCustomer(2);
}
}
Lambda로 리팩터링 예제
public class OnlineBankingLambda {
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) throws Exception {
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
}
public class LambdaMain {
public static void main(String[] args) throws Exception {
new OnlineBankingLambda().processCustomer(1, (Customer c) -> {
System.out.println(c.getName() + "케이뱅크 오버라이딩");
});
new OnlineBankingLambda().processCustomer(2, (Customer c) -> {
System.out.println(c.getName() + "케이뱅크 오버라이딩");
});
new OnlineBankingLambda().processCustomer(1, (Customer c) -> {
System.out.println(c.getName() + "농협 오버라이딩");
});
new OnlineBankingLambda().processCustomer(2, (Customer c) -> {
System.out.println(c.getName() + "농협 오버라이딩");
});
}
}
public interface Observer {
void notify(String tweet);
}
public class Guardian implements Observer{
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("queen")) {
System.out.println("Yet more news from London..." + tweet);
}
}
}
public class LeMonde implements Observer{
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("wine")) {
System.out.println("Today cheese, wine and news!" + tweet);
}
}
}
public class NYTimes implements Observer {
@Override
public void notify(String tweet) {
if (tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY!" + tweet);
}
}
}
public class Feed implements Subject{
private final List<Observer> observers = new ArrayList<>();
@Override
public void registerObserver(Observer o) {
this.observers.add(o);
}
@Override
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}
public class LagacyMain {
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver(new NYTimes());
f.registerObserver(new Guardian());
f.registerObserver(new LeMonde());
f.notifyObservers("The queen said her favourite book is Modern Java in Action!");
}
}
Lambda로 리팩터링 예제
public class LambdaMain {
public static void main(String[] args) {
Feed f = new Feed();
f.registerObserver((String tweet) -> {
if (tweet != null && tweet.contains("money")) {
System.out.println("Breaking news in NY!" + tweet);
}
});
f.registerObserver((String tweet) -> {
if (tweet != null && tweet.contains("queen")) {
System.out.println("Yet more news from London..." + tweet);
}
});
f.registerObserver((String tweet) -> {
if (tweet != null && tweet.contains("wine")) {
System.out.println("Today cheese, wine and news!" + tweet);
}
});
f.notifyObservers("The queen said her favourite book is Modern Java in Action!");
}
}
public abstract class ProcessingObject<T> {
protected ProcessingObject<T> successor;
public void setSuccessor(ProcessingObject<T> successor) {
this.successor = successor;
}
public T handle(T input){
T r = handleWork(input);
if(successor != null){
return successor.handle(r);
}
return r;
}
abstract protected T handleWork(T input);
}
public class HeaderTextProcessing extends ProcessingObject<String> {
@Override
protected String handleWork(String text) {
return "From Raoul, Mario and Alan : "+ text;
}
}
public class SpellCheckerProcessing extends ProcessingObject<String>{
@Override
protected String handleWork(String text) {
return text.replaceAll("labda", "lambda");
}
}
public class LagacyMain {
public static void main(String[] args) {
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2); //두 작업 처리 객체를 연결
String result = p1.handle("Aren't labdas really sexy?!!");
System.out.println(result);
}
}
Lambda로 리팩터링 예제
public class LambdaMain {
public static void main(String[] args) {
UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!!");
System.out.println(result);
}
}
public class ProductFactory {
public static Product createProduct(String name){
return switch (name) {
case "loan" -> new Loan();
case "stock" -> new Stock();
case "bond" -> new Bond();
default -> throw new RuntimeException("No such product" + name);
};
}
}
public class LagacyMain {
public static void main(String[] args) {
Product p = ProductFactory.createProduct("loan");
System.out.println(p.getClass());
}
}
Lambda로 리팩터링 예제
public class LambdaProductFactory {
final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
map.put("loan", Loan::new);
map.put("stock", Stock::new);
map.put("bond", Bond::new);
}
public static Product createProduct(String name) {
Supplier<Product> p = map.get(name);
if (p != null) return p.get();
throw new IllegalArgumentException("No such Product " + name);
}
}
public class LambdaMain {
public static void main(String[] args) {
Product p = LambdaProductFactory.createProduct("loan");
System.out.println(p.getClass());
}
}