Hello, Freakin world!

[Zuul] route 타입 필터 작성 - A/B 테스팅 구현하기 본문

Spring Cloud/Gateway

[Zuul] route 타입 필터 작성 - A/B 테스팅 구현하기

johnna_endure 2021. 3. 13. 17:23

이번에는 route 타입 필터를 작성해서 간단하게 A/B 테스트를 구현해보겠습니다.

전체 코드는 맨 아래 링크에서 확인 가능합니다.


시나리오

 

기존 member-service에서 /hello 엔드포인트가 버전업 됐습니다.

새로운 버전의 /hello를 고객들에게 직접 제공해 테스트해 보려고 합니다. 하지만 기존의 서비스를 한번에 새 버전으로 교체하는 건 위험부담이 크므로 기존 요청의 반만 새 버전으로 라우팅합니다.


라우팅 필터 작성

 

ABRoutingFilter

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper;
import org.springframework.util.MultilueMap;
...

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.*;

@Component
public class ABRoutingFilter extends ZuulFilter {

    @Autowired
    private ProxyRequestHelper helper;
    private final Logger logger = LoggerFactory.getLogger(ABRoutingFilter.class);


    @Override
    public String filterType() {
        return ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        return SIMPLE_HOST_ROUTING_FILTER_ORDER-1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        boolean isMemberService = context.get(SERVICE_ID_KEY).equals("member-service");

        return isMemberService;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        logger.info("run() 호출");

        int randomInt = generate0or1();
        //0이면 새로운 서비스로 라우팅
        if(randomInt == 0) {
            HttpClient client = HttpClients.createDefault();
            HttpGet request = new HttpGet("http://new-member-service:8080/hello");

            try {
                HttpResponse response = client.execute(request);
                LinkedMultiValueMap<String, String> responseHeader = new LinkedMultiValueMap<>();
                Arrays.stream(response.getAllHeaders()).forEach(h -> responseHeader.add(h.getName(), h.getValue()));

                helper.setResponse(response.getStatusLine().getStatusCode(), response.getEntity().getContent(), responseHeader);
                context.setRouteHost(null);
            } catch (IOException e) {
                e.printStackTrace();
                context.setRouteHost(null);
            }
        }

        return null;
    }


    private int generate0or1() {
        return new Random().nextInt(2);
    }
}

동적 라우팅의 로직은 간단합니다.

0과 1을 랜덤하게 반환하는 함수를 만들고 0일 경우에 새로운 서비스로 라우팅하도록 합니다.

 

위 코드에서 중요한 부분은 filterOrder(), run() 메서드와 ProxyRequestHelper 클래스입니다.

 

필터 순서를 정하기 위해서는 미리 존재하는 경로 필트들에 대해서 알아둬야 됩니다.

 

시동 클래스에 @EnableZuulProxy 를 시동 클래스에 적용했다면 2개의 라우트 필터가 이미 존재합니다.

  - RibbonRoutingFilter : 라우팅을 위해 리본, 히스트릭스 등을 사용합니다.

  - SimpleHostRoutingFilter : 정해진 URL로 요청을 보냅니다.

 

SimpleHostRoutingFilter에서 마지막으로 요청을 보내기 때문에 동적 라우팅을 위한 필터는 SimpleHostRoutingFilter 보다 순서가 앞서야합니다. 그리고 동적 라우팅에 성공했다면 SimpleHostRoutingFilter 필터를 건너뛰고 (원래 매핑된 경로로 라우팅 방지) 받은 응답을 주울에 저장한 뒤 다음 페이즈로 넘어가야 됩니다.

 

받은 응답을 저장하기 위해서는 스프링 클라우드 주울에서 제공하는 헬퍼 클래스인 ProxyRequestHelper를 이용합니다.

helper.setResponse(response.getStatusLine().getStatusCode(),response.getEntity().getContent(), responseHeader);

SimpleHostRoutingFilter를 무시하기 위한 코드는 아래와 같습니다.

context.setRouteHost(null);

 

RequestContext.setRouteHost  메서드에 null을 전달했습니다.

그 이유는 SimpleHostRoutingFilter의 shouldFilter 메서드 구현을 보면 알 수 있습니다.

 

SimpleHostRoutingFilter

@Override
public boolean shouldFilter() {
    return RequestContext.getCurrentContext().getRouteHost() != null 
            && RequestContext.getCurrentContext().sendZuulResponse();
}

RouteHost가 null일 경우 위 조건을 회피할 수 있기 때문입니다.


전체 코드

 

 

johnna-endure/spring-cloud-study

Contribute to johnna-endure/spring-cloud-study development by creating an account on GitHub.

github.com

 

Comments