[Spring Cloud] - 5. Zuul Gateway를 이용한 Filtering

이번 포스트에서는 Routing에 이어서 Zuul Gateway를 이용한 Filtering에 대해서 이야기해보도록 하겠습니다.

 

Filtering

Spring Boot에서 필터링을 사용하는 경우는 PreFilter와 같은 엔드포인트의 보안 등에서 사용됩니다. 이 필터링을 이용하기 위해서는 Spring Security에서 제공하는 JWT 등의 보안 수단을 사용하여 인증을 받고, 엔드포인트에 접근하는 방식이죠.

Zuul Gateway에서도 엔드포인트의 보안을 적용할 수 있는 필터링 기능을 제공합니다. 

위 아키텍처는 Zuul Gateway의 코어를 그린 아키텍처입니다. Zuul Servlet을 통해 들어오는 요청을 Routing 하게 되는데요. 그리고, 그 밑단에는 ZuulFilter Runner가 있어서 엔드포인트에 대한 Filtering을 적용하게 됩니다.

Filtering은 클라이언트의 HTTP 요청을 받고, 응답하는 과정에서 라우팅하는 동안의 수행하는 액션 중에 하나이다.

  • Pre-Filter: API 서버로 라우팅 되기 전에 수행하는 필터 -> 인증, 로깅, 디버깅 등의 처리.
  • Route-Filter: 요청에 대한 라우팅을 제어하기 위해 수행하는 필터 -> Ribbon을 이용한 클라이언트 요청 동적 라우팅
  • Post-Filter: API 서버로 라우팅 된 후 수행하는 필터 -> 응답에 HTTP 헤더 추가 및 API 응답속도, 메트릭 등의 데이터 수집
  • Error-Filter: 위 3단계 필터에서 발생된 오류 수행 필터 -> Exception

API 서버를 구축할 때 자주 사용되는 필터링들이 모여져 있으며, API 요청/응답시에 보여지는 라이프 사이클이라고 봐도 무방합니다.

 

How to use

이제 그럼 이 Filtering을 한 번 만들어보도록 하죠. 여태까지 했던 그대로 지난 Routing에서 했던 Gateway 프로젝트를 그대로 가져와 이용해보도록 하겠습니다.

먼저 Pre-Filter를 구현해봅시다.

/**
 * Created by Neon K.I.D on 1/22/20
 * Blog : https://blog.neonkid.xyz
 * Github : http://github.com/NEONKID
 */
class GatewayPreFilter : ZuulFilter() {
    val logger = LogManager.getLogger()

    override fun run() {
        val ctx = RequestContext.getCurrentContext()
        val req = ctx.request
        logger.info("Using Pre Filter: ${req.method} request to ${req.requestURL}")
    }

    override fun shouldFilter() = true

    override fun filterType() = FilterConstants.PRE_TYPE

    override fun filterOrder() = 0
}

Pre-Filter는 클라이언트가 Gateway로 거칠 떄 먼저 지나가는 입구 중 하나입니다. API 서버에 라우팅 되기 이전에 처리되는 곳이라는 것이죠. 

따라서 서버의 로그에 가장 맨 앞 부분에 남기 때문에, 클라이언트가 어떤 메소드로 어떤 URI를 불렀는지를 나타내주는 것을 불러봤고, 이 떄 사용한 것은 바로 RequestContext 객체입니다.

다음은 Route-Filter 입니다.

/**
 * Created by Neon K.I.D on 1/22/20
 * Blog : https://blog.neonkid.xyz
 * Github : http://github.com/NEONKID
 */
class GatewayRouteFilter : ZuulFilter() {
    val logger = LogManager.getLogger()

    override fun run() {
        logger.info("Using Route Filter: ")
    }

    override fun shouldFilter(): Boolean {
        return RequestContext.getCurrentContext().routeHost != null
                && RequestContext.getCurrentContext().sendZuulResponse()
    }

    override fun filterType() = FilterConstants.ROUTE_TYPE

    override fun filterOrder() = 0
}

Route-Filter는 라우팅을 하는 역할이지만, shouldFilter라는 오버라이딩 메소드를 통해서 특정 Context일 때, 필터링을 제공하도록 한다는 점이 다릅니다. 

만약, 클라이언트가 요청한 서비스 외에 다른 서비스에도 해당 요청을 해야 한다면, 이 필터링을 사용하는 것도 괜찮겠죠.

/**
 * Created by Neon K.I.D on 1/22/20
 * Blog : https://blog.neonkid.xyz
 * Github : http://github.com/NEONKID
 */
class GatewayPostFilter : ZuulFilter() {
    val logger = LogManager.getLogger()

    override fun run() {
        val req = RequestContext.getCurrentContext()
        val res = req.response
        res.addHeader("X-Sample", UUID.randomUUID().toString())

        logger.info("Using Post Filter: X-Sample - ${res.getHeader("X-Sample")}")
    }

    override fun shouldFilter() = true

    override fun filterType() = FilterConstants.POST_TYPE

    override fun filterOrder() = 0
}

Post-Filter는 API 호출이 정상적으로 이루어진 후, 다시 Gateway에서 API 서버로부터 응답이 왔을 경우, 해당 응답을 나타내주는 필터라고 할 수 있습니다. 

음, 이를테면 백엔드 구간에서 중요한 정보를 필터링하여, 서버에 남도록 하고, 그 외의 정보는 클라이언트에게 넘겨주는 방식으로도 사용할 수 있죠.

