인프런 스프링 - 고급편 강의 내용을 참고하여 정리한 내용입니다.
GOF의 디자인 패턴 중 하나인 템플릿 메서드 패턴을 소개한다.
템플릿 메서드 패턴은 이름에서도 알 수 있듯이 고정된 어떤 형식을 갖춘 메서드를 여러 클래스에서 재사용하는 패턴이라고 예측해볼 수 있다.
우리가 기본적으로 설계를 잘하기 위해서는 변하는 부분
과 변하지 않는 부분
을 잘 구분짓는 것이 중요하다. 보통은 변하는 부분을 기준으로 객체에 책임을 부여하고 클래스 단위로 구분지을 확률이 높다. 변하지 않는 부분은 여러 객체가 공통적으로 사용할 확률이 높고, 중복된 코드로 이어질 가능성이 크다.
이 때, 변하지 않는 공통 부분에 대해서 추상 클래스(abstract class)로 모듈화하고, 변하는 부분에 대해서는 다형성을 적용하는 방식을 템플릿 메서드 패턴
이라고 한다.
조금 더 풀어서 말하면, 부모 클래스인 추상 클래스에 알고리즘의 주 골격인 템플릿을 정의하고, 일부 변경되는 로직의 구현은 자식에게 위임하여 정의하는 것이다. 이렇게 하면, 자식 클래스가 알고리즘 전체 구조를 변경하지 않고, 특정 부분만 재정의 할 수 있다.
로그 추적기를 통해 로그를 찍는 부분을 추상 클래스로 모듈화해보자.
// AbstractTemplate
abstract class AbstractTemplate<T>(
private val trace: LogTrace
) {
fun execute(message: String): T {
val status = trace.begin(message)
try {
val result = call()
trace.end(status)
return result
} catch (e: Exception) {
trace.exception(status, e)
throw e
}
}
protected abstract fun call(): T
}
핵심 기능으로 자식클래스에서 재정의할 부분은 call() 추상메서드이고 이를 제외한 나머지 로그를 찍는 부분은 템플릿화하여 공통으로 사용한다.
이때, AbstractTemplate 클래스를 상속하는 자식클래스를 만들 수도 있지만, 익명 내부 클래스를 사용하면, 자식클래스를 따로 정의하지 않고 바로 인스턴스를 생성함과 동시에 클래스를 정의할 수 있다.
@RestController
class OrderControllerV4(
private val orderService: OrderServiceV4,
private val trace: LogTrace,
) {
@GetMapping("/v4/request")
fun request(itemId: String): String {
val template = object: AbstractTemplate<String>(trace) {
protected override fun call(): String {
orderService.orderItem(itemId)
return "ok"
}
}
return template.execute("OrderController.request()")
}
}
@Service
class OrderServiceV4(
private val orderRepository: OrderRepositoryV4,
private val trace: LogTrace,
) {
fun orderItem(itemId: String) {
val template = object: AbstractTemplate<Unit>(trace) {
protected override fun call() {
orderRepository.save(itemId)
}
}
template.execute("OrderService.orderItem()")
}
}
이러한 단점으로 인해 템플릿 메서드 패턴보다, 전략패턴을 사용하면 상속의 단점을 개선하면서 코드 중복을 줄이고 좀 더 유연한 설계를 가져갈 수 있다.