주야장천 쓰면서도 이름을 몰랐네, 템플릿 메서드 패턴

들어가며

코드를 작성하고 한 번 쭉 읽어보면 뿌듯할 때가 있다.

보통은 할 수 있는 한 가장 멋지게 설계했을 때 그런 느낌이 드는데, 지금 생각해보면 템플릿 메서드 패턴을 사용했을 때였던 것 같다.

 

템플릿 메서드 패턴

상위 클래스에서 처리의 뼈대를 결정하고 하위 클래스에서 그 구체적인 내용을 결정하는 패턴이다.

상위 클래스 쪽에 템플릿이 될 추상 메서드가 정의되어 있고, 하위 클래스에서 추상 메서드를 구현해 처리한다.

 

예시

AbstractDisplay, CharDisplay, StringDisplay, Main 4개의 클래스를 이용한다.

카테고리에 쓰는 글들은 모두 JAVA 언어로 배우는 디자인패턴 입문을 참고했다.

 

1. AbstractDisplay.java

public abstract class AbstractDisplay {
    public abstract void open();
    public abstract void print();
    public abstract void close();
    
    public final void display() { // 템플릿 메서드
        open();
        for (int i = 0; i < 5; i++) {
            print();
        }
        close();
    }
}

 

2. CharDisplay.java

public class CharDisplay extends AbstractDisplay {
	private char ch;
    
    public CharDisplay(char ch) {
    	this.ch = ch;
    }
    
    @Override
    public void open() {
    	System.out.priintln("<<");
    }
    
    @Override
    public void print() {
    	System.out.print(ch);
    }
    
    @Override
    public void close() {
    	System.out.println(">>");
    }
}

 

3. StringDisplay.java

public class StringDisplay extends AbstractDisplay {
	private String string;
    private int width;
    
    public StringDisplay(String string) {
    	this.string = string;
        this.width = string.length();
    }
    
    @Override
    public void open() {
    	printLine();
    }
    
    @Override
    public void print() {
    	System.out.print("|" + string + "|");
    }
    
    @Override
    public void close() {
    	printLine();
    }
    
    public void printLine() {
    	System.out.print("+");
        for (int i = 0; i < width; i++) {
        	System.out.print("-");
        }
        System.out.print("+");
    }
}

 

4. Main.java

public class Main {
	public static void main(String[] args) {
    	AbstractDisplay d1 = new CharDisplay("H"); // up-casting
        AbstractDisplay d2 = new StringDisplay("heather-dev");
        
        d1.display();
        d2.display();
    }
}

 

5. Main.java 실행 결과

<<HHHHH>>
+-----------+
|heather-dev|
|heather-dev|
|heather-dev|
|heather-dev|
|heather-dev|
+-----------+

 

장점, 로직 공통화

상위 클래스의 템플릿 메소드에 알고리즘이 기술되어 있어, 하위 클래스에는 알고리즘을 일일이 기술할 필요가 없는 것이 장점이다.

 

왜 템플릿 메소드 패턴일까?

위 예시에서는 AbstractDisplay.java 의 display() 가 '템플릿 메서드'다.

말 그대로 템플릿, 즉 틀이 되는 메서드라는 얘기다.

어떤 틀이 되는 건지, 템플릿 메서드의 의미가 뭔지 조금 더 자세하게 알아보자.

 

AbstractDisplay d1 = new CharDisplay("H");
AbstractDisplay d2 = new StringDisplay("heather-dev");

 

위 코드에서 d1과 d2 는 둘 다 AbstractDisplay 타입의 레퍼런스 변수지만,

실제로 d1 이 가리키는 인스턴스는 CharDisplay 타입이고 d2 이 가리키는 인스턴스는 StringDisplay 타입이다.

 

CharDisplay 와 StringDisplay 타입의 인스턴스를 AbstractDisplay 타입의 레퍼런스 변수로 받을 수 있는 이유는

자식 클래스의 객체가 부모 클래스 타입으로 형변환 가능하기 때문이고, 이를 업캐스팅이라고 한다.

 

하지만 AbstractDisplay 타입의 레퍼런스 변수로 받은 이상, d1, d2 로는 AbstractDisplay 에 선언된 메서드만 사용 가능하다.

StringDisplay 에만 있는 printLine() 메서드를 사용하려면 StringDisplay 타입으로 d2 를 명시적 형 변환해줘야 가능하다.

 

그래서 d1, d2 가 AbstractDisplay 타입이라는 점이 중요하다.

AbstractDisplay 에는 open(), print(), close(), display() 라는 메서드가 있기 때문에 d1, d2 도 네 가지 메서드를 호출할 수 있다.

여기에서는 open, print, close 를 차례로 호출하지 않고 display() 호출만으로 목표를 달성할 수 있다는 것이 핵심이다.

 

display() 내부에서는 open, print, close 를 차례로 호출한다.

display 기능을 위해 display() 메서드만 호출하면 open, print, close 를 굳이 따로 호출하지 않아도 된다는 의미다.

그래서 display() 가 틀이다.

 

AbstractDisplay 를 상속받은 클래스에서 추상메서드인 open, print, close 를 구현하기만 하면,

부모 클래스인 AbstractDisplay 타입의 레퍼러스 변수로도 얼마든지 display 기능을 사용할 수 있기 때문이다.

 

적용

원래는 Affiliate System 에 프로모션 기능을 붙이는 과정을 예로 들었다.

하지만 이 글 발행 후 생각해보니 해당 기능 구현 과정에서 사용한 설계가 미흡했고,

템플릿 메소드 패턴이 아니라 객체지향의 상속 개념을 이용한 것에 가깝다는 결론에 도달했다.

 

그래서 적용 파트에 적었던 예시는 다음 글에서 다시 적어보려고 한다.

 

마치며

템플릿 메서드 패턴과 관련 있는 패턴이 팩토리 메서드 패턴이라고 하는데, 그건 또 어떤 패턴일지 기대된다.

앞으로도 템플릿 메서드 패턴을 사용하면서 확장에 용이한 코드들을 작성하는 멋진 프로그래머가 되고 싶다.