[디자인 패턴] Strategy, Template Method

2025. 5. 23. 01:32·cs 공부 기록용

디자인 패턴이 너무 어려워서 정리하기 위한 포스팅 / 개인 기록용

 

Strategy, Template Method 패턴은 디자인 패턴 중 행동 패턴에 속한다.

행동 패턴이라 함은 객체의 행동을 캡슐화하고 상황에 따라 동적으로 동작할 수 있도록 만드는 패턴이다.

 

Strategy, Template Method는 비슷한 느낌이다. 

어떠한 함수, 행위들을 보니 전체적인 흐름, 진행 알고리즘은 동일한데 세부적인 구현사항이 다른 경우 사용한다.

큰 틀(흐름, 진행 알고리즘)을 추상화하고 구현, 혹은 상속을 통해 하위 클래스가 직접 상황에 따른 세부적인 구현을 한다.

그리고 기능을 사용하는 클라이언트에서는 추상화 타입을 사용하여 실제 기능을 실행한다.

근데 디자인 패턴은 글로 봐도 절대 쉽게 이해가 되지 않는다. 아래에서 자세히 알아보자.

Strategy Pattern

Strategy Pattern을 라면을 만드는 클라이언트가 사용하는 상황이라고 가정하자.

신라면, 안성탕면, 진라면 암튼 여러 라면들이 있다.

분석해보니 각 라면들의 조리법은 물을 끓이고, 스프를 넣고, 특정 추가 재료를 넣고, 끓이는 단계로 흐름은 동일하다는 것을 알게 되었다.

 

위에서 언급되었듯이 이 전략은 큰 틀을 추상화하고 구현, 혹은 상속을 통해 하위 클래스가 직접 상황에 따른 세부적인 구현을 한다고 했다.

전략패턴에서 큰 틀은 전략(인터페이스)에 해당한다.

따라서 라면을 끓이는 과정인 큰 틀 RamenMakingStrategy 인터페이스에는 라면을 끓이는 과정들이 추상화 되어있다.

stage1, stage2, stage3, stage4를 물을 끓이고, 스프를 넣고, 재료를 넣고, 끓이는 단계라고 생각하면 된다.

 

그리고 상황에 따라 세부적인 조리 방법이 다를것이다. 예를 들어 안성탕면은 물을 500ml만 넣고 끓이고, 재료를 넣을 때 계란을 2개 넣고..

신라면은 물을 600ml만 넣고 끓이고, 추가 재료는 없고 등등.. 따라서 상황에 따른 Strategy를 구현한다.

strategy의 구현체들은 위 그림에서 ARamenMakingStrategy(안성탕면), BRamenMakingStrategy(신라면)라고 생각하자.

 

그리고 Context라는 개념이 있다. 전략 패턴의 경우 클라이언트는 사실상 Context를 통해 간접적으로 strategy 구현체를 사용하게 된다.

Context에는 사용될 전략이 존재한다. 사실 여기까지 그림과 설명으로 봐도 이해하기는 쉽지 않을 수 있다.

실제 코드로 보자.

interface RamenMakingStrategy {
    fun boilWater()
    fun addPowder()
    fun addIngredients()
    fun boil()
    fun make() {
        boilWater()
        addPowder()
        addIngredients()
        boil()
        println("라면 완성")
    }
}

 

먼저 라면을 끓이는 과정을 큰 틀로 정의한 전략을 인터페이스로 추상화 한다.

class SinRamenMakingStrategy : RamenMakingStrategy {
    override fun boilWater() {
        println("물을 500ml 넣고 1분 끓입니다.")
    }

    override fun addPowder() {
        println("스프를 넣고 다시다를 넣습니다.")
    }

    override fun addIngredients() {
        println("날계란 2개를 넣습니다.")
    }

    override fun boil() {
        println("1분간 강불에 끓입니다.")
    }
}

class JinRamenMakingStrategy : RamenMakingStrategy {
    override fun boilWater() {
        println("물을 600ml 넣고 1분 끓입니다.")
    }

    override fun addPowder() {
        println("스프를 넣습니다.")
    }

    override fun addIngredients() {
        println("날계란 1개를 풀어서 넣습니다.")
    }

