본문 바로가기
공부/프로그래밍

[springboot] feign 설정하기

by demonic_ 2021. 7. 26.
반응형

서비스별로 분리하면서 REST API 형태로 주고받으려 하는데 이전 같았으면 RestTemplate나 Retrofit2를 사용했을 텐데 연동때마다 사용될 코드량이 너무 많아 Feign을 사용하기로 했다. MSA 구조에서도 자주 사용하기에 이참에 정리해 두려고 한다.

 

의존성은 다음을 추가한다(gradle 기준)

dependencies {
    // feign
    implementation "org.springframework.cloud:spring-cloud-starter-openfeign"
}

...
dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:2020.0.3"
    }
}
...

 

참고로 버전이 맞지 않을경우 다음과 같은 에러가 발생한다

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'configurationPropertiesBeans' defined in class path resource [org/springframework/cloud/autoconfigure/ConfigurationPropertiesRebinderAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cloud.context.properties.ConfigurationPropertiesBeans]: Factory method 'configurationPropertiesBeans' threw exception; nested exception is java.lang.NoClassDefFoundError: org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata
at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:658) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:486) ~[spring-beans-5.3.8.jar:5.3.8]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1334) ~[spring-beans-5.3.8.jar:5.3.8]

 

버전에 관한 정보는 다음 사이트에서 확인 가능하니 현재 사용중인 SpringBoot에 맞게 설정해주면 된다.

https://spring.io/projects/spring-cloud

 

Spring Cloud

Spring Cloud is an umbrella project consisting of independent projects with, in principle, different release cadences. To manage the portfolio a BOM (Bill of Materials) is published with a curated set of dependencies on the individual project. Go here to r

spring.io

 

이제 설정을 해보자.

@EnabelFeignClients 를설정해야 하는데, Application 에다 주로 한다.

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

 

내 경우 추가설정을 위해 한곳에 몰아 넣으려고 다른 파일에 넣곤 하는데 다음처럼 한다.

다만 이럴경우 패키지 경로를 지정해주어야 한다.

@Configuration
@EnableFeignClients(basePackages = "[패키지경로]")
public class FeignConfiguration {
...
}

 

@SpringBootApplication 이 있는 위치는 대부분 최상단에 있기 때문에 그 아래에 위치한 모든것을 스캔하는데 용이하다. @EnableFeignClients 역시 마찬가지다. 그런데 위치가 바뀌게 되면 하위메뉴에 설정할 파일을 넣지 못할 수 있기 때문에 직접 패키지 경로를 설정하여 이를 해결하는 것이다.

 

위 설정엔 아직 아무런 추가설정을 하지 않은 상태다.

 

그럼 이제 클라이언트를 만들어보자. RestTemplate 나 Retrofit를 사용한다면 URL, 헤더, 파라미터, Body 구성 등을 하나하나 해줘야 하는데 Feign을 사용할 경우 interface로 간단히 구현할 수 있다.

 

설정은 다음과 같다.

@FeignClient(name = "user", url = "https://user.custommdomain.co.kr")
public interface UserClient {

    @GetMapping(value = "/api/user")
    ResponseUser getUser();
}

 

Content-type의 기본은 application/json 이다. 그래서 동일한 것을 사용하면 추가로 설정할 필요가 없다.

리턴받는 ResponseUser는 다음과 같다

@Getter
@ToString
public class ResponseUser {
    private Long id;
    private String username;
    private int age;
}

그럼 getUser 메서드를 호출해보자.

@RestController
@RequiredArgsConstructor
public class TestController {
    private final UserClient userClient;

    @GetMapping("/api/demonic/user")
    public List<ResponseUser> getUser() {
        List<ResponseUser> user = userClient.getUser();

        System.out.println("user = " + user);

        return user;
    }
}

 

getUser를 호출하면 다음과 같은 결과를 얻을 수 있다.

user = [ResponseUser(id=1, username=유저1, age=29), ResponseUser(id=2, username=유저2, age=20)]

 

 

# 로그설정

통신하는 로그를 자세히 보고 싶다면 다음처럼 설정하면 된다.

feign.client.config.default.logger-level = FULL

