Spring Cloud Microservice
이번 글에서는, Spring Cloud Eureka 에 대해서 소개하며, Eureka 외에 Hystrix, Ribbon, Feign 에 대한 내용도 함께 작성하였다. 스프링 클라우드 기술이 꽤 방대해서 이 글에서 모두 다루기에는 무리가 있다. 자세한 내용은 공식 레퍼런스를 참고하길 바란다.
스프링 프레임워크는 마이크로서비스 아키텍처를 위해서 'Spring Cloud' 라는 기술을 제공한다. 이 글에
서는 스프링 클라우드 기술의 핵심인 Spring Cloud Eureka 에 대해서 집중적으로 다룬다.
이 글에서는
개념적인 내용은 왠만해서는 (시간 관계상) 다 생략하겠다...
필자의 예전 글 중에서 관심주제가 있다면 읽어보길 바란다. (이상한 글도 있으니 알아서 잘 필터링 하길 바라며)
https://brunch.co.kr/@springboot/113
https://brunch.co.kr/@springboot/202
https://brunch.co.kr/@springboot/262
https://brunch.co.kr/@springboot/423
https://brunch.co.kr/@springboot/58
https://brunch.co.kr/@springboot/2
이 글을 읽기 전에, 알고 있으면 좋은 지식에 대해서 먼저 소개한다. (사실, 필자도 잘 모른다.)
자세한 내용은 쿨하게 생략한다.
로드밸런싱은 HTTP 요청 트래픽을 다수의 서버에 분산시켜주는 것을 의미한다. 로드밸런싱을 구현함으로서 처리량을 극대화할 수 있고, 응답시간을 최소화할 수 있다. 또한, 단일 서버의 과부하를 막을 수 있다. L4 와 같은 장비를 이용해서 로드밸런싱을 수행하였다. 자세한 내용은 생략...
API 게이트웨이를 활용해서 로드밸런싱 기능을 구축할 수 있다. API 게이트웨이는 로드밸런싱 외에 인증, 보안 등 다양한 용도로 사용한다.
https://www.baeldung.com/zuul-load-balancing
https://brunch.co.kr/@springboot/38
필자의 샘플코드는 아주 간단하다.
1)eureka-server : 유레카 서버
2)product-api : 상품 정보를 제공하는 API 서버이다. 유레카에 등록된다.
3)front-api : product-api 를 호출한 결과를 사용자에게 제공하는 프론트 API 서버이다. 유레카에 등록되지는 않지만, 유레카 서버에 연동되어 product-api 서버리스트를 가져와서 사용한다.
필자는 구글 클라우드 플랫폼(GCP) 환경에 구축하였다.
배포 방법에 대한 자세한 내용은 생략한다. 로컬 환경에서 테스트해도 상관 없다. 하지만, 이번 글은 MSA 환경에서의 "Discovery Pattern"을 제대로 설명하기 위해서 부득이하게 클라우드 환경에 배포해서 글을 작성하였다.
이 글의 핵심 내용인 Discovery Pattern 에 대해서 아주 간략하게 설명한다.
자세한 내용은 생략한다.
https://spring.io/microservices
L4, L7 을 사용해서 로드밸런서를 사용한다면 아래와 같이 시스템을 구축한다.
해당 아키텍처에서 'Front API' 는 'Product API' 를 호출하기 위해서 L4,L7 의 VIP 를 알고 있다. VIP 를 호출하면, L4,L7 에서 로드밸런싱으로 Product API 로 포워딩 하게 된다.
API 게이트웨이에서 로드밸런서 역할을 수행할 수 있다.
Spring Cloud Gateway 를 사용해서 API 게이트웨이를 구축할 수 있다.
마이크로서비스 아키텍처는 일반적으로 Cloud 환경에서 동작한다. 클라우드에 올라가는 인스턴스는 네트워크 위치가 동적으로 생성이 되는 경우가 많다. 또한, 인스턴스가 추가, 삭제, 오토스케일링 등의 이유로 IP 가 동적으로 변경될 수 있다. 이런 MSA 환경에서 'Discovery Pattern'을 활용하게 된다.
https://microservices.io/patterns/client-side-discovery.html
해당 블로그는 마이크로서비스 관련해서 거의 모든 내용을 소개하는 곳이다.
각각의 개별 인스턴스는 "서비스 레지스트리"에 자신(애플리케이션)을 등록해야 한다.
필자가 구현한 Product API 를 레지스트리에 등록하면 아래와 같다.
Product API 는 레지스트리에 "product-api" 라는 애플리케이션 이름으로 등록한다. 그리고, Front API 는 Product API 의 주소를 레지스트리를 통해서 알수 있다. Front API 에서 Product API 의 주소를 application 프로퍼티 파일에 미리 설정할 필요가 전혀 없다. 실시간으로 레지스트리에서 알수 있기 때문이다.
위와 같은 아키텍처는, 최종적으로 아래와 같은 시스템으로 구축된다.
이때, Ribbon 은 클라이언트 사이드 로드 밸런서 역할을 수행한다. L4 또는 API 게이트웨이에서 로드밸런싱을 해줬다면, Ribbon 은 클라이언트에서 직접 로드밸런싱을 한다. (즉, 클라이언트에서 알아서 분산 호출을 하게 된다.) 그리고, 이 글의 핵심 주제인 'Eureka 서버'는 레지스트리의 역할을 한다.
필자의 블로그에서는 시간관계상 간단한 내용만 설명하였는데, 해당 기술은 대부분 넷플릭스의 기술이다. 자세한 내용은 공식 레퍼런스를 참고하자.
https://cloud.spring.io/spring-cloud-netflix/reference/html/
이제 본격적으로 Eureka 시스템을 구축해보겠다.
먼저 유레카 서버를 구축한다.
유레카 서버 구축은 아래와 같이 디펜던시를 추가하고,
@EnableEurekaServer 어노테이션을 선언해주면 유레카 서버로 동작한다.
(참고로, @EnableEurekaServer 대신에 @EnableDiscoveryClient 어노테이션을 사용해도 된다.)
유레카 서버를 GCP에 8716 포트로 배포하였다.
유레카 서버는 잘 실행되었다.
하지만, 아직 애플리케이션이 등록되지 않았기 때문에, 아무것도 나오지 않을 것이다.
유레카에 애플리케이션을 등록해보자. 상품 정보를 제공하는 Product API 를 구축한다. 디펜던시에 eureka-client 를 추가한다.
중요한 점은, 프로퍼티에 반드시 애플리케이션 이름을 지정해야 한다. 해당 이름은 유레카에 등록되는 이름이기 때문에 매우 중요하다. 필자는 product-api 라는 이름으로 등록하였다.
유레카 서버 경로를 설정하고, 애플리케이션을 유레카에 등록하기 위해서 registerWithEureka 를 true 로 정의한다.
자!!
애플리케이션을 실행하면, 유레카 서버에 Product API 애플리케이션이 등록된 것을 확인할 수 있다.
필자는 Product-API 를 2대 배포하였다. 유레카에 product-api 라는 이름의 애플리케이션이 등록이 되었는데, 유레카 서버는 등록된 product-api 의 IP(경로) 및 서버리스트를 알고 있다. 그래서, 다른 API에서 유레카에 product-api 의 아이피 주소를 문의하면, 유레카 서버는 Product API 서버들의 IP 주소를 전부 알려준다. 다른API 는 product-api 의 아이피를 전달받았기 때문에, product-api 를 직접 호출할 수 있게 된다. 단, 이때 다른API(클라이언트, front-api) 측에서 로드 밸런싱을 직접 수행하게 된다. 다른API(클라이언트, front-api)는 product-api 의 ip 주소를 프로퍼티에 설정할 필요가 전혀 없다.
왜냐면, 유레카 서버가 친절하게 서버리스트를 모두 알려주기 때문이다.
Product API 는 매우 간단한 엔드포인트를 제공하는데, 4초의 지연시간이 발생하는 느린 API 이다.
이번에는 타겟 API(Product AP) 를 호출하는 클라이언트 입장에서의 Front API 를 개발해보자. 단, Product API 와는 다르게 유레카 서버에 애플리케이션으로 등록하지는 않을 것이다. 비록, Front API 는 Product API 서버리스트를 유레카를 통해서 받아와서 사용하지만, 굳이 애플리케이션으로 유레카 서버에 등록하지는 않는 것이다.
유레카 클라이언트 디펜던시를 추가하고, HTTP 통신을 하기 위해서 openfeign 디펜던시를 추가해준다.
이때 중요한 설정으로,
@FeignClient 어노테이션에, 유레카에 등록된 타겟 API의 애플리케이션 이름을 작성해준다.
단, 위에서 설명했지만, Front API 는 유레카에 애플리케이션으로 등록하지는 않는다. 즉, 유레카에 애플리케이션으로 등록하지 않기 때문에, 또다른 API 에서 유레카를 통해서 Front API 서버 정보를 가져올 수는 없다. registerWithEureka 설정을 false 로 해주면 된다. 이런 경우에 Front API 는 유레카 정보를 사용만 할 뿐, 자신의 정보를 다른 서버들에게 제공하지는 않게 된다.
유레카 클라이언트 디펜던시를 추가할때, 전부 애플리케이션으로 등록할 필요는 없는 것이다.
이때, Ribbon ReadTimeout 5초로 설정하였다. 상품 API 의 지연시간이 4초인데, 5초안에 응답하기 때문에 타임아웃 에러가 발생하지는 않는다. Feign 에서는 hystrix 를 사용하기 위해서 아래와 같이 Hystrix 설정을 추가한다.
지금까지의 설정으로
Eureka, Feign, Ribbon, Hystrix 를 모두 사용하는 마이크로서비스 시스템을 빠르게 구축하였다.
front-api 를 테스트하기 위해서 심플한 엔드포인트를 하나 만들고,
호출해보면 잘 된다.
서버 로그를 확인해보면,
front-api 에서는 유레카 서버에 연동함으로, 타겟 API 인 product-api 의 서버 리스트를 받게 된다.
product-api 는 2대이기 때문에, 기본적으로 라운드로빈 방식으로 로드밸런싱을 수행해서 HTTP 요청을 보낸다.
Product API 서버 중 한대를 내려보자. 2번 서버를 강제로 kill 한다. 유레카 서버에서는 product-api 서버가 다운되면 바로 알게된다. 유레카 서버에서 status DOWN 이라는 로그를 확인할 수 있다.
API 중 1대가 DOWN 되었기 때문에, product-api 는 이제 1대로 운영 중이다.
front-api 에서도 product-api 서버 중 1대가 DOWN 되었다는 사실을 유레카를 통해서 전달받게 된다. 그래서, front-api 에서는 product-api 서버 중 살아있는 서버에만 HTTP 요청을 할 수 있다.
다시 2번 서버를 살려보자. 유레카 서버는 2번 서버가 살아났다는 것을 바로 알아챈다.
이때, front-api 에서는 product-api 서버가 살아났다는 사실을 유레카를 통해서 알게 알게되고, 다시 정상적으로 로드밸런싱으로 번갈아가면서 HTTP 호출하게 된다.
자, 간단하게 마이크로서비스 아키텍처를 구축하였다.
모든 시스템이 이렇게 쉽게 구현이 되면 얼마나 좋을까?
하지만, 실무에서는 이렇게 간단하지 않다....
유레카 심화 내용에 대해서 알아보자. 여기서부터는 조금 어려워진다. 잘못된 내용은 꼭 제보해주길 바란다.
유레카 클라이언트 디펜던시가 추가된 애플리케이션에서 유레카를 사용하지 않는 환경에선 어떻게 해야할까?
이게 뭔 소리지?
상용 환경에서는 유레카를 사용한다. 하지만, 일반적으로 로컬 환경에서 개발할 때는 유레카 연동을 하지 않고 개발하는 경우가 많다. application-local.yml 파일에 의해서 로컬 프로파일로 동작하게 설정해보자.
application-local.yml 파일에 유레카 클라이언트 설정을 false 로 한다. 유레카를 사용하지 않기 때문에 반드시 타겟 서버 리스트를 명시해줘야 한다.
유레카를 사용하지 않기 때문에 유레카를 통해서 product-api 의 서버 정보를 가져올 수 없다. 그래서 반드시 서버 호출 정보를 위와 같이 명시해줘야 한다.
사실, Feign 클라이언트 클래스에서도 url 지정이 가능하지만, 이 글에서 자세한 내용은 생략하겠다.
listOfServers 에는 서버 리스트를 나열해서 작성할 수 있다. 아래와 같이 2대의 서버를 지정해주면, 로드밸런싱으로 호출한다.
서버:포트 의 포맷으로 입력해야 한다. url path 까지 입력하면 오류가 발생할 것이다.
loalhost:8081/api/ 이런식으로 입력하면 오류!!
만약, 라운드로빈 방식이 아니라 다른 방식의 로드밸런싱을 수행하고 싶다면....
잘 찾아보길 바란다. 추가 설정을 할 수 있다. 자세한 내용은 역시 생략한다.
만약, 유레카를 사용하는 환경(Eureka Enabled True)에서 아래와 같이 서버리스트를 작성하면 어떻게 될까?
이 경우에는 listOfServers 설정과 상관 없이 유레카에 등록된 서버리스트 정보를 사용한다. product-api 라는 이름으로 유레카에 등록이 되어있기 때문에, 이 경우에 listOfServers 에 설정한 값은 동작하지 않는다. 만약, 위와 같은 상황(유레카 사용 및 유레카에 애플리케이션이 등록된 상황)에서 유레카를 통해서 받아온 서버 정보를 사용하고 싶지 않다면... 즉, 강제로 서버 리스트를 사용하고 싶다면 아래와 같이 NIWSServerListClassName 를 ConfigurationBasedServerList 로 지정해줘야 한다.
위와 같이 설정하였다면, 유레카를 연동한 환경에서 로드밸런싱 서버리스트를 강제로 지정해서 로드밸런싱을 수행할 수 있다.
자... 지금까지 간단하게 Eureka 연동에 대해서 알아보았다. 사실 많이 어렵지는 않은 내용이지만 글로 이해하기는 쉽지 않다. 필자의 글이 불친절해서 더더욱 이해하기 어려울 것이다.
직접 구축해보길 바란다. 그럼 유레카 관련해서는, 이만 글을 마치겠다.
참고로, 아래 내용은 확실하지 않은 내용이라서 가볍게 참고만해주길 바란다. 스프링 버전에 따라서 다를 수 있다. 반드시 각자 많은 테스트를 해보길 바란다.
Timeout 에 대해서 더 상세하게 살펴보자. 조금 어려운 내용이라서, 너무 어렵다고 생각된다면 쿨하게 패스하길 바란다.
Eureka, Hystrix, Ribbon, Feign 를 사용하는 프로젝트에서 Timeout 설정이 잘못된 케이스를 종종 찾게 된다. 이 글을 읽는 개발자는 실무에서 사용하는 프로퍼티 설정을 반드시 점검해보길 바란다. 단, 필자의 글을 전적으로 믿지 말고, 직접 테스트하고 꼼꼼하게 검증해보길 바란다. 일부 속성값은 스프링부트 버전에 따라서 다르게 동작할 가능성이 있다.
Hystirx Timeout 설정은 쓰레드의 타임아웃 설정을 의미한다. 자세한 내용은...
https://github.com/Netflix/Hystrix/wiki/Configuration#executionisolationthreadtimeoutinmilliseconds
Hystrix 타임아웃을 1초로 설정해보자.
Ribbon Read Timeout 은 5초로 설정하였다.
API 를 호출하면 어떻게 될까? 현재 아래와 같은 상황이다.
Http Client 에서 5초 ReadTimeout 설정을 했지만, Hystrix 의 쓰레드 타임아웃은 1초로 설정되었다. 그래서, Http 요청을 1초만에 끊는다. 필자의 샘플 코드는 4초의 지연 시간을 설정하였기 때문에 아래와 같이 타임아웃 오류가 발생할 것이다.
Hystrix 타임아웃은 쓰레드의 타임아웃이다. Http Client ReadTimeout, ConnectTimeout 과 혼동하지 않기를 바란다. 그래서, 일반적으로, Hystrix 타임아웃 설정은 Ribbon 또는 Feign 의 ConnectionTimeout + ReadTimeout 시간보다 길게 설정하게 된다. 하지만, Hystrix 타임아웃을 너무 길게 잡으면, 쓰레드를 너무 오래 붙잡고 있는 상황이 발생할 수 있다. 즉, 쓰레드 개수가 부족한 상황이 발생할 수도 있다. 쉽지 않지만, 적절한 설정 값을 각자 시스템 환경에 맞게 셋팅해야한다.
그리고, Hystrix 를 사용하도록 설정했다면 반드시 Timeout 설정해주는게 좋을 듯 싶다. 이유는, Hystrix Timeout 디폴트 값이 1초로 잡혀있기 때문에, 응답이 느린 API 를 호출하는 경우에 쉽게 끊기는 현상이 발생할수 있기 때문이다.
필자가 제일 헷깔리는 내용이다. 해당 내용을 간단하게 정리해서 회사의 일부 팀원에게 전파하기는 했지만, 혹시라도 잘못된 내용이 있다면 꼭 알려주길 바란다.
자, 이제 Feign, Ribbon 설정을 아래와 같이 해보자.
Feign default 의 readTimeout을 3초, conectTimeout 을 100ms 로 설정하였다. Ribbon의 ReadTimeout 설정은 5초이다. 4초 지연이 있는 API 를 호출하게 되면 어떻게 될까?
아래와 같이 ReadTimeout 이 발생한다.
Feign , Ribbon 의 설정은 모두 HTTP Connection Timeout, ReadTimeout 를 의미한다. 위와 같이 Feign, Ribbon 둘 다 설정하게 되면, 둘 중에 어떤 값을 따르는 것인가?
방금 발생한 ReadTimeOut 은 명확하게 Feign ReadTimeout 에 의한 오류로 확인된다.
만약 아래와 같이 feign 의 connectionTime 설정을 주석처리하고, ReadTimeout 설정만 남겨두자.
이번에는 어떻게 될까? 각자, 정답을 생각해보길 바라며..
필자는 처음에, 당연히 오류가 발생할 것으로 생각했다.
이유는, Feign의 readTimeout 시간은 3초로 설정되어있으니깐...
하지만, 오류가 발생하지 않는다....뭐지... 짜증난다.
아래 설정으로 다시 테스트해보자.
필자는 feignClient 설정을 connectTimeout 100ms, readTimeout 3초로 설정 하였다.
spring cloud openfeign 의 FeignClientFactoryBean 소스 코드를 찾아보자.
해당 클래스를 살펴보면,
connectionTimeout, readTimeout 둘다 null이 아닌 경우의 조건식을 찾을 수 있다.
Feign default 의 설정의 ConnectionTimeout, ReadTimeout 설정을 둘 다 하게 된다면, Feign 의 설정을 사용자가 커스텀하게 프로퍼티에 설정한 값을 사용하게 된다. 둘 중에 하나라도 설정하지 않으면 Feign 의 디폴트 값이 사용하게 되는데 connection 10초, readtimeout 이 60초이다. 필자는 ReadTimeout, ConnectTimeout 모두 커스텀하게 설정했기 때문에 이 경우에는 디폴트 설정이 적용되지 않는다.
자 그리고... Ribbon 의 설정을 살펴보자. springframework.cloud.openfeign.ribbon 패키지의 LoadBalancerFeignClient 클래스를 보자.
execute 메서드를 보면, option 파라미터로 필자가 설정한 Feign 설정이 넘어온다는 것을 알 수 있다.
이때, Feign 디폴트 옵션과 비교하는 조건이 있다.
필자가 connectTimeout 100ms, readTimeout 3초로 설정했기 때문에 디폴트 옵션과 같지 않다. 즉, feign 디폴트인 10초, 60초 가 아니기 때문에 else 구문을 실행하고, else 구문에서 필자가 커스텀하게 설정한 Feign 설정을 최종 설정으로 셋팅하게 된다.
(즉, 필자의 커스텀한 Ribbon 설정을 사용하지 않는다는 의미다...!!!!)
만약, feign 설정을 따로 명시하지 않았거나 또는 디폴트 설정과 똑같이 적용했다면 어떻게 될까?
(참고로, Feign 설정의 connectTime, readTimeout 둘 중 하나만 명시해도 디폴트 설정으로 되기 때문에 같은 케이스이다.)
이 경우는 Feign 설정이 모두 디폴트 설정으로 셋팅이 되는데...
options == DEFAULT_OPTIONS 조건을 실행한다.
자 이경우에는!! Ribbon의 설정을 따르게 된다. 아래 설정에 의해서 ReadTimeout 5초로 적용된다.
추가로, Feign 설정을 default: 로 하지 않고, feign 네임을 직접 지정하면 어떻게 될까?
이 경우는 알아서 찾아보길 바란다. 필자는 정답을 알고 있지만... 이정도는 직접 해보길 바라며..
사실, 더 많은 내용이 있지만 글이 너무 길어져서... 그만 써야겠다.
여기까지 읽고 이해한 개발자가 있다면 정말 대단하다. 블로그에 디버깅했던 내용을 올렸을 때 이해하는 개발자가 거의 없었다. 주저리주저리 글이 너무 길어진 것 같다. 이제 그만하자.
이정도로 대충 마무리를 해야겠다.
소중한 주말 토요일 시간을 모두 투자해서 빠르게 글을 작성해봤다. 사실 필자는, 스프링 클라우드 인프라를 직접 운영할 기회가 없어서, 스프링 클라우드에 대한 관심이 높지는 않다. 앞으로도, 스프링 클라우드를 상세하게 검토할 기회는 당분간 없을 것 같지만, 혹시라도 나중에 기회가 된다면 좀 더 재미있는 글을 남기도록 하겠다.
이 글이 조금이라도 도움이 되었길 바라며... 이만 글을 마치도록 하겠다. 끝...
https://spring.io/microservices
https://d2.naver.com/helloworld/3963776
https://d2.naver.com/helloworld/7225347
https://brunch.co.kr/@springboot/202
https://brunch.co.kr/@springboot/262
https://www.youtube.com/watch?v=J-VP0WFEQsY
https://woowabros.github.io/experience/2019/05/29/feign.html