/**
 * Created by Neon K.I.D on 1/22/20
 * Blog : https://blog.neonkid.xyz
 * Github : http://github.com/NEONKID
 */
class GatewayErrorFilter : ZuulFilter() {
    val logger = LogManager.getLogger()

    override fun run() {
        val throwable = RequestContext.getCurrentContext().throwable
        logger.error("Using Error Filter: $throwable")
    }

    override fun shouldFilter(): Boolean {
        val ctx = RequestContext.getCurrentContext()
        return ctx.throwable != null
    }

    override fun filterType() = FilterConstants.ERROR_TYPE

    override fun filterOrder() = 0
}

마지막으로 Error-Filter는 오류가 발생되었을 때 처리하는 필터링입니다. 위 코드처럼 요청 작업시 발생한 Exception이 존재한 경우 오류를 로깅하는 것이 기본형입니다.

 

What is Zuul Filter ?

음 그런데, ZuulFilter라는 것은 무엇일까요? Pre, Post, Route, Error 이렇게 주요 4가지 필터링이 무슨 역할을 하고, 어떻게 수행하는지는 알겠는데, 각 필터링마다 부모 클래스인 Zuul Filter가 존재합니다.

ZuulFilter 클래스는 Zuul Gateway에서 기본적으로 제공하는 Filter 추상 클래스 중에 하나입니다. 즉, Zuul Gateway에서 Filter를 구현하기 위한 구조만을 나타낸 클래스이지요. 여러가지 필터가 있지만, 기본적으로 간단한 Filter를 사용할 때는 Zuul Filter를 부모 클래스로 사용합니다.

/**
 * Created by Neon K.I.D on 1/22/20
 * Blog : https://blog.neonkid.xyz
 * Github : http://github.com/NEONKID
 */
class GatewayCustomFilter : ZuulFilter() {
    override fun run(): Any {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun shouldFilter(): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun filterType(): String {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun filterOrder(): Int {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}

각 추상 메소드들에 대해서 간략하게 얘기해보겠습니다. ZuulFilter 클래스를 사용하기 위해 구현해야 하는 메소드는 아래 4개입니다.

  • run: 해당 필터가 실행될 때 수행하는 메소드
  • shouldFilter: 이 필터가 발생하는 조건 메소드
  • filterType: 이 필터는 무슨 필터인지 반환하는 메소드
  • filterOrder: 필터의 우선 순위를 반환하는 메소드

따라서 우리가 위에서 구현했던 shouldFilter 메소드에 true를 주어준다면, 모든 필터링이 동작하게 됩니다. 특히 Error Filter의 경우 true로 반환값을 준다면, 항상 Error-Filter가 발생하게 되는 것이죠.

 

Test

간단한 테스트를 진행해보도록 하겠습니다. 테스트 역시 지난 포스트와 마찬가지로 curl 명령어를 사용하여 테스트 해 볼 수 있습니다.

 

2020-01-22 17:47:03.399  INFO 12861 --- [nio-9100-exec-1] x.n.g.filters.GatewayPreFilter           : Using Pre Filter: GET request to http://localhost:9100/v1/member
2020-01-22 17:47:03.400  INFO 12861 --- [nio-9100-exec-1] x.n.g.filters.GatewayRouteFilter         : Using Route Filter: 
2020-01-22 17:47:03.412 ERROR 12861 --- [nio-9100-exec-1] x.n.g.filters.GatewayErrorFilter         : Using Error Filter: com.netflix.zuul.exception.ZuulException: Filter threw Exception
2020-01-22 17:47:03.414  WARN 12861 --- [nio-9100-exec-1] o.s.c.n.z.filters.post.SendErrorFilter   : Error during filtering

com.netflix.zuul.exception.ZuulException: Forwarding error
	at org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter.handleException(SimpleHostRoutingFilter.java:261) ~[spring-cloud-netflix-zuul-2.2.2.BUILD-SNAPSHOT.jar:2.2.2.BUILD-SNAPSHOT]
	at org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter.run(SimpleHostRoutingFilter.java:241) ~[spring-cloud-netflix-zuul-2.2.2.BUILD-SNAPSHOT.jar:2.2.2.BUILD-SNAPSHOT]

...

2020-01-22 17:47:03.432  INFO 12861 --- [nio-9100-exec-1] x.n.g.filters.GatewayPostFilter          : Using Post Filter: X-Sample - null

저는 Gateway만 실행한 상태로 Member API를 호출해봤습니다. 당연한 얘기겠지만, API 서버가 동작하고 있지 않기 때문에 클라이언트에서는 500 Internal Server Error 오류를 반환하게 되고, 서버에서는 어떤 오류가 발생했는지, 어떤 API를 클라이언트가 호출했는지 등 우리가 미리 작성했던 로직들이 동작하는 것을 보실 수 있습니다.

 

마치며...

Zuul Gateway의 Filtering 부분을 간단하게 살펴봤습니다. Spring boot에서 이용했던 것 그대로 Filter를 사용하여 엔드포인트의 보안 등을 관리할 수 있고, 이를 통해 인증을 구현할 수도 있습니다. 따라서 기존의 Spring boot를 이용한 백엔드 개발자라면 쉽게 Zuul Gateway의 필터링을 사용할 수 있습니다.

다음 포스트에서는 Service Discovery에 대한 주제로 이어가도록 하겠습니다.

 

Ref: Zuul-core (https://medium.com/netflix-techblog/announcing-zuul-edge-service-in-the-cloud-ab3af5be08ee)

comments powered by Disqus

Tistory Comments 0