마이크로 서비스 간 통신
MSA의 마이크로 서비스들끼리 통신하는 방법으로는 동기 방식과 비동기 방식이 있다. 일반적인 HTTP 요청이 동기 방식에 해당하고, Spring Cloud Bus를 사용하는 것처럼 AMQP 프로토콜을 사용해 각 서비스 간 통신하는 방식이 비동기 방식이다.
위처럼 주문 서비스가 여러 개의 서비스로 구성되어 있고 Eureka 서버에서 라운드 로빈 방식이나 다른 방식으로 요청을 분산시키고 있다 할 때, 유저 서비스가 오더 서비스와 통신하여 데이터를 처리할 필요가 있는 상황에 대해 해결할 수 있는 방법은 동기 방식인 RestTemplate과 비동기 방식인 FeignClient 방식이 있다.
RestTemplate 사용하기
RestTemplate는 기존의 웹 애플리케이션이 HTTP를 통해 GET 방식이나 POST 방식으로 다른 서비스 혹은 API를 호출하기 위해 사용되는 방식이다.
위처럼 RestTemplate 방식은 클라이언트가 사용자 정보를 요청 하는 상황에서 userId를 기준으로 해당 사용자가 주문했던 내역을 같이 조회하여 응답하고 싶을 때, user-service는 RestTemplate를 통해 order-service에 사용자의 주문 내역을 요청하는 방식이다.
환경 설정 추가
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
Java
복사
먼저 RestTemplate을 사용하기 위해 위처럼 user-service에 빈을 등록해주고,
order_service:
url: http://127.0.0.1:8000/order-service/%s/orders
YAML
복사
이와 같이 config 저장소의 user-service.yml 파일에 조회해올 url을 추가한다.
user-service → order-service 호출
@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(environment.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;
}
Java
복사
위처럼 config 정보에서 url 정보를 가져오고, 해당 url에 RestTemplate으로 주문 내역을 조회하는 요청을 보낸다. 그 결과를 반환 값에 담아서 응답을 보낸다.
동작 확인하기
회원 가입을 진행하고 그 결과로 받은 userId로
주문을 생성한다.
이후 로그인을 진행하여 받은 JWT 토큰을 넣어서 user 정보를 조회하면,
이처럼 유저 정보에 주문 내역을 담아서 조회해오게 된다.
Discovery service의 이름으로 url 찾기
위 방식으로 구성하면, 추후 IP 주소나 포트 번호 변경되는 경우에 config의 url을 매번 바꾸어 주어야 한다. 이런 문제를 해결하기 위해 url 자체에 Discovery service에 해당 서비스가 등록한 이름으로 저장해두고, 이를 통해 변경 사항이 발생해도 config 정보를 바꾸지 않아도 되도록 만들어보자.
order_service:
url: http://ORDER-SERVICE/order-service/%s/orders
YAML
복사
이처럼 url의 IP:Port 위치에 Discovery Service에 등록된 이름을 추가하고,
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
Java
복사
RestTemplate 빈에 @LoadBalanced 애노테이션을 추가해주면 해결된다.
FeignClient 사용하기
환경 설정
FeignClient는 Netflix에서 RestTemplate를 추상화한 인터페이스를 Spring Cloud에 제공한 라이브러리이다. FeignClient를 사용하기 위해서는
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
Java
복사
이와 같이 openfeign 라이브러리 의존성을 추가해주고,
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
Java
복사
서비스에 @EnableFeignClients를 추가해주면 사용할 수 있다.
호출하기
FeignClient를 사용하기 위한 환경 설정이 끝났다면,
@FeignClient(name = "order-service")
public interface OrderServiceClient {
@GetMapping("/order-service/{userId}/orders")
List<ResponseOrder> getOrders(@PathVariable String userId);
}
Java
복사
이처럼 인터페이스를 추가한 후 @FeignClient 애노테이션으로 서비스 이름을 지정해주면, 내부의 메서드 시그니처와 HTTP 요청 방식에 알맞는 요청을 호출할 수 있다.
/* Using FeignClient */
List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);
userDto.setOrders(orderList);
Java
복사
위와 같이 의존성 주입을 받은 후 간단하게 호출하여 사용할 수 있다. 이전의 RestTemplate 방식에 비해 코드가 간결하고 깔끔해진 것을 볼 수 있다.
로그 출력하기
@Configuration
public class LoggerConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Level.FULL;
}
}
Java
복사
FeignClient 로그는 위와 같이 LoggerLevel 설정하는 빈을 추가하여 등록할 수 있다.
logging:
level:
org.example.userservice.service: DEBUG
YAML
복사
그 후 FeignClient 인터페이스가 있는 패키지에 로그 DEBUG 옵션을 설정하면,
이처럼 요청에 대한 정보를 로그로 보여준다.
예외처리
@FeignClient(name = "order-service")
public interface OrderServiceClient {
@GetMapping("/order-service/{userId}/orders_wrong")
List<ResponseOrder> getOrders(@PathVariable String userId);
}
Java
복사
위와 같이 API 주소가 틀리거나 다른 마이크로 서비스가 제대로 동작 중이지 않아 응답을 받지 못하는 경우,
발생하는 에러가 클라이언트에 그래로 전달되고, 추가적으로 서버 문제를 나타내는 500 Internal Server Error가 발생한다.
try {
List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);
userDto.setOrders(orderList);
} catch (FeignClientException e) {
log.error(e.getMessage());
}
Java
복사
이를 해결하기 위해 try-catch로 감싸 발생한 오류를 출력으로 내보내면,
클라이언트는 오류 발생 여부와 관계없이, 주문 정보는 누락되지만 사용자에 대한 정보는 응답으로 발생한다. 위에서는 오류에 대해서 출력하고 끝냈지만, 오류를 출력하는 대신 다른 Exception을 던지는 것도 가능하다.
ErrorDecoder 추가하기
위의 try-catch 방식은 매 호출마다 감싸주어야 한다는 단점이 있다. 이를 ErrorDecoder를 추가하여 일괄적으로 처리하도록 수정해보자.
order_service:
url: http://ORDER-SERVICE/order-service/%s/orders
exception:
order_is_empty: User's order is empty
YAML
복사
이처럼 config 저장소의 구성 정보에 오류 발생 시 처리할 메세지 내용을 추가해두고,
@Component
@RequiredArgsConstructor
public class FeignErrorDecoder implements ErrorDecoder {
private final Environment environment;
@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()),
environment.getProperty("order_service.exception.order_is_empty"));
}
break;
default:
return new Exception(response.reason());
}
return null;
}
}
Java
복사
이와 같이 ErrorDecoder를 상속받는 클래스를 컴포넌트로 등록하면서 그 내부에 decode 메서드를 재정의해두면 된다.
/* Using FeignClient + Error Decoder */
List<ResponseOrder> orderList = orderServiceClient.getOrders(userId);
userDto.setOrders(orderList);
Java
복사
ErrorDecoder가 적용되어 있다면, 이처럼 try-catch로 감싸주지 않더라도 발생하는 예외에 대해서 재정의해둔 decode 메서드를 통해 처리된다.