Microservcie간 통신 방법
- REST Template
- OpenFeign(@FeignClient)
- Kafka
💡 Feign Web Service Client
- FeignClient → HTTP Client
- REST Call을 추상화 한 Spring Cloud Netflix 라이브러리
- 사용 방법
- 호출하려는 HTTP Endpoint에 대한 interface를 생성
- @FeilgnClient 선언
- Load balanced 지원
user를 조회하면 주문도 조회하는 것을 해보자.
1. 라이브러리 추가
user-service
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
UserServiceApplication.java
@EnableFeignClients 추가
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserServiceApplication { // ..생략.. }
2. @FeignClient Interface 생성
@FeignClient(name = "order-service")
public interface OrderServiceClient {
// interface에서는 어차피 public이라 안붙여도 ㄱㅊ
@GetMapping("/order-service/{userId}/orders")
List<ResponseOrder> getOrder(@PathVariable("userId") String userId);
}
3. UserServiceImpl.java에서 FeignClient 사용
@Service
public class UserServiceImpl implements UserService{
UserRepository userRepository;
BCryptPasswordEncoder passwordEncoder;
Environment env;
RestTemplate restTemplate;
OrderServiceClient orderServiceClient;
public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder, Environment env, RestTemplate restTemplate, OrderServiceClient orderServiceClient) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.env = env;
this.restTemplate = restTemplate;
this.orderServiceClient = orderServiceClient;
}
// .. 생략 ...
@Override
public UserDto getUserByUserId(String userId) {
UserEntity userEntity = userRepository.findAllByUserId(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 rest template */
// String orderUrl = "http://127.0.0.1:800/order-service/%s/orders"; // user-serivce.yml로 이동
// String orderUrl = String.format(env.getProperty("order_service.url"), userId); // %s에 userId를 넣어주기 위해 String.format 사용
// ResponseEntity<List<ResponseOrder>> orderListResponse = restTemplate.exchange(orderUrl, HttpMethod.GET, null,
// new ParameterizedTypeReference<List<ResponseOrder>>() {
// });
// List<ResponseOrder> orderList = orderListResponse.getBody();
/* Using a feign client */
List<ResponseOrder> orderList = orderServiceClient.getOrder(userId);
userDto.setOrders(orderList);
return userDto;
}
// ... 생략 ...
}
order-service를 거쳐가 값이 나온다.
💡 REST Tamplate vs. FeignClient
FeignClient는 REST Tamplate보다 코드양이 적고 직관적이다.(메서드를 추가하면 되니까)
하지만 직접 개발한 사람이 아닌 경우에는 파악하기 어려울 수 있다.
왜냐하면 인터페이스 안에 또 다른 어플리케이션에 있는 메서드를 사용하기 때문에 유저서비스만 봐서는 뭐하는지 모를 수 있다. 유저와 오더 서비스 둘다 파악해야 한다.
반면에 REST Tamplate은 기존에 있던 HTTP의 IP하고 포트번호, URI, endPoint, HTTP메서드, 파라미터 이런 것들만 사용하고 전달하기 때문에 한 서비스만 봐도 파악할 수 있다.
- FeignClient
- 간결하고 직관적인 코드:
FeignClient를 사용하면 메서드를 추가하여 원격 서비스의 API를 호출할 수 있습니다. 이는 코드의 간결성과 직관성을 높여줍니다. - 인터페이스 기반 통신:
FeignClient는 인터페이스를 통해 외부 서비스의 API를 호출하므로, 사용자는 자연스럽게 해당 서비스의 기능을 호출할 수 있습니다. - 모듈 간의 의존성:
FeignClient를 사용하면 다른 마이크로서비스의 메서드에 직접적으로 의존하게 됩니다. 이는 해당 마이크로서비스를 이해해야 한다는 의미이며, 따라서 유지보수와 이해가 어려울 수 있습니다. - 장점
- 코드 간결성과 직관성이 높다.
- 인터페이스 기반 통신으로 추상화 수준이 높다.
- 단점
- 외부 서비스에 대한 명시적인 의존성이 생길 수 있다.
- 다른 마이크로서비스에 대한 이해가 필요하다.
- 간결하고 직관적인 코드:
- REST Template
- 직접적인 HTTP 요청 관리:
REST Template은 직접 HTTP 요청을 수행하므로, HTTP의 기본 개념(IP, 포트번호, URI, HTTP 메서드 등)을 이해하고 있으면 사용하기 쉽습니다. - 명시적인 의존성:
REST Template을 사용하면 다른 서비스의 API에 대해 명시적인 HTTP 요청을 작성해야 합니다. 이는 외부 서비스에 대한 명시적인 의존성을 만들어내지만, 서비스 간의 의존성을 감추기 위해선 추가적인 추상화가 필요할 수 있습니다. - 서비스 간의 결합도:
REST Template은 보다 낮은 수준의 서비스 간 통신을 제공하므로, 각 서비스 간의 결합도가 상대적으로 높을 수 있습니다. - 장점
- 외부 서비스에 대한 명시적인 의존성이 생길 수 있다.
- 다른 마이크로서비스에 대한 이해가 필요하다.
- 단점
- 코드가 상대적으로 더 복잡할 수 있다.
- 서비스 간의 통신이 직접적이어서 유지보수가 어려울 수 있다.
- 직접적인 HTTP 요청 관리:
FeignClient 예외 처리
FeignClient 사용 시 발생한 로그 추적
사용자 id 없을 때
잘못된 endpoint(주소값) 호출했을 때
~/userId/order_ng 에서 order_ng값이 없을 때, 오류만 뜨는게 아니라 user에 대한 정보는 보여주고 order에 대한 것만 안보여주는 식으로 해보자
user-service
1. 라이브러리 추가
application.yml
logging:
level:
com.example.userservice.client: DEBUG
2. Logger 빈 생성
UserServiceApplication.java
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class UserServiceApplication {
// .. 생략 ..
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
3. 잘못된 주소값을 호출해보자.
오류만 보여준다.
500에러와 404에러가 같이 뜨는 것을 확인 할 수 있다.
주소값을 잘못 호출했으니 클라이언트의 문제인데 500이라는 서버에러를 보여준다.
FeignClient의 로그를 보여준다.
🙄 오류만 뜨는게 아니라 user에 대한 정보는 보여주고 order에 대한 것만 안보여주는 식으로 해보자
방법 1) try-catch문을 이용해보자.
@Override
public UserDto getUserByUserId(String userId) {
UserEntity userEntity = userRepository.findAllByUserId(userId);
if (userEntity == null)
throw new UsernameNotFoundException("User not found");
UserDto userDto = new ModelMapper().map(userEntity, UserDto.class);
/* Using a feign client */
/* Feign exception handling */
// List<ResponseOrder> orderList = orderServiceClient.getOrder(userId);
List<ResponseOrder> orderList = null;
try {
orderList = orderServiceClient.getOrder(userId);
} catch (FeignException ex) {
log.error(ex.getMessage()); // 오류는 로그로 보여주고
}
userDto.setOrders(orderList);
return userDto; // 조회했던 사용자로 보여준다.
}
조회한 User는 확인하고 Error는 로그에서 확인할 수 있다.
방법 2) FeignErrorDecoder 사용
💡 FeignErrorDecoder
💡 FeignErrorDecoder
Feign 패키지에서 제공
1. FeignErrorDecoder 구현
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")) { // getOrders함수일 경우만, 예외객체를 만들어서 반환
return new ResponseStatusException(HttpStatus.valueOf(response.status()),
"User's order is empty.");
}
break;
default:
return new Exception(response.reason()); // 예외 오류 출력
}
return null;
}
}
2. Application 클래스에 ErrorDecorder 빈등록
@Bean
public FeignErrorDecoder FeignErrorDecoder() {
return new FeignErrorDecoder();
}
3.
ServiceImpl에서 생성자 주입을 하지 않아도 에러가 발생했을 때 디코드가 사용될 것이기 때문에 주입받지 않아도 사용 가능하다.
ErrorDecorder로 예외처리를 할 것이니, 이전에 try-catch로 예외처리 해둔 것은 주석처리하자.
UserServiceImpl
@Override
public UserDto getUserByUserId(String userId) {
UserEntity userEntity = userRepository.findAllByUserId(userId);
if (userEntity == null)
throw new UsernameNotFoundException("User not found");
UserDto userDto = new ModelMapper().map(userEntity, UserDto.class); // (바꾸고 싶은 변수, 바꾸고 싶은 "클래스")
/* Using a feign client */
/* Feign exception handling */
// List<ResponseOrder> orderList = orderServiceClient.getOrder(userId);
// List<ResponseOrder> orderList = null;
// try {
// orderList = orderServiceClient.getOrder(userId);
// } catch (FeignException ex) {
// log.error(ex.getMessage());
// }
/* ErrorDecoder */
List<ResponseOrder> orderList = orderServiceClient.getOrder(userId);
userDto.setOrders(orderList);
return userDto;
}
에러 처리 뿐만 아니라, HTTP Response를 위한 상태코드(응답코드)와 메시지 등을 제어 할 수 있다는 것은 사용자에게 보여주기 위한 용도보다는 클라이언트 애플리케이션(Frontend)를 개발하는 개발자에게 더 유용한 정보가 될 수 있습니다. 강의에서는 클라이언트 페이지나 프론트엔트를 적용하지 않고, 그냥 RESTful API의 결과 값만을 보여주고 있지만, 실 서비스에서는 사용자에게 직접적인 에러메시지를 보여주는 것보다는 제한적인 메시지를 등을 디자인된 형식으로 보여줘야 합니다. 예를 들어 JSON의 데이터 포맷이 아니라, 테이블형태로 디자인된 모습으로 보여지는 것도 프론트단에서 처리해 주어야 합니다.
프론트엔드와 백엔드 사이에서 정해진 규칙으로 데이터를 주고 받고, 적절한 예외 처리 및 에러 핸들링을 위해 에러 코드를 제어하는 것이 좋습니다.
추가로 질문하신 orderList의 null처리는 user의 상세보기(GET) 에서 제외 하시거나, Filter 또는 새로운 ResponseDTO나 API 등으로 처리하시면 됩니다. 예를 들어 /users/edowon/orders 이런 식으로 /users/edowon 을 'edowon'이라는 사용자의 상세보기라 가정하고, 'edowon' 계정의 주문 목록은 /users/edowon/orders, 주문의 상세 보기는 /users/edowon/orders/ord00001 과 같이 처리해 보실 수 있습니다.
방법 3) .yml 등 설정파일에 등록
user-service.yml
order_service:
url: http://order-service/order-service/%s/orders
exception:
orders_is_empty: User's orders is empty.
UserServiceImpl
@Component
public class FeignErrorDecoder implements ErrorDecoder {
private Environment env;
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")) { // 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;
}
}
UserServiceApplication
@Component로 등록해 줬기 때문에 userServiceApplication.java에서 등록했던 빈은 없애준다.
// @Bean
// public FeignErrorDecoder FeignErrorDecoder() {
// return new FeignErrorDecoder();
// }
값을 조회해보자.