    override fun boil() {
        println("2분간 강불에 끓입니다.")
    }
}

 

그리고 큰 틀을 세부적으로 구현하는 신라면, 진라면 전략 구현체를 생성한다.

class RamenMakingContext(
    private var strategy: RamenMakingStrategy
) {
    fun make() {
        strategy.make()
    }
}

 

그리고 실질적으로 클라이언트가 사용하고자 하는 전략을 Context가 전달받아 대신 처리해준다.

fun client() {
    val ramenMakingContext = RamenMakingContext(SinRamenMakingStrategy())
    ramenMakingContext.make()
}

 

클라이언트는 사용하고자 하는 전략을 Context에 전달하고 Context가 제공하는 함수를 통해 기능을 호출한다.

package com.eager.kostudy

import org.junit.jupiter.api.Test

interface RamenMakingStrategy {
    fun boilWater()
    fun addPowder()
    fun addIngredients()
    fun boil()
    fun make() {
        boilWater()
        addPowder()
        addIngredients()
        boil()
        println("라면 완성")
    }
}

class SinRamenMakingStrategy : RamenMakingStrategy {
    override fun boilWater() {
        println("물을 500ml 넣고 1분 끓입니다.")
    }

    override fun addPowder() {
        println("스프를 넣고 다시다를 넣습니다.")
    }

    override fun addIngredients() {
        println("날계란 2개를 넣습니다.")
    }

    override fun boil() {
        println("1분간 강불에 끓입니다.")
    }
}

class JinRamenMakingStrategy : RamenMakingStrategy {
    override fun boilWater() {
        println("물을 600ml 넣고 1분 끓입니다.")
    }

    override fun addPowder() {
        println("스프를 넣습니다.")
    }

    override fun addIngredients() {
        println("날계란 1개를 풀어서 넣습니다.")
    }

    override fun boil() {
        println("2분간 강불에 끓입니다.")
    }
}

class RamenMakingContext(
    private var strategy: RamenMakingStrategy
) {
    fun make() {
        strategy.make()
    }
}

class RamenStrategyPatternStudy {
    @Test
    fun client() {
        val ramenMakingContext = RamenMakingContext(SinRamenMakingStrategy())
        ramenMakingContext.make()
    }
}

 

위 코드는 전체적인 흐름이다.

interface DiscountStrategy {
    fun discount(originalPrice: Int): Int
}

class WelcomeDiscountStrategy : DiscountStrategy {
    override fun discount(originalPrice: Int): Int {
        return originalPrice / 2
    }
}

class SummerDiscountStrategy : DiscountStrategy {
    override fun discount(originalPrice: Int): Int {
        return originalPrice - 1000
    }
}

class DefaultDiscountStrategy : DiscountStrategy {
    override fun discount(originalPrice: Int): Int {
        return originalPrice
    }

}

class DiscountStrategyContext(
    var discountStrategy: DiscountStrategy
) {
    fun discount(originalPrice: Int): Int {
        discoutStrategy.discount()
    }
}

fun client() {
    val context = DiscountStrategyContext(DefaultDiscountStrategy())
    
    if (user.isNew()) {
        context.discountStrategy = WelcomeDiscountStrategy())
    }
    
    if (isSummer()) {
        context.discountStrategy = SummerDiscountStrategy()
    }
    
    context.discount(originalPrice)
}

 

또한 클라이언트는 상황에 따라 Context에 보관되어 있는 전략을 변경할 수 있다. 위 예시의 경우 할인 정책을 전략으로 정의하고

상황에 따라 다른 할인 전략을 사용할 수 있도록 한다. 예를 들어 신규 가입자 할인, 여름 할인, 기본 할인이 있다.

클라이언트는 자신이 사용할 전략을 Context에 전달하고 Context에게 요청을 위임한다.

이렇게 하면 상황에 따라 다른 전략을 사용해 처리할 수 있다.

Template Method

https://refactoring.guru/ko/design-patterns/template-method

 

템플릿 메서드 패턴도 전략 패턴과 유사하게 전체적인 큰 틀은 추상화하여 정의하고 세부적인 로직은 하위 클래스에서 정의하는 것은

