Kotlin

[코틀린 코딩 습작] Intercepting Fillter

[하마] 이승현 (wowlsh93@gmail.com) 2021. 5. 15. 19:10


컨베이어 공장에서 자동차가 완성 될 때 단계별로 조금씩 완성되어 가는 모습을 볼 수 있다. 이런 비슷한 경우는 소프트웨어 개발에서도 흔하며, 그 결과로 관련 패턴들도 매우 다양하게 있지다.   
Gof의  Chain of Responsibility패턴은 모든 과정을 거치는게 아니라, 처리 할 수 있으면 처리하고, 못하면 다음 녀석에게  넘기는 "의도"를 말한다. 그리고 이 글의 제목이기도 한 Intercepting Filter의 경우는 (웹개발자 라면 친숙한  HTTP가 호출되어서 최종 컨트롤러 까지 갔다가[inbound], 다시 HTTP리턴을 해주는[outbound] 모습을 상상해 보자) 단계별로 모든 처리를 진행해서 타겟까지 호출되고, 타겟에서 최종 처리된 결과를 역순의 단계로 다시 돌아오는 모형에서 중간 중간 필요한 Filter를 사용자가 쉽게 넣을 수 있게 해주는 의도를 가지고 있다.

clinet -> filter1(word filter) -> filter2 (count filter) -> target  [ inbound]
client <- filter1(word filter) <- filter2 (count filter) <- target  [outbound] 

방식1) 순서를 제어하는 Filter Manager가 필터들을 관리하고 호출을 통제한다.

interface Filter{
    fun process(chain: FilterChain, req: Any, res: Any)
}

class WordFilter: Filter {
    override fun process(chain: FilterChain, req: Any, res: Any){
        //in bound
        val req = req as String
        val listReq = req.splitToSequence(" ").toList()

        chain.process(listReq, res)
    }
}

class CountFilter: Filter {
    override fun process(chain: FilterChain, req: Any, res: Any) {
        chain.process(req, res)
        //out bound

        val res = res as Response?
        val resMap = res?.result  as Map<String, Int>
        res?.result = resMap.filter{it -> it.value >= 2}.size

    }
}

class Target: Filter{
    override fun process(chain: FilterChain, req: Any, res: Any) {
        val req = req as List<String>
        println("Target")
        val mapReq = req.groupBy { it }.mapValues { it.value.size }
        val res = res as Response?
        res?.result = mapReq
    }
}

class FilterChain {

    val filters = mutableListOf<Filter>()
    var nextFilter = -1
    fun addFilter(filter: Filter): FilterChain {
        filters.add(filter)
        return this
    }

    fun process(req: Any, res: Any) {
        nextFilter++
        filters.get(nextFilter).process(this, req, res)
        // 더이상의 필터가 없을 경우 target을 실행시키기도 한다.
        // if (hasNextFilter) nextfilter.process else target.execute() 식으로 
    }
}

class Response {
    var result : Any? = null
}

fun main() {
    val res :Response = Response()

    FilterChain()
        .addFilter(WordFilter())
        .addFilter(CountFilter())
        .addFilter(Target())  // target은 필터에서 제외되어 따로 처리하기도 한다.
        .process("I AM A BOY I AM A GIRL", res)

    println(res.result)
}

FilterChain은 각 Filter들을 관리하며, Filter들의 연관관계 및 순서등을 조율할 수 있다. 
현재 코드에서는 Filter의 리스트상의 순서대로 호출해주고 있지만, 얼마든지 변형 가능 할 것이다. 

각 필터의 process 함수에서는 각각의 비지니스 로직을 처리할수있는데, CountFilter 처럼 다음 필터를 호출한 후에 비지니스 로직을 처리하면 자연스럽게 후처리(out bound) 목적의 코드가된다. 

위의 코드는 사실 너무 복잡한 면이 있는데, 이를 변형한 다른 방법으론 아래와 같이 짤 수도 있을 것이다.
개인적으로는 이런 방식이 좀 더 의도가 명확하게 느껴진다. 

방식2) Filter Manager가 필요 없으며 필터 스스로 다음 필터를 알아야 한다.

data class Request (var param : Any?)
class Response {
    var result : Any? = null
}

interface Filter {
    fun addLast(chain: Filter): Filter
    fun doNext(req: Request): Response
}

abstract class AbstractFilter: Filter{
    var next: Filter? = null
    override fun addLast(chain: Filter): Filter {
        require(this != chain) { "Filter duplicated: $this"}
        val setter = this.next?.let { it::addLast }?:this::next.setter::invoke
        setter(chain)
        return this
    }
}

class WordFilter: AbstractFilter() {

    override fun doNext(req: Request): Response {
        val param = req?.param as String
        req?.param = param.splitToSequence(" ").toList()

        val ret = next?.doNext(req)!!
        return ret
    }
}

class CountFilter: AbstractFilter() {
    override fun doNext(req: Request): Response {
        val ret = next?.doNext(req)

        val resMap = ret?.result as Map<String, Int>
        ret?.result = resMap.filter { it -> it.value >= 2 }.size
        return ret
    }
}

class Target: AbstractFilter(){
    override fun doNext(req: Request): Response {
        val param = req?.param as List<String>
        println("Target")
        val mapReq = param.groupBy { it }.mapValues { it.value.size }
        val res = Response()
        res?.result = mapReq
        return res
    }
}

class FilterChain{

    fun process(req: Request): Response  {
        val chain = WordFilter()
            .addLast(CountFilter())
            .addLast(Target())

        return chain.doNext(req)
    }
}


fun main() {
    val req = Request("I AM A BOY I AM A GIRL")
    val res = FilterChain().process(req)
    println(res?.result as Int)
}

 

먼저의 방식이 FilterChain이 Filter들의 순서를 단독적으로 제어하는 것에 비해서, (즉 각 Filter는 자기의 책임을 다하고, FilterChain에게, 다음것이 먼지모르겠으나 다음 처리를 해주세요~ 라고 위임) 이 방식은 링크드 리스트 방식으로 필터 스스로가 다음 필터를 내장하게 만들었다. 따라서 좀 더 직관적으로 볼 수 있겠다.  

P.S)
- 참고로 위의 예제는 좀 문제가 많다. word, count 하는 것일 뿐이라면 그냥 한줄로 짜도된다. 
- 패턴이나 코드들은 상황에 따라서 짜야하는게 맞다. 유명 패턴이라고 맞는게 절대 아니다.