템플릿 메서드 패턴 예제 1 - 커피, 차 만들기

리팩토링 전

public class Beverage {
    public void prepareRecipe(String kind) {
        System.out.println("물 끓이는 중");

        if (kind.equals("Coffee")) {
            System.out.println("필터를 통해서 커피를 우려내는 중");
        }
        if (kind.equals("Tea")) {
            System.out.println("차를 우려내는 중");
        }

        System.out.println("컵에 따르는 중");

        if (kind.equals("Coffee")) {
            System.out.println("설탕과 우유를 추가하는 중");
        }
        if (kind.equals("Tea")) {
            System.out.println("레몬을 추가하는 중");
        }
    }
}

음료의 종류에 따라 전체 로직이 비슷하고 일부분만 로직이 다르다고 가정하자. 이로 인해 if문을 통해 로직이 다른 부분을 분기 처리한 상황이다. 하지만 if문으로 처리하게 되면, 음료의 종류가 늘어날 때마다 코드가 복잡해지게 된다.

템플릿 메서드 패턴 적용

Step 1) 로직이 ‘다른 부분’과 ‘동일한 부분’을 각각 메서드로 추출

public class Beverage {
    public final void prepareRecipe(String kind) {
        boilWater();
        brew(kind);
        pourInCup();
        addCondiments(kind);
    }

    private void boilWater() {
        System.out.println("물 끓이는 중");
    }

    private void brew(String kind) {
        if (kind.equals("Coffee")) {
            System.out.println("필터를 통해서 커피를 우려내는 중");
        }
        if (kind.equals("Tea")) {
            System.out.println("차를 우려내는 중");
        }
    }

    private void pourInCup() {
        System.out.println("컵에 따르는 중");
    }

    private void addCondiments(String kind) {
        if (kind.equals("Coffee")) {
            System.out.println("설탕과 우유를 추가하는 중");
        }
        if (kind.equals("Tea")) {
            System.out.println("레몬을 추가하는 중");
        }
    }
}

Step2) ‘공통된 부분’과 ‘다른 부분’을 분리할 수 있도록 클래스 분리

공통된 로직이 있는 부분이 있어서 ‘인터페이스’를 사용하는 것보다 ‘상속’을 사용하는 것이 나을 것이다. 왜냐하면 ‘인터페이스’를 사용해서 부모 클래스를 만들어버리면 자식 클래스에 공통된 부분의 로직이 각각 중복해서 들어갈 것이기 때문이다.

따라서 ‘상속’을 사용해서 공통된 부분의 로직은 부모 클래스에서 구현을 하고, 다른 부분의 로직은 부모 클래스에서는 추상 메서드로 선언을 해놓고 자식 클래스에서 구현하도록 만들면 된다.

public abstract class Beverage {
		// 자식 클래스가 아무렇게나 로직을 수정하지 못하게 final로 선언
    public final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }

    private void boilWater() {
        System.out.println("물 끓이는 중");
    }
    abstract void brew();

    private void pourInCup() {
        System.out.println("컵에 따르는 중");
    }

    abstract void addCondiments();
}
  • 주의) 상속을 해서 자식 클래스가 마음대로 Override를 해서 로직을 수정하지 못하게 하려면 메서드에 final을 선언하거나, private을 선언해야 한다. (클라이언트가 사용하지 않는 메서드라면 private으로 선언해야하고, 이 때는 어차피 자식 클래스가 접근하지 못하므로 final을 굳이 선언할 필요는 없다. 그러나 클라이언트가 사용하는 메서드라서 어쩔 수 없이 public을 선언해야 한다면 자식 클래스에서 접근이 가능해지므로 자식 클래스에서 Override 하는 것을 막기 위해서 final을 선언하면 된다.)
public class Tea extends Beverage {
    @Override
    void brew() {
        System.out.println("차를 우려내는 중");
    }

    @Override
    void addCondiments() {
        System.out.println("레몬을 추가하는 중");
    }
}
public class Coffee extends Beverage {
    @Override
    void brew() {
        System.out.println("필터를 통해서 커피를 우려내는 중");
    }

    @Override
    void addCondiments() {
        System.out.println("설탕과 우유를 추가하는 중");
    }
}

참고) 전략패턴으로도 구현 가능

전략 패턴 적용

public class Beverage {
		**private BeverageStrategy beverageStrategy;

		public Beverage(BeverageStrategy beverageStrategy) {
				this.beverageStrategy = beverageStrategy;
		}**

    public void prepareRecipe(String kind) {
        System.out.println("물 끓이는 중");
				**beverageStrategy.brew();**
        System.out.println("컵에 따르는 중");
				**abeverateStrategy.addCondiments();**
    }

		public void hook() {	
		}
}
public interface BeverageStrategy {
    public void brew();

    public void addCondiments();
}
public class TeaStrategy implements BeverageStrategy {
    @Override
    public void brew() {
        System.out.println("차를 우려내는 중");
    }

    @Override
    public void addCondiments() {
        System.out.println("레몬을 추가하는 중");
    }
}
public class CoffeeStrategy implements BeverageStrategy {
    @Override
    public void brew() {
        System.out.println("필터를 통해서 커피를 우려내는 중");
    }

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

References