동일하다. 하지만 Context라는 중간 매개체는 없고 일반적으로 템플릿 메서드 패턴은 큰 틀이 추상 클래스로 정의될 수 있다.

package com.eager.questioncloud.application.event

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.future.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import org.springframework.scheduling.annotation.Scheduled
import software.amazon.awssdk.services.sns.SnsAsyncClient
import java.util.concurrent.CopyOnWriteArrayList

abstract class AbstractEventProcessor<T : SQSEvent>(
    private val snsAsyncClient: SnsAsyncClient
) {
    abstract fun getUnpublishedEvents(): List<T>

    abstract fun updateRepublishStatus(eventIds: List<String>)
    
    @Scheduled(fixedDelay = 10000)
    open suspend fun republishScheduled() {
        var hasMoreEvents = true

        while (hasMoreEvents) {
            val events = getUnpublishedEvents()

            if (events.isEmpty()) {
                hasMoreEvents = false
            }

            val publishedEventIds = republish(events)
            updateRepublishStatus(publishedEventIds)
        }
    }    
    
    private suspend fun republish(eventIds: List<T>): List<String> {
        val publishedEventIds = CopyOnWriteArrayList<String>()
        supervisorScope {
            eventIds.forEach { event ->
                launch(Dispatchers.IO) {
                    snsAsyncClient.publish(event.toRequest()).await()
                    publishedEventIds.add(event.eventId)
                }
            }
        }
        return publishedEventIds
    }    
}

 

개인 프로젝트에서 Transaction Outbox Pattern을 구현할 때 이벤트를 재발행하는 로직을 리팩토링 했었는데 그 결과가

우연히 템플릿 메서드 패턴이였다.

 

프로젝트에서 발행되는 이벤트의 종류는 2개가 있다. 

  1. DB에서 발행되지 않은 이벤트들을 조회한다.
  2. AWS SNS API를 통해 이벤트를 발행한다.
  3.  발행된 이벤트들을 DB에 발행 완료로 업데이트 한다.

이벤트의 종류가 다르더라도 재발행이 처리되는 전체적인 큰 틀은 위와 동일하다.
단지 이벤트 타입과, 이벤트 로그를 보관하는 테이블이 달라 다른 Repository를 사용해야 한다는 것이다.

@Component
class QuestionPaymentEventProcessor(
    private val questionPaymentEventLogRepository: QuestionPaymentEventLogRepository,
    private val applicationEventPublisher: ApplicationEventPublisher,
    private val snsAsyncClient: SnsAsyncClient,
) : AbstractEventProcessor<QuestionPaymentEvent>(snsAsyncClient) {
    override fun getUnpublishedEvents(): List<QuestionPaymentEvent> {
        return questionPaymentEventLogRepository.getUnPublishedEvent()
            .stream()
            .map { log -> SQSEvent.objectMapper.readValue(log.payload, QuestionPaymentEvent::class.java) }
            .toList()
    }

    override fun updateRepublishStatus(eventIds: List<String>) {
        questionPaymentEventLogRepository.publish(eventIds)
    }
}

// ------------------------------------------------------------------------------

@Component
class ReviewEventProcessor(
    private val questionReviewEventLogRepository: QuestionReviewEventLogRepository,
    private val snsAsyncClient: SnsAsyncClient,
    private val applicationEventPublisher: ApplicationEventPublisher
) : AbstractEventProcessor<ReviewEvent>(snsAsyncClient) {
    override fun getUnpublishedEvents(): List<ReviewEvent> {
        return questionReviewEventLogRepository.getUnPublishedEvent()
            .stream()
            .map { log -> SQSEvent.objectMapper.readValue(log.payload, ReviewEvent::class.java) }
            .toList()
    }

    override fun updateRepublishStatus(eventIds: List<String>) {
        questionReviewEventLogRepository.publish(eventIds)
    }
}

 

 

위 2개의 구현체들은 단순히 상위 클래스(AbstractEventProcessor)에서 정의된 알고리즘을 위해 특정 메서드를 세부적으로 한다.

