[MSA] MSA 전환 프로젝트 - 6. 마이크로 서비스 간 통신 with Spring Cloud OpenFeign
프로젝트를 도메인 별로 각각의 마이크로 서비스로 만들어 Service Registry에 등록도 해보았고 등록된 서비스를 Api Gateway를 통해 하나의 진입점을 통해서 접속할 수 있도록 만들어 놓기도 했다.
각각의 서비스는 분리해 놓으니 오히려 필요한 부분만 갖게 되어 혼자서는 이전보다 더 잘 동작하는 상황이 되었지만
하나의 애플리케이션이 아닌 서비스 별로 각각 기동되는 애플리케이션끼리 필요한 데이터를 어떻게 주고받아야 하는가 하는 문제에 도달하게 되었다.
1. 서비스 간 통신 방식의 종류
서비스 간 통신을 하기 전에, 어떤 통신 방식이 있고 그 중 어떤 것을 활용해 볼지에 대해서 먼저 알아보자.
서비스 간 통신의 종류는 크게 동기 방식과 비동기 방식으로 나뉘게 된다.
- 동기 방식
- RestTemplate
- OpenFeign
- 비동기 방식
- WebClient
- Message Queue
따라서 우리는 먼저 서비스 간 통신 방식을 결정하기 위해서 동기방식으로 서비스를 호출할 지 비동기 방식으로 서비스를 호출할지를 결정해야 한다.
동기 방식은 이미 기존해 사용해 온 HTTP/REST 요청이 동기 방식이기 때문에 익숙한 방식이긴 하지만 이는 하나씩 처리해야 하는 동기 방식의 한계로 인해 다수의 요청이 왔을 때 지연이 길어질 수 있다.
비동기 방식은 여러 요청을 동시에 처리할 수 있기 때문에 지연이 적고 성능이 좋다는 장점이 있지만 메시지 큐를 사용할 때 메시지의 순서나 처리 상태를 관리해야 하는 등 고려해야할 사항이 많다.
때문에 현재 간단한 프로젝트를 진행하는 만큼 간단하고 직관적인 동기방식을 사용해 마이크로 서비스 간 통신을 구현해보려 한다.
동기 방식에도 RestTemplate, OpenFeign 두 가지 종류가 있다.
RestTemplate
- 장점
- 다양한 HTTP 메서드를 지원하고, 요청을 다양한 방식으로 커스터마이즈 가능
- 오랜 기간 사용되어 왔기 때문에 많은 개발자들에게 익숙함
- 많은 커스터마이즈를 제공해 복잡한 통신 시나리오를 다룰 수 있음
- 단점
- 작성해야할 코드가 많음 => 가독성과 유지보수성을 해칠 가능성이 높음
- 현재 Spring 버전에서 권장되지 않음 (maintenance mode)
OpenFeign
- 장점
- 선언하는 방식으로 구현 가능해 간단함
- @FeginClient 어노테이션과 인터페이스를 사용하여 클라이언트를 정의하고 메서드를 호출하는 방식으로 통신할 수 있음
- 내장된 로드밸런싱 기능과 서비스 디스커버리를 활용할 수 있음
- 요청 및 응답에 대한 로깅, 오류 처리, 보안 등을 쉽게 구현할 수 있음
- 단점
- 상대적으로 새로운 기술이기 때문에 RestTemplate에 비해 사용자들이 덜 익숙할 수 있음
- 자동 설정과 기본 설정이 일부 한정적일 수 있고 일부 특정한 사용 사례에서 제약이 있을 수 있음
둘 중에 어떤 것을 고르기 전에 한 번 둘 다 사용해 보기로 했다.
2. RestTemplate
2-1. RestTemplate란?
Spring에서 지원하는 객체를 통해 간편하게 REST 방식 API를 호출할 수 있는 Spring 내장 클래스
REST API 서비스를 요청 후 응답 받을 수 있도록 설계되어 있으며 HTTP 포로토콜의 메서드(GET, POST, DELETE, PUT)들에 적합한 여러 메서드를 제공하며 json, xml 응답을 모두 받을 수 있다.
말 그대로 RESTful한 형식의 템플릿으로 Header, Content-Type 등을 설정하여 외부 API를 호출하며 서버 간 통신(Server to Server)에 주로 사용된다.
2-2. RestTemplate의 동작 원리
- 애플리케이션 내부에서 REST API에 요청하기 위해 RestTemplate의 메서드를 호출한다.
- RestTemplate은 MessageConverter를 이용해 java object를 Request Body에 담을 message(json 등)로 변환한다.
- ClientHttpRequestFactory에서 ClientHttpRequest를 받아와 요청을 전달한다.
- 실질적으로 ClientHttpRequest가 HTTP 통신으로 요청을 수행한다.
- RestTemplate이 에러핸들링을 한다.
- ClientHttpResponse에서 응답 데이터를 가져와 오류가 있으면 처리한다.
- MessageConverter를 이용해 Response Body의 message를 java object로 변환한다.
- 결과를 애플리케이션에 돌려준다.
복잡해보이지만 결국 의존성을 주입한 RestTemplate의 메서드를 호출하면 엔드포인트에 맞는 API와 통신을 해서 응답 데이터를 가져온다는 뜻이다.
2-3. RestTemplate 적용
현재 프로젝트에는 user-service와 order-service가 있다.
order-service는 주문을 위한 서비스로 userId와 주문에 대한 상세 정보를 입력받아 DB에 저장하는 메서드가 있고, 그 userId를 이용해 해당 user가 주문한 orderList를 조회할 수 있는 getOrder 메서드가 있다.
@GetMapping("{userId}/orders")
public ResponseEntity<List<ResponseOrder>> getOrder(@PathVariable("userId") String userId) throws Exception {
Iterable<OrderEntity> orderList = orderService.getOrdersByUserId(userId);
List<ResponseOrder> result = new ArrayList<>();
orderList.forEach(v->{
result.add(new ModelMapper().map(v, ResponseOrder.class));
});
return ResponseEntity.status(HttpStatus.OK).body(result);
}
그리고 우리는 user-service에서 user를 조회할 때 해당 user의 userId로 주문한 목록 즉 orderList를 같이 출력하려하기 때문에 user-service에서 order-service의 getOrder를 호출해야할 필요가 있다.
2-3-1. RestTemplate Bean 등록
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
RestTemplate을 Bean으로 등록하는 메서드를 애플리케이션의 메인클래스나 Configuration 클래스에 구현해 해당 모듈을 Bean으로 등록해주자.
2-3-2. UserService.getUserByUserId
여기서 주목해야할 것은 @AutoWired를 통해 주입받은 RestTemplate 객체의 exchange(url, HttpMethod, requestEntity, responseType) 메서드이다.
설정 파일의 환경변수로 등록해놓은 order-service의 getOrder 메서드를 호출할 url과 함께 GET 방식 호출,
body는 없기 때문에 ResponseEntity는 null,
그리고 responseType에는 응답받을 orderList를 java object로 변환한 List<ResponseOrder>를 PrameterizedTypeReference의 매개변수로 넣어 준다.
이렇게 new PrameterizedTypeReference에 알맞는 DTO를 정의해서 받아주면 API를 호출하며 자동으로 DTO객체로 변환해준다.
3. OpenFeign
3-1. OpenFeign이란?
Spring Cloud OpenFeign의 공식 문서에는 다음과 같이 소개하고 있다.
Feign은 어노테이션 기반의 Web Service Client로,
어노테이션 방식으로 동작함에 따라 Web Service Client를 쉽게 구성할 수 있습니다.
Spring MVC을 지원하고, Spring Web에서 사용되는 HttpMessageConverters도 지원합니다.
쉽게 말하면 OpenFeign은 어노테이션 기반으로 Spring MVC 및 HttpMessageConverter를 지원함에 따라
기존 Spring MVC에서 어노테이션을 이용해서 HTTP 통신을 하는 것과 유사하게 구현할 수 있다는 뜻이다.
따라서 다른 방식과 비교해서 비교적 간단하게 구현이 가능하고 Spring Cloud에서 제공되는만큼 다른 기술들(Eureka, Circuit Breaker, Load Balancer)과 통합이 쉽다.
3-2. OpenFeign 적용
3-2-1. Dependency 추가
dependencies {
...
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}
서비스를 호출할 쪽, 해당 프로젝트에서는 user-service에서 order-service를 호출할 것이기 때문에 user-service에 OpenFeign 의존성을 추가해준다.
3-2-2. OpenFeign 활성화
@EnableFeignClients
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
@EnableFeignClient 어노테이션을 선언하면 하위 클래스에서 @FeignClient가 등록된 클래스를 찾아 구현체를 생성하는 역할을 한다.
3-2-3. @FeignClient interface 생성
@FeignClient(name="order-service", path= "/order-service/{userId}/orders")
public interface OrderServiceClient {
@GetMapping
List<ResponseOrder> getOrders(@PathVariable String userId);
}
인터페이스를 생성하고 @FeignClient 어노테이션을 붙여 활성화 해준다. 메서드로는 통신할 order-service의 getOrders 메서드를 그대로 작성해준다.
@FeignClient에는 name과 path 두가지 옵션을 넣을 수 있는데 name은 통신할 서버의 Eureka에 등록된 이름 즉 application name을 입력해주면 되고 path에는 Mapping될 엔드포인트를 넣어주면 된다.
3-2-3. UserService.getUserByUserId
@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);
//// List<ResponseOrder> orders = new ArrayList<>();
// /* Using as 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> ordersList = orderListResponse.getBody();
/*Using as OpenFeign*/
/*Feign exception handling*/
List<ResponseOrder> ordersList = null;
try {
ordersList = orderServiceClient.getOrders(userId);
} catch (FeignException exception) {
log.error(exception.getMessage());
}
userDto.setOrders(ordersList);
return userDto;
}
아래의 OpenFeign을 활용한 부분만 보면 된다.
앞서 만들었던 OrderServiceClient interface의 getOrders메서드를 호출하기만 하면 되는 간단한 코드다.
확실히 주석 처리된 RestTemplate을 사용한 부분에 비해 훨씬 간결한 코드임을 알 수 있다.
이번에는 마이크로 서비스 간에 통신을 하는 방법에 대해서 알아보았다.
동기 방식과 비동기 방식이 있고 동기 방식에도 RestTemplate과 OpenFeign으로 나눠지는 방식 중에서
현재 프로젝트는 maintanence mode에 들어간 RestTemplate보다는 현재 권장되고 여러가지 장점이 있는 OpenFeign을 활용해 진행하게 될 것 같다.
특히 가독성이 좋은 점이 굉장히 마음에 들었다.