Spring Framework/☁️ Spring Cloud

[Spring Cloud] Microservices간의 통신 - RestTemplate & FeignClient

soogoori 2023. 12. 7. 15:05

 

 

 

Microservices간의 통신하기 

 

클라이언트가 user-service에서 userId를 사용하여 사용자에 대한 정보를 요청했을 시 사용자의 주문이력까지 가져와야하는 상황을 가정해보면, 요청했던 userId 값을 order-service의 파라미터로 전달해서 엔드포인트를 호출해야 주문이력을 가져올 수 있다. 

 

RestTemplateFeignClient를 이용해 해당하는 정보를 가져와보자. 

 

 

RestTemplate 

RestTemplate은 http 통신을 유용하게 할 수 있도록 스프링에서 제공하는 템플릿이다. 

Spring 5.0 이후부터 레거시 라이브러리로 간주되었다. 

 

1️⃣ user-service에 RestTemplate을 Spring Bean으로 등록

@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserServiceApplication.class, args);
	}

	@Bean
	@LoadBalanced // 주소가 아닌 MicroService name 으로 호출
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}
}

 

2️⃣ user-service.yml 설정 변경 

order_service:
  url: http://ORDER-SERVICE/order-service/%s/orders
  exception:
    orders_is_empty: User's order is empty

 

3️⃣ UserServiceImpl 

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{

    private final UserRepository userRepository;
    private final RestTemplate restTemplate;
    private final Environment env;
    
    @Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if(userEntity== null){
            throw new UsernameNotFoundException("User not found");
        }

        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);

        /* Using RestTemplate */
        String orderUrl = String.format(env.getProperty("order_service.url"), userId);

        ResponseEntity<List<ResponseOrder>> orderListResponse =
                restTemplate.exchange(orderUrl, HttpMethod.GET, null,
                                        new ParameterizedTypeReference<List<ResponseOrder>>() {
                });

        List<ResponseOrder> orderList = orderListResponse.getBody();
        userDto.setOrders(orderList);

        return userDto;
    }
}

 

 

 

 

FeignClient 

FeignClient는 Netflix에서 개발한 Http Client로, 인터페이스를 작성하고 어노테이션을 통해 Http Client를 구현할 수 있다. 

 

 

1️⃣ dependencies 추가

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

 

 

2️⃣ application 파일에 @EnableFeignClients 추가

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserServiceApplication.class, args);
	}

	@Bean
	@LoadBalanced // 주소가 아닌 MicroService name 으로 호출
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}
}

 

SpringBootApplication이 실행될 때 @FeignClient 어노테이션이 붙은 파일들을 읽고 구현체를 만들어야므로 어노테이션을 읽을 수 있게 @EnableFeignClients 어노테이션을 UserServiceApplication 파일에 붙여준다. 

 

 

3️⃣ @FeignClient 사용해서 OrderServiceClient 구현

@FeignClient(name = "order-service")
public interface OrderServiceClient {

    @GetMapping("/order-service/{userId}/orders")
    List<ResponseOrder> getOrders(@PathVariable String userId);
}
  • @FeignClient에 호출하고자 하는 microservice 이름을 지정한다. ➔ Eureka server에 인스턴스로 등록 시 사용된 이름

 

4️⃣ OrderServiceClient 주입받고 UserServiceImpl 구현 

@Sl4fj
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{

    private final OrderServiceClient orderServiceClient; // FeignClient 인터페이스 주입

    @Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if(userEntity== null){
            throw new UsernameNotFoundException("User not found");
        }

        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);

        /* Using FeignClient */
        /* Feign Exception Handling */
        List<ResponseOrder> orderList = null;
        try {
            orderList = orderServiceClient.getOrders(userId);
        } catch (FeignException ex) {
            log.error(ex.getMessage());
        }
        
        userDto.setOrders(orderList);

        return userDto;
    }
}

 

 

예외처리  - ErrorDecoder

@Component
@RequiredArgsConstructor
public class FeignErrorDecoder implements ErrorDecoder {

    private final Environment env;
    
    @Override
    public Exception decode(String methodKey, Response response) {

        switch (response.status()) {
            case 400:
                break;
            case 404:
                if (methodKey.contains("getOrders")) {
                    return new ResponseStatusException(HttpStatus.valueOf(response.status()),
                            env.getProperty("order_service.exception.orders_is_empty"));
                }
                break;
            default:
                return new Exception(response.reason());
        }
        return null;
    }
}

 

첫번째 매개변수인 methodKey에 FeignClient를 통해 호출한 함수의 이름을 포함하면 해당하는 함수가 호출됐을 때 알맞은 Exception 객체를 만들어서 반환한다. 

 

@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{

    private final UserRepository userRepository;
    private final OrderServiceClient orderServiceClient;
    @Override
    public UserDto getUserByUserId(String userId) {
        UserEntity userEntity = userRepository.findByUserId(userId);

        if(userEntity== null){
            throw new UsernameNotFoundException("User not found");
        }

        UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);

        /* ErrorDecoder */
        List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);
        userDto.setOrders(orderList);

        return userDto;
    }
}

 

ErrorDecoder를 구현하면 try-catch문을 사용하지 않고 간단하게 코드를 구현할 수 있다. 

 

 

 

참고

https://techblog.woowahan.com/2630/

https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/