그리고 실제로 위 스케줄러가 동작할 때에는 상위 클래스에서 정의 된 republishScheduled의 흐름대로 동작한다.

따라서 동일한 이벤트 재발행 로직을 사용하면서도 각 이벤트 종류에 맞게 레포지토리를 사용하여 처리할 수 있다.

package com.eager.kostudy

import org.junit.jupiter.api.Test

abstract class AbstractRamenMaker {
    protected abstract fun boilWater()
    protected abstract fun addPowder()
    protected abstract fun addIngredients()
    protected abstract fun boil()
    fun make() {
        boilWater()
        addPowder()
        addIngredients()
        boil()
        println("라면 완성")
    }
}

class SinRamenMaker : AbstractRamenMaker() {
    override fun boilWater() {
        println("물을 500ml 넣고 1분 끓입니다.")
    }

    override fun addPowder() {
        println("스프를 넣고 다시다를 넣습니다.")
    }

    override fun addIngredients() {
        println("날계란 2개를 넣습니다.")
    }

    override fun boil() {
        println("1분간 강불에 끓입니다.")
    }
}

class JinRamenMaker : AbstractRamenMaker() {
    override fun boilWater() {
        println("물을 600ml 넣고 1분 끓입니다.")
    }

    override fun addPowder() {
        println("스프를 넣습니다.")
    }

    override fun addIngredients() {
        println("날계란 1개를 풀어서 넣습니다.")
    }

    override fun boil() {
        println("2분간 강불에 끓입니다.")
    }
}

class RamenTemplatePatternStudy {
    @Test
    fun client() {
        val sinMaker = SinRamenMaker()
        val jinMaker = JinRamenMaker()

        sinMaker.make()
        jinMaker.make()
    }
}

 

그리고 똑같이 전략 패턴에서 나온 라면 예시도 템플릿 메서드 패턴으로 구현될 수 있다.

전략 패턴과 흐름은 매우 유사하다. 다만 템플릿 메서드 패턴은 추상화를 추상 클래스로 한다는 점, 중간에 Context와 같은 중간 단계가

없이 클라이언트에서 직접 make()와 같은 메서드를 사용하여 작업을 처리한다.

정리

전략 패턴, 템플릿 메서드 패턴 모두 특정 기능의 전체적인 알고리즘을 추상화하여 정의하고 세부적인 작업들은 하위 클래스에서

직접 구현하여 처리하는 행동 패턴이다.

 

전략 패턴의 경우 인터페이스로 전체적인 알고리즘을 추상화하고, 전략 구현체를 보관하고 있는 Context라는 중간 단계를 사용하여

유연하게 처리한다.

 

템플릿 메서드 패턴의 경우 추상 클래스로 전체적인 알고리즘을 추상화하고, 이를 구현하는 구현체를 직접 클라이언트가 사용한다.

즉 상속을 사용하는 디자인 패턴이기 때문에 결합력이 강하다고 한다.

 

전략 패턴은 런타임 시 상황에 따라 사용할 전략을 변경할 수 있다는 장점이 있고 인터페이스를 사용한 추상화이기에 결합도가 낮다는

장점이 있다.

 

'cs 공부 기록용' 카테고리의 다른 글

[디자인 패턴] Bridge, Adapter  (0) 2025.05.30
[디자인 패턴] Factory Method, Abstract Factory  (1) 2025.05.25
[디자인 패턴] Proxy, Decorator  (0) 2025.05.24
운영체제 기록용  (0) 2025.02.08
'cs 공부 기록용' 카테고리의 다른 글
  • [디자인 패턴] Bridge, Adapter
  • [디자인 패턴] Factory Method, Abstract Factory
  • [디자인 패턴] Proxy, Decorator
  • 운영체제 기록용
e4g3r
e4g3r
e4g3r 님의 블로그 입니다.
  • e4g3r
    e4g3r 님의 블로그
    e4g3r
  • 전체
    오늘
    어제
    • 분류 전체보기 (41) N
      • spring (22)
      • kotlin, java (11) N
      • database (3)
      • cs 공부 기록용 (5)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
e4g3r
[디자인 패턴] Strategy, Template Method
상단으로

티스토리툴바