ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 4. 그레이들 멀티프로젝트 - 유레카서버와 페인클라이언트 Eureka FeignClient
    스프링개발자/301 - 아키텍처 2020. 8. 31. 03:31

    [배경]

    여러 어플리케이션간의 호출을 하는 방법은 여러가지가 있다. 가장 기본은 RestTemplate를 이용하는 방법이지만, 단점이 있다. 그에 해결책으로 쓸 수 있는 Eureka Server와 FeignClient의 조합에 대해 알아보자.

     

    다음은 유레카 서버에 대한 설명

    Eureka Server is an application that holds the information
    about all client-service applications. Every Micro service will register 
    into the Eureka server and Eureka server knows all the client applications 
    running on each port and IP address. Eureka Server is also known as Discovery Server.
    
    유레카 서버는 어플리케이션인데, 클라이언트 어플리케이션들의 정보들을 담고 있다.
    각각의 마이크로서비스가 유레가 서버에 등록을 하면, 유레카 서버는 포트넘버, IP주소 등의 
    등록된 클라이언트 어플들의 정보를 알수 있다. 
    유레카 서버는 디스커버리 서버 라고도 불린다.
    

     

     

    다음은 페인 클라이언트에 대한 설명

    FeignClient is a library for creating REST API clients in a declarative way. 
    So, instead of manually coding clients for remote API and maybe using Springs 
    RestTemplate we declare a client definition and the rest is generated during runtime for use.
    
    페인클라이언트는 REST API 클라이언트를 `선언적인`방법으로 구현하는 하나의 라이브러리이다.
    RestTemplate을 이용하는 대신에 client를 문법적으로 정의내리고 rumtime시 구현체가 생성된다.

    RestTemplate보다 안정성 있고 확장성 있게 호출할 수 있다.


    1. 유레카 어플리케이션 만들기

    이전 글(https://2ndprince.tistory.com/45) 에서와 마찬가지로, 추가 모듈을 만들어 어플리케이션을 만든다.

    Settings.gradle에 추가하는 것도 잊지말자. 유레카 어플의 build.gradle에 유레카 의존성을 추가한다.

    루트폴더에 있는 build.gradle에서 옮긴다.

    bootRun {
        enabled = true
        main = 'com.example.monorepo.EurekaServerApplication'
    }
    
    jar {
        enabled = false
    }
    
    bootJar {
        enabled = true
    }
    
    dependencies {
        compile project(":spring-resources:shared-entities")
        implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
    }
    server.port=8761
    spring.application.name=eureka-application

    유레카 서버가 8761 포트에 실행되는걸 확인하자. 그리고나서 @EnableEurekaServer 어노테이션을 메인클래스에 추가한다.

    package com.example.monorepo;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
    
    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaServerApplication {
        public static void main(String[] args) {
            SpringApplication.run(EurekaServerApplication.class, args);
        }
    }
    

    유레카 application.properties에 다음의 두가지를 추가한다. 유레카 서버는 끄고, 클라이언트들이 켜야하는 옵션들이다.

    eureka.client.register-with-eureka=false
    eureka.client.fetch-registry=false

    localhost:8761에 들어가서 유레카 dashboard가 뜬 것을 확인하자. 중간 즈음에 보면, 등록된 instances도 아직은 없다.

     

    이렇게 하면 유레카 서버는 설정되었고, 나머지 두가지의 어플(main, weather)를 유레카 클라이언트로 등록하자.

     

    2. 유레카 클라이언트 등록

     

    유레카 클라이언트 의존성을 추가한다. 루트폴더의 build.gradle이다. (mvnrepository 이용)

    compile group: 'org.springframework.cloud', 
    name: 'spring-cloud-starter-netflix-eureka-client', version: '2.2.5.RELEASE'

    클라이언트 의존성이 이미 추가되어서, 별다른 셋팅 없이도, 유레카 서버가 아닌 어플들은 모두 클라이언트로 등록된다.

    그래도 정확하게 명시하기 위해, 클라이언트 어플들에 다음의 라인을 추가한다. application.properties파일

    eureka.client.fetch-registry=true

    이후에 다시 한번 localhost:8761에 들어가보면

    클라이언트 어플들이 등록되있다. 다만 호스트 주소가 localhost가 아니라 docker 인데, 이건 꽤나 골치아픈 버그이다.

    다른 분들도 겪고 있는데, 해결책이 분명하지 않아서 좀 더 알아봐야겠다. 일단 자료 남기기.(https://stackoverflow.com/questions/57319678/spring-boot-cloud-eurka-windows-10-eurkea-returns-host-docker-internal-for-clien)

     

     

    3. FeignClient vs RestTemplate

    어플리케이션간의 호출을 구현하는 두가지 방법이 있다.

    첫번째 아래의 그림은 RestTemplate으로 구현한 호출이다.

    비교적 간단하게 구현할 수 있다. (구현방법은 이전 글을 참고하자.)

    두번째 방법은 Eureka server와 FeignClient 를 이용하는 방법이다.

    조금 더 복잡하지만 다음과 같은 장점을 갖는다.

    • Declarative: 프로그램적으로 원하는 타입을 명시할 수 있다
    • 단순한 interface이다
    • Load-balancing 을 제공한다
    • CRUD을 쉽게 확장 구현할 수 있다.

    결론적으로, 호출이라는 불확실한 행위를 프로그램적으로 안정성 있게 구현하고 효과적으로 유지보수 할 수 있다.

     

    4. 의존성 추가 (Prerequisite)

    Eureka와 FeignClient dependency를 추가한다.

    <root build.gradle>
    compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-netflix-eureka-client', version: '2.2.5.RELEASE'
    compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '2.2.5.RELEASE'

    다음의 오류가 날 수 있다.

    오류메세지를 읽어보면, 해결방법은 이렇다.

    springBoot {
        mainClassName = 'com/example/monorepo/MainApplication.java'
    }

     

    다음의 오류가 추가적으로 날 수 있다.

    ***************************
    APPLICATION FAILED TO START
    ***************************
    
    Description:
    
    Parameter 0 of constructor in com.example.monorepo.PlanService required a bean of type 'com.example.monorepo.feign.FeignPlanClient' that could not be found.
    
    The injection point has the following annotations:
    	- @org.springframework.beans.factory.annotation.Autowired(required=true)
    
    
    Action:
    
    Consider defining a bean of type 'com.example.monorepo.feign.FeignPlanClient' in your configuration.

    오랜시간 디버깅 끝에 발견한 해결방법은; @EnableFeignClients 

    논리적으로 생각해보면, ComponentScan도 맞다.

    @SpringBootApplication
    @EnableFeignClients
    public class MainApplication {
        public static void main(String[] args) {
            SpringApplication.run(MainApplication.class, args);
        }
    }
    

     

    5. 드디어 FeignProvider와 FeignClient 구현

    다음의 두 클래스를 shared-entities 모듈에 추가한다.

     

    public interface FeignPlanProvider {
    
        @GetMapping(value ="/weather/forecast")
        List<Double> forecast(@RequestParam("days") int days);
    }
    

     

    @Component
    @FeignClient(name = "weather-application")
    public interface FeignPlanClient extends FeignPlanProvider {
    
    }

    provider가 있음에도 굳이 client를 따로 두는 이유는, 로직을 추가하기 쉽고, Single Responsibility Principle을 위함(provider와 client 는 역할이 다름)

     

    여기의 FeignClient가 RestTemplate보다 향상된 역할을 하는 바로 그 핵심 클래스이다.

     

    6. FeignController

    다음의 RestController를 weather-application에 추가한다

     

    @RestController
    @Slf4j
    public class FeignPlanController implements FeignPlanProvider {
    
        @Autowired
        ForecastService forecastService;
    
        @Override
        public List<Double> forecast(int days) {
            log.info("feign call works");
            return forecastService.predict(days);
        }
    }

     

    7. 테스트 해보기

    MainController에 다음의 endpoint를 추가한다.

    @GetMapping("/feign/weekly")
        public String makeWeeklyPlanUsingFeign(){
            List<Boolean> booleans = planService.canGoOutsideForWeekUsingFeign(0.4);
            return booleans.toString();
        }

    PlanService의 다음의 메소드를 추가한다.

    public List<Boolean> canGoOutsideForWeekUsingFeign(double threshold){
            List<Double> forecast = feignPlanClient.forecast(7);
            ArrayList<Boolean> yesno = new ArrayList<>();
            for (Double d : forecast) {
                if (d < threshold) {
                    yesno.add(true);
                } else {
                    yesno.add(false);
                }
            }
            return yesno;
        }

    3개의 어플리케이션(Eureka, Main, Weather)를 모두 동작하고

    http://localhost:8080/main/feign/weekly 를 get해보자.

    이전글의 RestTemplate과 같은 결과값이 나온다.

    [true, false, true, false, false, false, true]

    WeatherApplication의 Console에서 우리가 입력한 다음의 로그 메세지(feign call works)를 확인하자.

    2020-09-26 00:07:39.325  INFO 11748 --- [nio-8081-exec-1] c.example.monorepo.FeignPlanController   : feign call works

    Endpoint URL를 일절 입력하지 않고도, FeignClient를 이용해서 어플리케이션간의 호출을 할 수 있다.

    URL를 입력하지 않았지만, 유레카가 어플리케이션의 이름을 이용해서 서로 호출한다.

    FeignClient의 name은, application.properties의 spring.application.name과 일치해야 한다.

     

    전반적인 내용참고:

    cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html

     

    완성된 소스코드는 여기에

    github.com/2ndPrince/monorepo/tree/eureka


    [TroubleShooting]

    1. 모듈 생성이 spring-applications subdirectory에 안되면, root directory에 만들고, 옮긴다. src.main.java등의 패키지 경로가 자동으로 안생겨서, manually 생성해줬다. Intellij가 어느정도는 도와줬다.

    2. 어플리케이션 이름을 우리는 이미 명시했어서 추가 설명이 없었지만, 유레카 서버와 클라이언트는, 어플 이름으로 서로를 구분하니까, 꼭 이름을 직접 설정하자.

     

    추가자료

    https://exampledriven.wordpress.com/2016/07/01/spring-cloud-eureka-ribbon-feign-example/

     

    3. Feign call은 post형태이고 RequestParam 혹은 RequestBody을 붙여야한다. 다음의 오류가 꽤나 속을 썩였다. 

    WeatherApp의 console을 보니 post호출이 지원되지 않는다고 메세지가 떴는데, 내 코드 어디에도 postMapping은 구현한 적이 없었기에 햇갈렸다.

    feign.FeignException$MethodNotAllowed: 
    [405] during [GET] to [http://weather-application/weather/forecast] 
    [FeignPlanClient#forecast(int)]: 
    [{"timestamp":"2020-09-26T03:56:34.902+00:00","status":405,"error":"Method Not Allowed",
    "message":"","path":"/weather/forecast"}]

    힌트 얻은 글:

    stackoverflow.com/questions/51711348/how-to-make-get-requests-to-other-feign-clients-inside-of-a-post-request

     

    정리하면, feign method는 client간의 request의 파라미터 값을 body로 전송하기 때문에, 아래의 forecast method가 post형태로 호출되고, body로 메소드 인자가 들어가야 된다. 결과적으로: @RequestParam("days")를 붙여야 함

    public interface FeignPlanProvider {
    
        @GetMapping(value ="/weather/forecast")
        List<Double> forecast(@RequestParam("days") int days);
    }
    

     

    여기서 잠깐.

    GET과 POST의 차이점은 알고 넘어가는게 좋다.

    Both GET and POST method is used to transfer data from client to server 
    in HTTP protocol but Main difference between POST and GET method is 
    that GET carries request parameter appended in URL string while POST 
    carries request parameter in message body which makes it more secure way 
    of transferring data from client to server in http protocol.
    
    GET과 POST 모두 클라이언트에서 서버로 데이터를 전송하는데 사용되는 http protocol의 종류이다.
    하지만 GET은 request parameter를 url의 일부로 전송하며, 이는 안정적이지 않다.
    반면에 POST는 request parameter를 message body의 형태로 전송하여 안전하다.
    
    결론: 전송하는 방식차이에 따른 안정성 문제

     

Designed by Tistory.