전략 패턴이란?
전략 패턴이란 런타임에 교체 가능한 알고리즘을 만드는데 사용되는 패턴이다. 하나의 객체가 다양하면서 변화하는 역할을 해야할 때 사용된다. 이름이 전략 패턴인 이유는, 객체의 행위를 전략(Strategy)을 통해 동적으로 바꿔주도록 해서 객체를 유연하게 만드는 패턴이기 때문이다.
전략 패턴 예시
우리가 이메일을 보낸다고 해보자. 이메일에는 시작말과 맺음말이 들어가며 사람마다 혹은 기기마다 이를 다르게 설정할 수 있어야 한다. 예를 들어 아마 한 동안 아이폰에서 메일을 보냈을 때 끝에 "Sent from IOS"라는 문구가 들어갔던 것을 기억할 것이다. 어떤 사람들은 아이폰을 사용하지 않음에도 저 문구가 멋있어 보여서 마지막에 "Send from IOS"라는 문구를 붙여서 보내기도 했다.
이를 하기 위한 간단한 방법은 다음과 같이 사용하는 것이다.
val message = "Hello"
println("$message: Written from IOS")
그러면 "Hello: Written from IOS" 가 출력된다.
하지만, 이를 사용자 마음대로 바꾸도록 하고 싶을 수 있다. 이를 위해 우리는 메세지를 변환할 수 있는 MessageConverter을 만들어야 한다.
class MessageConverter(private var messageGenerateStrategy: MessageGenerateStrategy) {
fun convert(string: String): String {
return messageGenerateStrategy.process(string)
}
fun setStrategy(messageGenerateStrategy: MessageGenerateStrategy) {
this.messageGenerateStrategy = messageGenerateStrategy
}
}
MessageConverter은 MessageGenerateStrategy(메세지 생성 전략) 객체를 인자로 받으며, convert 함수를 수행 시 MessageGenerateStrategy의 전략에 따라 다르게 코드를 만들어 낸다. 또한 setStrategy 함수를 통해 MessageGenerateStrategy를 동적으로 바꿀 수도 있다.
자 이제 MessageGenerateStrategy를 만들어보자. Kotlin은 추상 클래스로서 사용되는 sealed class를 제공하기 때문에 sealed class를 사용해 MessageGenerateStrategy를 process라는 abstract fun이 포함된 형태로 만들어낸다.
sealed class MessageGenerateStrategy {
abstract fun process(string: String): String
}
자 이제 이를 상속받는 다양한 클래스를 만들어보자. 첫 째는 AttachHeader이다. 메세지의 앞부분에 문자를 붙일 수 있는 전략이다. 둘 째는 UpperCase이다. 문자를 대문자로 변환시켜주는 전략이다. 셋 째는 Etc이다 etc는 strategy 자체를 인자로 받으며 자유롭게 메세지 생성 방식을 설정할 수 있다.
sealed class MessageGenerateStrategy {
abstract fun process(string: String): String
data class AttachHeader(val header: String) : MessageGenerateStrategy() {
override fun process(string: String): String {
return "$header $string"
}
}
object UpperCase : MessageGenerateStrategy() {
override fun process(string: String): String {
return string.uppercase()
}
}
data class Etc(val strategy: (String) -> String) : MessageGenerateStrategy() {
override fun process(string: String): String {
return strategy(string)
}
}
}
자 이제 이들 전략을 각각 적용해 실행시켜보자. 먼저 메세지를 "Hello"로 생성한다.
val message = "Hello"
val fromIOSStrategy = MessageGenerateStrategy.Etc { message: String -> "${message} : Written from IOS" }
val messageConverter = MessageConverter(fromIOSStrategy)
println(messageConverter.convert(message))
// 출력
// Hello : Written from IOS
먼저 마지막에 ": Written from IOS"를 붙이는 전략을 MessageConverter 객체에 넣어보자.
이후 변환한 후 출력하면 "Hello : Written from IOS" 가 나오게 된다.
val messageHeaderStrategy = MessageGenerateStrategy.AttachHeader("Message : ")
messageConverter.setStrategy(messageHeaderStrategy)
println(messageConverter.convert(message))
// 츨력
// Message : Hello
다음으로는 위에서 생성한 messageConverter에 전략만 Header에 "Message : "를 붙이는 전략을 넣는다.
이후 변환하면 "Message : Hello" 가 나온다.
messageConverter.setStrategy(MessageGenerateStrategy.UpperCase)
println(messageConverter.convert(message))
// 츨력
// HELLO
다음으로는 대문자로 변환하는 전략을 messageConverter에 set 한다.
그러면 출력은 "HELLO"가 되는 것을 확인할 수 있다.
전체 코드는 아래에 있다.
전체 코드
fun main() {
val message = "Hello"
val fromIOSStrategy = MessageGenerateStrategy.Etc { message: String -> "${message} : Written from IOS" }
val messageConverter = MessageConverter(fromIOSStrategy)
println(messageConverter.convert(message))
// 출력
// Hello : Written from IOS
val messageHeaderStrategy = MessageGenerateStrategy.AttachHeader("Message : ")
messageConverter.setStrategy(messageHeaderStrategy)
println(messageConverter.convert(message))
// 츨략
// Message : Hello
messageConverter.setStrategy(MessageGenerateStrategy.UpperCase)
println(messageConverter.convert(message))
// 츨략
// HELLO
}
정리
전략 패턴은 유연하게 코드를 수정하기 위해 사용되는 디자인 패턴이다. Kotlin에서는 다양한 전략을 seal 할 수 있는 sealed class를 제공하므로 sealed class를 활용해 전략을 만들도록 하자.