logging.level.[클라이언트가 위치한 패키지 full path]=DEBUG
예) com.demonic.adaptor 패키지에 클라이언트에 모여있는 경우
=> logging.level.com.demonic.adaptor=DEBUG

# 만약 특정 client만 설정하고 싶다면 다음처럼 하면 된다.
## 여기서 클라이언트 ID란 @FeignClient 를 등록할때 쓴 name의 값이다
feign.client.config.[클라이언트 ID].logger-level = FULL

 

그리고 다시 호출해보면 다음처럼 로그가 쌓여있다.

메서드, 헤더, 파라미터, 결과 등 모두 찍히는 걸 알 수 있다.

UserClient     : [UserClient#getUser] ---> GET http://localhost:10000/api/user HTTP/1.1
UserClient     : [UserClient#getUser] ---> END HTTP (0-byte body)
UserClient     : [UserClient#getUser] <--- HTTP/1.1 200  (43ms)
UserClient     : [UserClient#getUser] cache-control: no-cache, no-store, max-age=0, must-revalidate
UserClient     : [UserClient#getUser] connection: keep-alive
UserClient     : [UserClient#getUser] content-type: application/json
UserClient     : [UserClient#getUser] date: Fri, 23 Jul 2021 11:12:53 GMT
UserClient     : [UserClient#getUser] expires: 0
UserClient     : [UserClient#getUser] keep-alive: timeout=60
UserClient     : [UserClient#getUser] pragma: no-cache
UserClient     : [UserClient#getUser] transfer-encoding: chunked
UserClient     : [UserClient#getUser] x-content-type-options: nosniff
UserClient     : [UserClient#getUser] x-frame-options: DENY
UserClient     : [UserClient#getUser] x-xss-protection: 1; mode=block
UserClient     : [UserClient#getUser] 
UserClient     : [UserClient#getUser] user = [ResponseUser(id=1, username=유저1, age=29), ResponseUser(id=2, username=유저2, age=20)]
UserClient     : [UserClient#getUser] <--- END HTTP (65-byte body)

 

그렇다면 이번에는 POST로 호출해보자.

다음처럼 메서드를 추가한다

@FeignClient(name = "user", url = "https://user.custommdomain.co.kr")
public interface UserClient {

    @GetMapping(value = "/api/user")
    ResponseUser getUser();

    // 추가
    @PostMapping("/api/user")
    Long registUser(RequestPostTestUser request);
}

호출하는 RequestPostTestUser의 구성은 다음과 같다

@Getter
public class RequestPostTestUser {
    private String username;

    private int age;

    public RequestPostTestUser(String username, int age) {
        this.username = username;
        this.age = age;
    }
}

그리고 호출부분을 다음처럼 만든다.

...
    @PostMapping("/api/demonic/user")
    public Long registUser() {
        RequestPostTestUser user = new RequestPostTestUser("유저3", 3);
        Long id = userClient.registUser(user);

        System.out.println("id = " + id);

        return id;
    }
...

실행결과 다음과 같다.

[UserClient#registUser] ---> POST http://localhost:10000/api/user HTTP/1.1
[UserClient#registUser] Content-Length: 30
[UserClient#registUser] Content-Type: application/json
[UserClient#registUser] 
[UserClient#registUser] {"username":"유저3","age":3}
[UserClient#registUser] ---> END HTTP (30-byte body)
[UserClient#registUser] <--- HTTP/1.1 200  (3ms)
[UserClient#registUser] cache-control: no-cache, no-store, max-age=0, must-revalidate
[UserClient#registUser] connection: keep-alive
[UserClient#registUser] content-type: application/json
[UserClient#registUser] date: Fri, 23 Jul 2021 11:33:46 GMT
[UserClient#registUser] expires: 0
[UserClient#registUser] keep-alive: timeout=60
[UserClient#registUser] pragma: no-cache
[UserClient#registUser] transfer-encoding: chunked
[UserClient#registUser] x-content-type-options: nosniff
[UserClient#registUser] x-frame-options: DENY
[UserClient#registUser] x-xss-protection: 1; mode=block
[UserClient#registUser] 
[UserClient#registUser] 3
[UserClient#registUser] <--- END HTTP (1-byte body)
id = 3

 

id 을 url에 추가하여 호출해보자

@FeignClient(name = "user", url = "https://user.custommdomain.co.kr")
public interface UserClient {

    @GetMapping(value = "/api/user")
    ResponseUser getUser();

    @PostMapping("/api/user")
    Long registUser(RequestPostTestUser request);

    // 추가
    @GetMapping("/api/user/{id}")
    ResponseUser getUserById(@PathVariable(value = "id") Long id);
}

 

호출하는 곳을 다음처럼 수정한다.

...
    @GetMapping("/api/demonic/user/id")
    public ResponseUser getUserById() {
        ResponseUser findUser = userClient.getUserById(3l);
        System.out.println("findUser = " + findUser);
        return findUser;
    }
...

 

결과

[UserClient#getUserById] ---> GET http://localhost:10000/api/user/3 HTTP/1.1
[UserClient#getUserById] ---> END HTTP (0-byte body)
[UserClient#getUserById] <--- HTTP/1.1 200  (62ms)
[UserClient#getUserById] cache-control: no-cache, no-store, max-age=0, must-revalidate
[UserClient#getUserById] connection: keep-alive
[UserClient#getUserById] content-type: application/json
[UserClient#getUserById] date: Fri, 23 Jul 2021 11:42:32 GMT
[UserClient#getUserById] expires: 0
[UserClient#getUserById] keep-alive: timeout=60
[UserClient#getUserById] pragma: no-cache
[UserClient#getUserById] transfer-encoding: chunked
[UserClient#getUserById] x-content-type-options: nosniff
[UserClient#getUserById] x-frame-options: DENY
[UserClient#getUserById] x-xss-protection: 1; mode=block
[UserClient#getUserById] 
[UserClient#getUserById] {"id":3,"username":"유저3","age":25}
[UserClient#getUserById] <--- END HTTP (38-byte body)
findUser = ResponseUser(id=3, username=유저3, age=25)

 

한가지 주의할점이 있다면 @PathVariable을 사용할 땐 value 값(위에서는 id)를 꼭 지정해주어야 한다. 그렇지 않으면 구동시 아래와 같은 에러가 발생한다.

UserClient': Unexpected exception during bean creation; nested exception is java.lang.IllegalStateException: PathVariable annotation was empty on param 0.

 

 

 

# 인증(Basic Authorization) 추가

추가로 여기서 Basic 인증을 추가하고 싶다면 다음과 같이 설정하면 된다.

@Configuration
@EnableFeignClients(basePackages = "[패키지경로]")
public class FeignConfiguration {

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor(
            @Value("${oauth2.client_id}") String username
            , @Value("${oauth2.secret}") String password) {

        return new BasicAuthRequestInterceptor(username, password);
    }
}

basicAuthRequestInterceptor 를 추가하면 호출할때마다 Basic 인증에 필요한 값을 넣는다. 다음 호출로그를 보면 Authorization이 추가된 것을 확인할 수 있다.

[UserClient#getUserById] ---> GET http://localhost:10000/api/user/3 HTTP/1.1
# 이 아래 부분에 추가된 것을 확인할 수 있다.
[UserClient#getUserById] Authorization: Basic ZWRpeWFPYXV0aDJTZXJ2aWAjJFNlY3JldEtleQ==
[UserClient#getUserById] ---> END HTTP (0-byte body)
[UserClient#getUserById] <--- HTTP/1.1 200  (3ms)
[UserClient#getUserById] cache-control: no-cache, no-store, max-age=0, must-revalidate
[UserClient#getUserById] connection: keep-alive
[UserClient#getUserById] content-type: application/json
[UserClient#getUserById] date: Fri, 23 Jul 2021 11:46:02 GMT
[UserClient#getUserById] expires: 0
[UserClient#getUserById] keep-alive: timeout=60
[UserClient#getUserById] pragma: no-cache
[UserClient#getUserById] transfer-encoding: chunked
[UserClient#getUserById] x-content-type-options: nosniff
[UserClient#getUserById] x-frame-options: DENY
[UserClient#getUserById] x-xss-protection: 1; mode=block
[UserClient#getUserById] 
[UserClient#getUserById] {"id":3,"username":"유저3","age":25}
[UserClient#getUserById] <--- END HTTP (38-byte body)
findUser = ResponseUser(id=3, username=유저3, age=25)

 

 

 

끝.

반응형

댓글