Topic = 템플릿 메서드 패턴의 의도와 Kotlin 적용 예제 학습해보기
🔑Purpose
- 변하는 것과 변하지 않는 것을 분리
- 반복되는 공통 로직을 템플릿이라는 변하지 않는 뼈대로 정의해두고, 변하는 부분을 별도의 서브클래스로 정의하여 분리한다.
예시로 비교해보면서 학습하는 탬플릿 메서드 패턴 개념
fun main {
fun algorithmA(){
val startTime = System.currentTimeMillis()
// 알고리즘 풀이
val endTime = System.currentTimeMillis()
val resultTime = endtime - startTime
}
fun algorithmB(){
val startTime = System.currentTimeMillis()
// 알고리즘 풀이
val endTime = System.currentTimeMillis()
val resultTime = endtime - startTime
}
... C
... D
... F
}
알고리즘 풀이 시간을 확인하기 위해서 위 와같이 알고리즘 함수 내부에 resultTime을 얻는 로직을 추가했더니 다음과 같은 문제가 발생했다.
단일 책임 원칙 위반
- 알고리즘 함수 내에서 시간 측정과 알고리즘 실행 두 가지 책임을 가지게 된다.
유지보수가 어렵다
- 모든 알고리즘에 동일한 코드가 중복될 뿐더러, 시간 측정 로직이 변경될 경우 모든 알고리즘 함수에서 동일한 코드를 수정해야 한다.
🚨위 시간 측정 로직과 같이 변하는 부분을 사이에 두고 작동하는 코드는 별도의 함수로 빼서 사용하기가 어렵다. 이러한 문제를 Template Method 패턴은 추상 클래스( AbstractClass )와 , 구현 클래스 ( ConcreteClass ) 를 통해 해결한다.
AbstractClass
- 변동되지 않는 로직인 템플릿 메서드를 구현하며, 그 탬플릿 메서드 내부에서 사용할 추상 메서드를 선언한다. 선언한 추상 메서드는 하위 클래스 ConcreteClass에서 구현된다.
ConcreteClass
- AbstractClass에 정의된 추상 메서드를 구체적으로 구현하며, 이 추상 메서드는 템플렛 메서드에서 호출된다.
[ 적용 예시 ]
[ AbstractClass ]
private val logger = KotlinLogging.logger { }
abstract class AbstractTemplate {
fun execute(){
val startTime = System.currentTimeMillis()
call()
val endTime = System.currentTimeMillis()
logger.info { "result time = ${endTime - startTime}" }
}
protected abstract fun call()
}
- Template method 의 부모 역할을 하는 abstract class를 정의한다. 추상 메서드 call() 을 제외한 execute() 내부 로직들은 해당 추상 템플릿을 상속 받아 추상 메서드를 정의한 객체가 별도의 호출 로직 없이 사용할 수 있으며, 추상 템플릿을 상속하지만, 템플릿 메서드를 사용하지 않길 바라는 로직은 override를 통해서 템플릿을 사용하지 않을 수 있다.
[ ConcreteClass ]
private val logger = KotlinLogging.logger { }
class SubClassLogic1 : AbstractTemplate() {
override fun call (){
logger.info { "비즈니스 로직 1 실행" }
}
}
- AbstractClass로 정의된 추상 메서드( 변동되는 로직 )를 구체적으로 구현한다.
- 추상 템플릿 메서드는, 상속을 사용하기 때문에 템플릿 메서드를 사용하는 객체마다 클래스로 선언하여 사용하여야 한다. 때문에 여러 클래스 파일이 생성하여 프로젝트 복잡도가 높아지는데, 익명 클래스를 통해 이 문제를 해결할 수 있다.
[ ConcreteClass 익명 클래스 ]
private val logger = KotlinLogging.logger { }
fun main {
fun templateMethod(){
val logic1 = object : AbstractTemplate() {
override fun call() {
logger.info { "비즈니스 로직 1 실행" }
}
}
logic1.execute()
}
}
- client 단에서 객체를 사용할 때, 익명 클래스로 선언할 수 있다.
📊탬플릿 메서드의 장점과 한계점 및 대안
🟩 장점
- 부모 클래스에 알고리즘의 골격인 템플릿을 정의하고, 일부 변경되는 로직은 자식 클래스에 정의함으로 인해 공통 로직 분리가 가능해졌다.
🟥 단점
- 상속을 사용하므로, 상속으로 인해 생기는 단점이 존재한다. 부모 클래스의 기능인 템플릿 메서드는 하위 클래스에서 사용하지 않는다. 이는 LSP 위반으로 부모 클래스의 로직이 필요하지 않은 상속을 강제하는 문제다.
🏗️ 대안1. Default Method Interface
추상 템플릿 메서드의 상속으로 인한 단점으로 Interface의 Default method 를 통해 해당 문제를 해결할 수 있다고 한다. 하지만, Kotlin의 경우 Interface에서 Default Method 를 사용할 때 AOP가 원하는대로 동작하지 않는 이슈가 발생한다고 한다.
Kotlin Default Method Interface 부분은 추가로 실습해보고 정리 할 예정...
🏗️ 대안2. Strategy Pattern
또, strategy pattern 을 통해 템플릿 메서드의 단점을 어느정도 해결할 수 있고, 디폴트 매서드 인터페이스의 경우 함수형 인터페이스로 간단하게 사용할 수 없어 편의성이 더 떨어진다. 다음에 strategy pattern 을 학습하면서 작동 방식을 비교하면서 다시 학습 해봐야겠다.
[ A. 오늘 복습한 내용 / B. 다음에 학습할 내용 ]
A. 탬플릿 메서드 복습...
B. Strategy 패턴
B. Factory Method 패턴
[Reference]
[ 🌟Kotlin Default Method Interface 🌟 ]
'웹개발 - Back 관련 > Architecture' 카테고리의 다른 글
[ GOF ] 싱글톤 패턴, object - Singleton Pattern with Kotlin (6) | 2024.10.08 |
---|---|
[ GOF ] 팩토리 매서드 패턴 - Factory Method Pattern with Kotlin (0) | 2024.09.11 |
[ GOF ] 전략 패턴 - Strategy Pattern with Koltin (0) | 2024.09.10 |
[ GOF ] 어댑터 패턴 - Adapter Pattern with Kotlin (0) | 2024.09.09 |
[ GOF ] 반복자 패턴 - Iterator Pattern with Kotlin (2) | 2024.09.09 |