하나의 어플리케이션 내에서 작업하고 메소드 호출로 동작하는 모놀리스 방식과 비교하여 마이크로 서비스는 물리적으로 분산된 서비스들간의 통신이 필수이다.
Communication types
- Synchronous HTTP Communication(동기) - 기존의 웹 어플리케이션에 하나의 요청이 들어오면 해당하는 요청이 끝날 때까지 다른 작업 불가
- Asynchronous Communication over AMQP(비동기) - Spring Cloud Bus에서 설명했는데 Spring Config의 정보를 각각의 마이크로서비스가 순차적으로 동기하는 것이 아니라 일단 연결되어 있는 모든 마이크로서비스들에게 전달
RestTemplate, Feign Client 두가지
RestTemplate
- 스프링에서 제공하는 http 통신에 유용하게 쓸 수 있는 템플릿. 기존의 HTTP의 GET, POST.. 를 이용
- RestTemplate 인스턴스 생성하고 필요로 하는 HTTP 메서드와 파라미터를 처리해서 동작
- jdbcTemplate 처럼 RestTemplate 도 기계적이고 반복적인 코드들을 깔끔하게 정리해준다.
- 동기처리를 하고 기존 다른 api 를 불러올때 쓰였던걸 msa 에 적용하는 것이다.
Users service 에서 Order Service를 호출하기 위해 RestTemplate 등록
UserServiceApplication
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
UserController.java
@GetMapping("/users/{userId}")
public ResponseEntity<ResponseUser> getUser(@PathVariable String userId) {
UserDto userDto = userService.getUserByUserId(userId);
ResponseUser returnValue = new ModelMapper().map(userDto, ResponseUser.class);
return ResponseEntity.ok(returnValue);
}
UserServiceImpl.java
UserRepository userRepository;
BCryptPasswordEncoder passwordEncoder;
RestTemplate restTemplate;
Environment env;
@Autowired
public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder, RestTemplate restTemplate, Environment env) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.restTemplate = restTemplate;
this.env = 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);
// List<ResponseOrder> orders = new ArrayList<>();
// RestTemplate 사용
String orderUrl = String.format(env.getProperty("order_service.url"), userId); // 사용자 id로 등록된 주문을 가져올 주소
// restTemplate 의 exchange 메서드 (주소, HTTP 메서드, request 할때 데이터 파라미터 타입, 반환 파라미터 타입)
ResponseEntity<List<ResponseOrder>> orderListResponse = restTemplate.exchange(orderUrl, HttpMethod.GET, null,
new ParameterizedTypeReference<List<ResponseOrder>>() {
});
List<ResponseOrder> ordersList = orderListResponse.getBody();
userDto.setOrders(ordersList);
return userDto;
}
user-service.yml
order_service:
url: http://127.0.0.1:8000/order-service/%s/orders
테스트
회원가입 -> 로그인 -> 사용자 주문(http://127.0.0.1:8000/order-service/{userId}/orders)
사용자 정보 확인(http://127.0.0.1:8000/user-service/users/{userId})
- 사용자 정보와, 주문정보 확인 가능
User Service 에서 Order Service를 간단하게 사용하는 방법
주소를 사용하지 않고 MicroService 이름을 사용하도록 변경 주소
MicroService 이름
-> RestTemplate Bean에 @LoadBalanced 사용
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
Feign Client
- Rest 방식을 사용하기 위해 추상화한 Spring Cloud Netflix 라이브러리
- Load balanced 지원
사용방법
- 호출하려는 HTTP Endpoint(마이크로서비스의 특정 메서드를 실행하는 형식)에 대한 Interface를 생성
- @FeignClient 선언
Spring Cloud Netflix 라이브러리 추가
<!-- Feign Client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// FeignClient 사용시에 필요없음
// @Bean
// @LoadBalanced
// public RestTemplate getRestTemplate(){
// return new RestTemplate();
// }
}
@FeignClient Interface 생성
- @FeignClient에는 호출하고자 하는 MSA이름을 지정한다.
- 여기서 MSA의 이름은 Eureka server에 인스턴스로 등록시 사용된 이름이다.
- 이렇게 FeignClient로 설정해주면 마치 자신의 API인 것처럼 정의가 가능하다.
- @GetMapping 쪽을 보면 기존에 Controller에서 endpoint를 지정하는 것과 거의 동일한 것을 알 수 있다. 다만 세부 구현 내용은 필요로 하지 않는다.
package com.example.userservice.client;
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);
}
UserServiceImpl.java 에서 Feign Client 사용
@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<>();
// RestTemplate 사용
// String orderUrl = String.format(env.getProperty("order_service.url"), userId); // 사용자 id로 등록된 주문을 가져올 주소
// // restTemplate 의 exchange 메서드 (주소, HTTP 메서드, request 할때 데이터 파라미터 타입, 반환 파라미터 타입)
// ResponseEntity<List<ResponseOrder>> orderListResponse = restTemplate.exchange(orderUrl, HttpMethod.GET, null,
// new ParameterizedTypeReference<List<ResponseOrder>>() {
// });
//
// List<ResponseOrder> ordersList = orderListResponse.getBody();
// Feign Client 사용
List<ResponseOrder> ordersList = orderServiceClient.getOrders(userId);
userDto.setOrders(ordersList);
return userDto;
}
RestTemplate 보다 훨씬 직관적으로 사용할 수 있다.
하지만 직접 개발한 사람이 아니라면 직관적이라는 측면이 반대로 파악하기 힘들 수 있다.
Feign Client 에서 로그 사용
application.yml
logging:
level:
com.example.userservice.client: DEBUG
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
Feign Client 에서 예외 처리
FeignException 처리
잘못된 주소로 이동할 경우
@FeignClient(name = "order-service")
public interface OrderServiceClient {
@GetMapping("/order-service/{userId}/orders_ng")
List<ResponseOrder> getOrders(@PathVariable String userId);
}
UserServiceImpl.java
예외 처리
// Feign Client 사용
// Feign Exception Handling
List<ResponseOrder> ordersList = null;
try {
ordersList = orderServiceClient.getOrders(userId);
} catch (FeignException ex) {
log.error(ex.getMessage());
}
userDto.setOrders(ordersList);
- 예외 발생한 부분 제외하고 출력
ErrorDecoder
Feign 에서 제공하는 인터페이스
- ErrorDecoder를 이용하여 예외를 모아 관리할 수 있다.
- ErrorDecoder를 구현하는 클래스를 생성하고 아래와 같은 리턴값, 매개변수를 갖는 decode 메서드를 오버라이딩한다.
- 첫번째 매개변수인 method에는 FeignClient를 통해 호출한 함수의 이름을 포함한다. 때문에 에러 케이스마다 호출한 함수의 명에 따라 구분하여 처리가 가능하다.
- 리턴값은 임의대로 Exception객체를 적절하게 구성하여 리턴한다.
FeignErrorDecoder.java
package com.example.userservice.error;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
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 order is empty");
}
default:
return new Exception(response.reason());
}
return null;
}
}
Bean 등록
@Bean
public FeignErrorDecoder getFeignErrorDecoder() {
return new FeignErrorDecoder();
}
UserServiceImpl.java
// ErrorDecoder 사용
List<ResponseOrder> ordersList = orderServiceClient.getOrders(userId);
500에서 404로 바뀐거 확인
예외 문장 user-service.yml 에서 가져오기
FeignErrorDecoder.java
package com.example.userservice.error;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
@Component
public class FeignErrorDecoder implements ErrorDecoder {
Environment env;
@Autowired
public FeignErrorDecoder(Environment env) {
this.env = 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_serivce.exception.order_is_empty"));
}
default:
return new Exception(response.reason());
}
return null;
}
}
@Component로 등록해서 굳이 Bean으로 등록할 필요없다.
// @Bean
// public FeignErrorDecoder getFeignErrorDecoder() {
// return new FeignErrorDecoder();
// }
Multiple Orders service
User service에서 분산된 Orders service를 호출하게 되면 Orders 데이터도 분산 저장되어 있어서 -> 동기화 문제 발생!!
(주문 데이터를 조회할 때 데이터가 나눠서 저장되어 있어서 호출할 때 마다 다른 데이터를 가져오게 된다.)
3가지 해결방안
- 분산된 Service에서 하나의 DB를 사용하면 해결 -> DB의 동시성, 트랜잭션 문제 잘 해결해야 한다.
- DB 간의 동기화 -> Message Queuing Server를 이용해서 변경된 데이터가 있으면 업데이트
- 하나의 DB와 Message Queuing Server 사용 (복합) -> 들어온 요청을 Message Queuing Server에서 먼저 받아서 DB로 순차적으로 전달
2개의 Orders Service 실행시 발생하는 문제점
- 사용자가 한명인데 2개의 DB에 나눠서 저장된다.
- 사용자 정보를 가져올 때마다 다른 값들이 출력된다.
'Spring > [인프런] Spring Cloud' 카테고리의 다른 글
데이터 동기화를 위한 Kafka 활용 2 (0) | 2022.07.04 |
---|---|
데이터 동기화를 위한 Kafka 활용 1 (0) | 2022.06.30 |
설정 정보의 암호화 처리(Encryption, Decryption) (0) | 2022.06.27 |
Spring Cloud Bus (0) | 2022.06.24 |
Configuration Service (0) | 2022.06.24 |