Post

Template Method Pattern

템플릿 메소드 패턴 (Template Method Pattern)

이름에서 알 수 있듯이 상위 클래스에서 템플릿 메서드로 알고리즘의 구조를 정의하고 그 중 일부 단계의 알고리즘은 서브 클래스에서 구현을 위임하는 패턴이다.

공통되거나 변경이 없는 로직은 슈퍼 클래스에서 구현하고 변경이 필요한 로직은 서브 클래스에서 구현한다.

추가적으로 Hook 메서드란 것도 있는데 이는 서브 클래스에서 선택적으로 오버라이딩할 수 있는 기본 구현을 가진 메서드이다.

패턴 구조

  1. AbstractClass : 상위 클래스로 알고리즘 구조를 정의하되 일부 단계는 하위 클래스가 구현하도록 한다.
  2. ConcreteClass: AbstractClass를 상속받아 구현한 클래스로 템플릿 메소드를 구현한다.

패턴 적용 전

예를 들어 커피와 차를 만드는 코드를 작성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class Coffee {
  fun prepare(addCondiments: Boolean) {
    boilWater()
    brewCoffee()
    pourInCup()
    if (addCondiments)
      addSugarAndMilk()
  }

  private fun boilWater() {
    println("물을 끓입니다.")
  }

  private fun brewCoffee() {
    println("커피를 우려냅니다.")
  }

  private fun pourInCup() {
    println("컵에 따릅니다.")
  }

  private fun addSugarAndMilk() {
    println("설탕과 우유를 추가합니다.")
  }
}

class Tea {
  fun prepare(addCondiments: Boolean) {
    boilWater()
    brewTea()
    pourInCup()
    if (addCondiments)
      addLemon()
  }

  private fun boilWater() {
    println("물을 끓입니다.")
  }

  private fun brewTea() {
    println("차를 우려냅니다.")
  }

  private fun pourInCup() {
    println("컵에 따릅니다.")
  }

  private fun addLemon() {
    println("레몬을 추가합니다.")
  }
}

fun main(){
  val coffee = Coffee()
  val tea = Tea()

  coffee.prepare(true)
  tea.prepare(false)
}

이 코드의 문제점은 코드가 중복되는 점이 많아 공통 로직을 변경할 때마다 여러 클래스를 반복적으로 수정해야한다는 것이다.

또 새로운 음료가 추가될 때마다 기존 코드를 복사하고 수정해야한다.

이러한 문제는 템플릿 메서드 패턴을 적용해 해결할 수 있다.

패턴 적용 후

먼저 음료를 만드는 공통 클래스를 만든다.

prepare메서드는 템플릿 메서드로 기본적인 알고리즘 뼈대가 구현되어 있고 brewaddCondiments는 추상 메서드로 선언된다.

brew와 addCondiments는 서브 클래스에서 구현해야하는 메서드인 것이다. 이외 boilWater와 pourInCup은 공통 메서드로 추상클래스에서 기본 구현된다.

customerWantsCondiments는 훅 메서드로 서브 클래스에서 선택적으로 오버라이딩할 수 있는 메서드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  abstract class Beverage {
    // 템플릿 메서드
    fun prepare() {
      boilWater()
      brew()
      pourInCup()
      if (addCondiments())
        addCondiments()
    }

    abstract fun brew()
    abstract fun addCondiments()
  
    private fun boilWater() {
      println("물을 끓입니다.")
    }

    private fun pourInCup() {
      println("컵에 따릅니다.")
    }
        
    // Hook 메서드
    open fun customerWantsCondiments(): Boolean = true
  }

이 후 Concrete Class인 Coffee와 Tea를 구현해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  class Coffee : Beverage() {
    override fun brew() { // 추상 메서드 구현
      println("커피를 우려냅니다.")
    }

    override fun addCondiments() { // 추상 메서드 구현
      println("설탕과 우유를 추가합니다.")
    }
  }

  class Tea : Beverage() {
    override fun brew() { // 추상 메서드 구현
      println("차를 우려냅니다.")
    }

    override fun addCondiments() { // 추상 메서드 구현
      println("레몬을 추가합니다.")
    }

    override fun customerWantsCondiments(): Boolean = false // Hook 메서드 오버라이딩
  }

  fun main() {
    val coffee = Coffee()
    val tea = Tea()

    coffee.prepare()
    tea.prepare() 
  }

장단점

장점

  • 공통 로직을 상위 클래스에 정의하기 때문에 중복을 줄이고 재사용성을 높인다.
  • 기존 코드를 수정하지 않고도 새로운 구현을 추가할 수 있다.
  • 제어의 역전의 원리를 사용하여 확장에 용이하다.

단점

  • 상속을 사용하기 때문에 상위 클래스의 변경이 하위 클래스에 영향을 미칠 수 있다.

템플릿 콜백 패턴

템플릿 콜백 패턴은 템플릿 메서드 패턴의 변형으로 상속 대신 콜백 메서드를 사용하는 패턴이다.

다음은 간단한 예시이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 콜백 인터페이스
interface Callback {
    fun doSomething()
}

// 템플릿 클래스
class Template {
    fun execute(callback: Callback) {
        println("시작")
        callback.doSomething()
        println("종료")
    }
}

// 사용 예
fun main() {
    val template = Template()
    
    // 익명 객체를 사용한 콜백
    template.execute(object : Callback {
        override fun doSomething() {
            println("콜백 실행")
        }
    })
    
    // 람다를 사용한 더 간결한 방식
    template.execute {
        println("람다로 콜백 실행")
    }
}

템플릿 콜백 패턴은 템플릿 메서드 패턴과 달리 상속을 사용하지 않기 때문에 상속의 단점을 피할 수 있다.

This post is licensed under CC BY 4.0 by the author.