이 글은 인프런 Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) 강의를 듣고 쓴 글입니다.
Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) - 인프런 | 강의
Spring framework의 Spring Cloud 제품군을 이용하여 마이크로서비스 애플리케이션을 개발해 보는 과정입니다. Cloud Native Application으로써의 Spring Cloud를 어떻게 사용하는지, 구성을 어떻게 하는지에 대해
www.inflearn.com
이 섹션에서는 마이크로서비스 간 통신을 위한 Rest Template 방법, Feign Client 방법을 알아보고, 이 때 예외 처리와 부하 분산에 대해 더 알아보겠다
Communication Types
지금까지 마이크로서비스 간에 통신은 2가지 방법으로 수행했다.
- 동기적 방법 (Synchronous) : HTTP (Eureka를 보고 요청을 보낼 서비스를 알아내서 HTTP 로 보냄)
- 비동기적 방법 (Asynchronous) : AMQP (Spring Cloud Bus를 통해 busrefresh)
이제 실제로 자유롭게 통신하는 2가지 방법을 더 공부할 것이다. Rest Template 방법, Feign Client 방법이다.
Rest Template
RestTemplate 이라는 Bean을 등록해서 User Service에서 Order Service에 통신하는 것을 실습해본다.
먼저 user-service에 메인 클래스에 RestTemplate Bean을 등록한다.
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
@LoadBalanced : 서비스가 Eureka service registry에 등록된 서비스를 호출하기 위해서는 Eureka 에 연동되어야만 하는데, @LoadBalanced annotation 은 load balance도 요구하지만 Eureka 에 연동되어 서비스에 접근하기 위한 목적으로도 필요한 annotation이다. 즉, Eureka에 연동되어 주소가 아닌 Eureka에 등록된 이름으로도 호출이 가능하다.
밑의 주소처럼 설정할 수 있다는 것이다. 현재 user-service의 설정 파일로 사용하고 있는 user-service.yml 파일에 다음과 같이 추가한다.
order_service:
url: http://ORDER-SERVICE/order-service/%s/orders
# url: http://127.0.0.1:8000/order-service/%s/orders -> @LoadBalance 없을 땐 이렇게 해야
기능 추가 - User Service에서 Order Service의 주문 조회
이제, User Service에서 "/users/{userId}" 경로로 유저 정보를 받아올 때, Order Service에게 Rest Template으로 요청을 해 주문 정보를 받아올 것이다. UserServiceImpl 클래스에서 getUserByUserId() 메소드를 다음과 같이 수정한다. 또한, 이 클래스의 멤버 파라미터로 Environment env 와 RestTemplate restTemplate 는 추가해야 할 것이다.
@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);
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();
userDto.setOrders(ordersList);
return userDto;
}
restTemplate의 exchange 메소드를 사용해 요청한다. 요청할 주소, HttpMethod, RequestEntity (여기서는 GET이므로 null) 과, 어떤 식으로 반환받을지를 알려주기 위해 ParameterizedTypeReference 를 파라미터로 준다.
OrderService에서 응답하는 형식 그대로 ResponseEntity<List<ResponseOrder>> 형으로 응답받고, 그 후에 .getBody() 메소드로 응답의 Body를 꺼낸다.
이제 실제 한 내용들을 실습해보자. 회원가입 -> 로그인 -> 주문 등록 (2개) -> 사용자 정보 조회
마지막 사용자 정보 조회에서 User Service와 Order Service의 통신이 잘 이루어진 것을 볼 수 있다.
Feign Client
FeignClient는 Rest한 Http를 위한 도구이다. Spring Cloud Netflix의 라이브러리이며, 굉장히 직관적이다.
호출하려는 HTTP Endpoint에 대한 Interface를 생성하고 @FeignClient를 붙이면 된다. 이는 Load Balance 도 제공한다.
FeignClient를 사용하기 위해 pom.xml 파일에 다음 의존성을 추가한다.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
그리고 코드의 RestTemplate을 쓰는 부분을 모두 주석처리하고 메인 클래스에 다음을 추가한다.
@EnableFeignClients
이제 호출하려는 HTTP Endpoint에 대한 Interface를 생성하고 @FeignClient를 붙이자. 다음과 같은 인터페이스를 추가한다. client 라는 패키지를 생성해 그 안에 두는 것이 좋다.
@FeignClient(name = "order-service")
public interface OrderServiceClient {
@GetMapping("/order-service/{userId}/orders")
List<ResponseOrder> getOrders(@PathVariable String userId);
}
UserserviceImpl에 프로퍼티로 OrderServiceClient orderServiceClient; 를 추가하고, 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);
/* Using a rest template */
// 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 a feign client */
List<ResponseOrder> ordersList = orderServiceClient.getOrders(userId);
userDto.setOrders(ordersList);
return userDto;
}
한 줄로 더욱 직관적인 요청이 가능하다.
이제 지금까지 했던 것과 똑같이 요청해보자. (회원가입 -> 로그인 -> 주문 등록 (2개) -> 사용자 정보 조회) (회원가입, 로그인 사진 생략)
Rest Template vs Feign Client
Feign Client는 누가 봐도 Rest Template 보다 직관적이고 명료하고 코드도 짧다. 따라서 굉장히 많은 상황에서 Feign Client 를 사용하곤 한다.
하지만, 통신하는 서비스들 간의 구조를 잘 모르는 사람이면 직관적인 통신을 하려고 할 때 문제점이 많을 것이다. Rest Template 을 사용하면 파라미터, 반환값 등을 모두 지정하기 때문에 자세히 컨트롤할 수 있다.
언제 무엇을 써야할 지는 개발자의 선택!
Feign Client 예외처리
Feign Client 에서의 로그 사용
예외처리나 Feign Client 동작의 디버깅을 위해 로그를 사용할 것이다. 이를 위해 application.yml 파일의 디버깅 레벨을 다음과 같이 바꿔주자.
logging:
level:
com.example.userservice.client: DEBUG
메인 클래스에는 Feign Client 의 로그를 볼 수 있게 다음의 빈을 등록하는 코드를 작성하자.
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
이 설정만으로도 Feign Client 의 많은 동작에 대해 로그가 나올 것이다.
이제 Feign Client Interface의 요청 주소 마지막에 _ng 를 붙여 잘못된 주소에 요청을 보내도록 해보자.
사용자 정보 조회를 하면, Order Service에 통신을 해야 하는데 잘못된 주소로 요청을 한 것이 보일 것이다.
이 때, User Service 라는 서버의 잘못이므로 500 이 반환되지만, Order service 입장에서는 404 이므로 위와 같은 오류 메시지가 출력된다.
우리는 Order Service에 잘못된 요청을 해도, Order Service에서 반환되는 정보만을 띄우지 않고 나머지 정보들은 정상적으로 뜨게 하고 싶다. 이를 위해 예외처리를 해주자!
이전의 getUserByUserId() 메소드 다음과 같이 부분 수정한다.
/* Using a feign client */
/* Feign exception handling */
List<ResponseOrder> ordersList = null;
try {
ordersList = orderServiceClient.getOrders(userId);
} catch (FeignException ex) {
log.error(ex.getMessage());
}
userDto.setOrders(ordersList);
return userDto;
이제 이전과 같이 테스트해보자.
잘못된 주소로 요청했지만, 반환받지 못한 값들을 제외하고 응답이 온 것을 볼 수 있다. 로그에 404 에러도 잘 나온 것을 확인 가능하다.
Error Decoder 를 이용한 예외 처리
에러 상태코드에 따라서 어떻게 예외 처리를 할 지 자세하게 구현해보도록 하자.
error 패키지를 만들고 다음과 같은 클래스를 만들자.
public class FeignErrorDecoder implements ErrorDecoder {
@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()),
"User's orders is empty");
}
break;
default:
return new Exception(response.reason());
}
return null;
}
}
400일 경우에 처리하지 말자. 404이면서 메소드에 getOrders가 있는 경우 주문 리스트가 비어있다는 메시지와 함께 예외 처리를 해준다.
이제 이 클래스를 빈으로 등록해서 다른 클래스에서도 사용하자. 메인 클래스에 다음 코드를 추가한다.
@Bean
public FeignErrorDecoder getFeignErrorDecoder() {
return new FeignErrorDecoder();
}
이제 UserServiceImpl 클래스에 FeignErrorDecoder 프로퍼티를 추가한다.
그리고 계속 수정하는 부분을 아래와 같이 수정하자.
List<ResponseOrder> ordersList = orderServiceClient.getOrders(userId);
userDto.setOrders(ordersList);
return userDto;
기존과 같이 복잡한 try-catch 구문이 필요없다.
이제 실행해보자.
404 오류가 뜨고, 우리가 의도했던 메시지도 맨 아래 잘 응답되는 것을 볼 수 있다.
하지만 이 메시지는 하드코딩하지 않고 설정파일에서 다룰 수 있으면 좋을 만한 것이다. user-service.yml 파일로 옮겨보자. 다음과 같이 수정한다.
order_service:
url: http://ORDER-SERVICE/order-service/%s/orders
exception:
orders_is_empty: User's orders is empty.
그리고 FeignErrorDecoder에 프로퍼티로 Environment를 등록하고, 클래스를 @Component 로 등록하고 메인 클래스의 @Bean 설정도 지워준다. 이제, 404 에러 처리 부분을 다음과 같이 바꾼다.
return new ResponseStatusException(HttpStatus.valueOf(response.status()),
env.getProperty("order_service.exception.orders_is_empty"));
다시 실행해주면 아까와 같이 나온다. 또, busrefresh 를 통해 반환할 문자열을 바꿀 수 있다.
데이터 동기화 문제
Service의 인스턴스가 하나가 아닐 때, 데이터베이스에 접근하면 동기화 문제가 생기게 된다. 지금까지 데이터베이스는 인스턴스 당 하나를 만들도록 설정해 놓았다. 이를 해결하기 위해 다음 3가지 방법을 살펴본다.
- 하나의 Database 사용
인스턴스 당 데이터베이스를 따로 두는 것이 아니라 공통된 데이터베이스를 두는 것이다. 이 경우 충돌이 일어날 가능성이 많아서 트랜잭션 등의 관리를 잘 해야 한다. - Database 간의 동기화
인스턴스 당 데이터베이스를 두고 Message Queuing Server (Apache Kafka, RabbitMQ ..) 를 사용해 데이터베이스를 동기화한다. - Kafka Connector + Database
두 해결법을 모두 쓰는 것이다. 각 인스턴스는 Message Queuing Server에 메시지를 보낸다. 이것은 미들웨어 역할을 하고 이런 문제에 특화되어 있어 동시성 문제, 충돌 등을 해결해 줄 수 있다. 여기서 데이터베이스를 접근해서 문제를 해결한다.
2,3번은 섹션 11, 12 번에서 자세히 다룬다. 우선 어떤 문제가 발생하는지부터 보자.
문제 발견
다중 인스턴스 환경에서 어떤 문제가 발생하는 지 보기 위해 Order Service 를 2개 기동해보자. 하나는 원래 방법대로, 하나는 터미널에서 아래 명령어를 입력해 실행하자.
mvn spring-boot:run
이제 회원가입, 로그인을 진행하고 5개의 주문을 등록해보자. Eureka가 Load Balancer 의 역할을 해주기 때문에 각 인스턴스에 요청이 번갈아가며 (Round Robin) 들어갈 것이다. 각 인스턴스의 데이터베이스를 확인해보자.
이러면 어떤 문제가 발생하는지 보자. 사용자 정보를 조회해보면 알 수 있다.
당연한 결과겠지만, 여기서 Order Service에 요청하는 것도 번갈아가며 수행된다. 데이터베이스의 동기화가 전혀 일어나지 않고 있다.
따라서 다음 섹션부터 위에 말한 방법대로 해결해볼 것이다.
'[MSA] Spring Cloud로 개발하는 마이크로서비스 애플리케이션' 카테고리의 다른 글
섹션 12 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (1) | 2023.09.02 |
---|---|
섹션 11 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (0) | 2023.08.24 |
섹션 9 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (0) | 2023.08.11 |
섹션 8 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (0) | 2023.08.11 |
섹션 7 : Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) (0) | 2023.08.04 |