공부 흔적남기기

템플릿 메소드 패턴 | 전략 패턴 | 템플릿 콜백 패턴 본문

코테/배경지식

템플릿 메소드 패턴 | 전략 패턴 | 템플릿 콜백 패턴

65살까지 코딩 2023. 12. 5. 21:35
728x90
반응형

템플릿 메소드 패턴, 전략패턴, 템플릿 콜백 패턴 위 3가지의 디자인 패턴은 어떤 공통점을 가지고 있을까?

위 디자인 패턴들은 같은 목적을 가진 패턴들이다.
복잡한 코드속에서 반복되는 부분을 외부로 템플릿화(공통화?) 시켜 결합성을 낮추고 (단일 책임 원칙) 변하는 부분(알고리즘 군)을 구현해 템플릿을 통해 실행시키는 방식이다.

예를 들어 코드의 모든 곳에 다음과 같이 로그를 남기는 코드가 있다고 가정해보자

@Service
class ItemServiceDirtyCode(
    private val itemRepository: ItemRepositoryDirtyCode,
    private val logService: LogServiceDirtyCode
) {
    fun getItems(): List<Item> {
        var trace: Trace? = null

        return runCatching {
            trace = logService.begin("ItemRepositoryDirtyCode/getItems")
            itemRepository.findItems()
        }.onSuccess {
            logService.finish(trace!!)
        }.onFailure {
            logService.execption(trace!!)
            throw it
        }.getOrThrow()
    }

    fun createItem(item: ItemDTO): Item {
        var trace: Trace? = null

        return runCatching {
            trace = logService.begin("ItemRepositoryDirtyCode/createItem")
            itemRepository.saveItem(item.toItem())
        }.onSuccess {
            logService.finish(trace!!)
        }.onFailure {
            logService.execption(trace!!)
            throw it
        }.getOrThrow()
    }
}

위 코드는 로그를 남기기위해 로직을 감싸는 로그 코드가 반복적으로 존재한다.

 

이 보기 싫은 코드를 먼저 템플릿 메서드 패턴을 이용해 해결해보자.

abstract class LogHelperTemplateMethod<T>(
    private val logService: LogServiceDirtyCode
) {

    protected abstract fun call(): T

    fun execute(message: String): T {
        var trace: Trace? = null

        return runCatching {
            trace = logService.begin(message)
            call()
        }.onSuccess {
            logService.finish(trace!!)
        }.onFailure {
            logService.execption(trace!!)
            throw it
        }.getOrThrow()
    }
}

반복되는 로그 코드를 다음과 같이 추상클래스의 일반 함수(템플릿)로 만들고 구현 코드(알고리즘)를 추상 메소드로 만들어 구현체가 상속받아 구현함으로서  로그의 책임을 추상템플릿에 넘기고 call을 구현해 이용할 수 있다.

@Service
class ItemServiceTemplateMethod(
    private val itemRepository: ItemRepositoryTemplateMethod,
    private val logService: LogServiceDirtyCode
) {
    fun getItems(): List<Item> {

        return object : LogHelperTemplateMethod<List<Item>>(logService) {
            override fun call(): List<Item> {
                return itemRepository.findItems()

            }
        }.execute("ItemRepositoryTemplateMethod/findItems")
    }

    fun createItem(item: ItemDTO): Item {
        return object : LogHelperTemplateMethod<Item>(logService) {
            override fun call(): Item {
                return itemRepository.saveItem(item.toItem())
            }
        }.execute("ItemServiceDirtyCode/createItem")
    }
}

다음과 같이 구현 가능하다. 훨씬 간결해졌다!

하지만 상속을 사용해서 생긴 문제점들이 있다. 
1. 상위 클래스와 하위 클래스는 전혀 연관이 없다. 
2. 상위 클래스가 변경되면 하위 모든 클래스를 변경해줘야한다. (상속보단 위임)

 

위 문제를 해결한 방법이 전략 패턴이다. 흔히 스프링 빈을 자동 주입할때 사용하는 방식이다.

fun interface LogCallStrategy<T> {

    fun call() : T
}

class LogHelperStrategy<T>(
    private val logService: LogServiceDirtyCode,
    private val logCallStrategy: LogCallStrategy<T>,
) {


    fun execute(message: String): T {
        var trace: Trace? = null
        return runCatching {
            trace = logService.begin(message)
            logCallStrategy.call()
        }.onSuccess {
            logService.finish(trace!!)
        }.onFailure {
            logService.execption(trace!!)
            throw it
        }.getOrThrow()
    }
}

logCallStrategy를 필드로 받아 상속대신 인터페이스를 사용해 위임(조립)을 사용하는 방식이다.  상속의 문제를 깔끔히 없앴다. 이 역시 변하지 않는 부분을 템플릿화 하고 변하는 부분을 조립해 인자로 받아서 사용한다.

@Service
class ItemServiceStrategy(
    private val itemRepository: ItemRepositoryStrategy,
    private val logService: LogServiceDirtyCode
) {
    fun getItems(): List<Item> {
        return LogHelperStrategy(logService) {
            itemRepository.findItems()
        }.execute("ItemRepositoryTemplateMethod/findItems")
    }

    fun createItem(item: ItemDTO): Item {
        return LogHelperStrategy(logService) {
            itemRepository.saveItem(item.toItem())
        }.execute("ItemServiceDirtyCode/createItem")
    }
}

fun interface이기 때문에 람다를 사용할 수 있어 더 깔끔해졌다. 이 전략 패턴은 템플릿 클래스가 생성되는 시점에 전략이 들어오기 때문에 전략(알고리즘)을 쉽게 바꿀수 없는 문제점이있다( setter로 바꾸더라도 heap에 저장되기 때문에 동시성 이슈가 생길 수 있음) 

이 문제를 해결하는 방식이 템플릿 콜백 패턴이다.  

fun interface LogCallCallBack<T> {

    fun call() : T
}

@Component
class LogHelperCallback(
    private val logService: LogServiceDirtyCode,
) {

    fun <T> execute(message: String, logCallCallback: LogCallCallBack<T>): T {
        var trace: Trace? = null
        return runCatching {
            trace = logService.begin(message)
            logCallCallback.call()
        }.onSuccess {
            logService.finish(trace!!)
        }.onFailure {
            logService.execption(trace!!)
            throw it
        }.getOrThrow()
    }
}

알고리즘을 함수의 인자로 받음으로서 다양한 전략(알고리즘) 구현체를 사용할 수 있으며 스프링빈으로 등록하여 사용하기도 편하다. 우리가 아는 JdbcTemplate, reidsTemplate, mongoTemplate 등등 이 다음과 같은 방식으로 구현되어 있다. 

@Service
class ItemServiceTemplateCallback(
    private val itemRepository: ItemRepositoryTemplateCallback,
    private val logHelperCallback: LogHelperCallback
) {
    fun getItems(): List<Item> {
        return logHelperCallback.execute("ItemRepositoryTemplateMethod/findItems") {
            itemRepository.findItems()
        }
    }

    fun createItem(item: ItemDTO): Item {
        return logHelperCallback.execute("ItemServiceDirtyCode/createItem") {
            itemRepository.saveItem(item.toItem())
        }
    }
}

이전과 비교해서 깔끔해졌다. 하지만 그럼에도 불구하고 모든 코드에 다음과 같이 코드를 붙여야 하는 단점이 있다. 이를 해결하기위해서 프록시패턴, 데코레이터 패턴이 사용되는데 다음 글에서 살펴보자.

728x90
반응형

'코테 > 배경지식' 카테고리의 다른 글

Clean Code 1장 복기  (0) 2022.10.04