[Spring Cloud] Microservice(MSA) 구축 : (6) MSA간의 통신, RestTemplate과 FeignClient 사용법
이전 포스팅에서는 Microservice들의 설정을 일괄적으로 할 수 있는 Config Server를 구축해 보았었다.
이번에는 분산된 Microsevice들 간의 통신에 대해서 다루어 보려고 한다.
MSA 간의 통신의 필요성
다음 마이크로서비스의 경우를 생각해보자.
만약 User를 ID를 기반으로 조회할 때, 해당 유저가 주문한 주문내역을 함께 응답으로 주고 싶다고 생각해보자.
이럴 경우, Order Service와의 통신이 이루어져야 한다.
전통적인 Monolithic 아키텍쳐는 단일 데이터베이스 아래에서 외래키 참조와 Join을 통해 다른 도메인의 데이터를 가져오곤 했었다.
그렇지만 MSA라는 개념이 도입된 이후로, 같은 서비스 내에서 참조 가능한 데이터는 외래키와 Join을 통해서 가져오면 되지만,
데이터베이스와 서비스가 분산되어있다면 Join에 집착하지 않고 MSA간의 통신을 기반으로 데이터를 가져오는 방향을 선호한다고 한다. 데이터의 일관성 또한, 데이터베이스 Relation보다는 마이크로서비스간의 통신과 동기화에 의존하게 되는 경향이 있는 것 같다.
RestTemplate 사용
RestTemplate는 HTTP 통신을 기반으로 다른 MSA에 요청을 날리고, 얻어온 응답을 활용하는 방식이다.
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced //Spring Cloud Namespace로 접근 가능하도록 하기 위해서
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
RestTemplate 사용을 위해 Spring Bean으로 등록해주자.
@LoadBalanced 어노테이션은 요청 URL에 Spring Cloud Eureka 서버의 Name을 통해서 조회 가능하도록 하기 위해서 설정해주자. (IP와 포트를 항상 신경쓰지 않아도 된다.)
▶ application.yml
order_service:
url: http://order-service/order-service/%s/orders
exception:
orders_is_empty: User's orders is empty.
▶ UserServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final Environment env;
private final RestTemplate restTemplate;
/* Rest Template을 사용한 MSA간 통신 */
@Override
public UserDto getUserByUserIdUseRestTemplate(String userId) {
UserEntity userEntity = userRepository.findByUserId(userId);
if(userEntity == null){
throw new UsernameNotFoundException("User Not Found");
}
UserDto dto = new ModelMapper().map(userEntity, UserDto.class);
String orderUrl = String.format(this.env.getProperty("order_service.url"), userId);
ResponseEntity<List<ResponseOrder>> orderListReponse =
restTemplate.exchange(orderUrl, HttpMethod.GET, null,
new ParameterizedTypeReference<List<ResponseOrder>>() {
});
List<ResponseOrder> orderList = orderListReponse.getBody();
dto.setOrders(orderList);
return dto;
}
}
Service에서는 RestTemplate을 주입받아 사용한다.
restTemplate.exchange에는, URL, Request Method, 응답을 바인딩할 반환받을 타입을 지정하여 사용한다.
이를 통해서 orderListResponse에는 HTTP 요청을 통한 응답값이 들어오게 된다.
RestTemplate을 사용하여 다른 MSA의 Order에 대한 정보를 DTO에 추가하여 응답해줄 수 있게 되었다.
▶ OrderController.java (다른 Microservice인 Order-Service)
@RestController
@RequestMapping("/order-service")
@AllArgsConstructor
public class OrderController {
private final Environment env;
private final OrderService orderService;
//...
@GetMapping("/{userId}/orders")
public ResponseEntity<List<ResponseOrder>> getOrderByUser(@PathVariable("userId") String userId){
Iterable<OrderEntity> orderList = orderService.getOrdersByUserId(userId);
List<ResponseOrder> result = new ArrayList<>();
ModelMapper mapper = new ModelMapper();
orderList.forEach(x -> {
result.add(mapper.map(x, ResponseOrder.class));
});
return ResponseEntity.status(HttpStatus.OK).body(result);
}
}
참고로 외부 Microservice인 OrderService는 다음과 같은 컨트롤러를 가지고 있다.
FeignClient
앞의 RestTemplate은 HTTP 통신을 기반으로 한다고 하였다.
Feign은 서비스 간 통신을 구현할 수 있도록 도와주는 라이브러리이다. 내부적으로 HTTP 통신 코드를 자동으로 생성해준다.
인터페이스를 정의하고 메서드를 추가함으로써 RESTful 서비스 간의 통신을 직접 정의하지 않고 인터페이스의 형태로 사용할 수 있게 한다.
▶ pom.xml
<!-- Feign Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
▶ OrderServiceClient.java
import com.example.userservice.vo.ResponseOrder;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@FeignClient(name="order-service")
public interface OrderServiceClient {
@GetMapping("/order-service/{userId}/orders")
List<ResponseOrder> getOrders(@PathVariable String userId);
}
다른 MSA와의 통신을 담당하는 인터페이스를 작성해주었다.
@FeignClient 어노테이션을 통해 외부 서비스와의 통신을 추상화해주고 자동으로 HTTP 요청을 생성하도록 정의해주었다. name에는 다른 MSA의 이름을 적어준다.(Service Discovery에 등록된 이름)
컨트롤러를 작성하는 것과 유사하게, GET으로 order-service의 order-service/{userId}/orders로 요청을 보낼 것이라는 것을 정의해주고, 응답에 해당하는 데이터 타입또한 반환형으로 정의해준다.
▶ UserServiceImpl.java
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final Environment env;
private final OrderServiceClient orderServiceClient;
/* Feign Client를 사용한 MSA간 통신 */
@Override
public UserDto getUserByUserId(String userId) {
UserEntity userEntity = userRepository.findByUserId(userId);
if(userEntity == null){
throw new UsernameNotFoundException("User Not Found");
}
UserDto dto = new ModelMapper().map(userEntity, UserDto.class);
List<ResponseOrder> orderList = null;
orderList = orderServiceClient.getOrders(userId);
dto.setOrders(orderList);
return dto;
}
}
위에서 정의한 인터페이스를 주입받은 후 비즈니스 로직에서 사용하고 있는 모습이다.
orderServiceClient.getOrders(userId)를 보면 알 수 있듯이 해당 인터페이스를 사용하여 함수를 호출하듯 사용할 수 있어졌다.
FeignClient 예외 처리
@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;
}
}
Feign에 대한 예외를 처리하는 클래스를 다음과 같이 컴포넌트로 등록하여 사용할 수 있다.
getOrders는 인터페이스에서 정의한 메서드 이름이며, 이에 대해서 404 예외가 날 시 특정 행위를 하도록 예외처리를 할 수 있다.