- Domain , Service Interface, Util 등은 공통으로 사용하는 모듈에 적용되어 전역적으로 쓰이기도한다.
- 공통적으로 사용되는 소스 들은 멀티모듈에서 사용되는 것으로 보이며, 각 서비스로 환경이 갈리면 어려울 것 같다.
- RestTemplate 를 구성하는 방법은 여러가지이나 일단 2.x 대의 Boot 에서는 @Bean 으로 등록해야한다.
@SpringBootApplication(scanBasePackages = "com.study")
public class ProductCompositeServiceApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ProductCompositeServiceApplication.class, args);
}
}
- RestTemplate 의 getForObject() 는 굉장히 TypeSafe 하며, JSON 응답을 Object 로 맵핑할 수 있다.
- RestTemplate 의 exchange 는 ResponseEntity 를 리턴하며 이를 통해 연계된 메서드로 정보를 추출할 수 있다.
- exchange() 로 가져온 정보는 Collection 에서 사용되는 Generic 을 정하지 않기 때문에 (런타임에 정해진다...나..?) 스프링에서 제공하는 Helper Class 인 PrameterizeTypeReference<T> 를 사용한다.
@Override
public Product getProduct(int productId) {
try{
String url = productServiceUrl + productId;
LOG.debug("Will call getProduct API on URL: {}",url);
Product product = restTemplate.getForObject(url,Product.class);
LOG.debug("Found a product with id: {}",product.getProductId());
return product;
}catch (HttpClientErrorException ex){
switch (ex.getStatusCode()){
case NOT_FOUND:
throw new NotFoundException(getErrorMessage(ex));
case UNPROCESSABLE_ENTITY:
throw new InvalidInputException(getErrorMessage(ex));
default:
LOG.warn("Got a unexpected HTTP error: {}, will rethrow it",ex.getStatusCode());
LOG.warn("Error body: {}",ex.getResponseBodyAsString());
throw ex;
}
}
}
private String getErrorMessage(HttpClientErrorException ex) {
try {
return objectMapper.readValue(ex.getResponseBodyAsString(), HttpErrorInfo.class).getMessage();
}catch (IOException ioex){
return ex.getMessage();
}
}
//ResTemplate 를 이용하여 다른 서비스에서 가져온 데이터를 가져온다.
@Override
public List<Recommendation> getRecommendations(int productId) {
try{
String url = recommendationServiceUrl + productId;
LOG.debug("Will call getRecommendation API on URL: {}",url);
List<Recommendation> recommendations = restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<List<Recommendation>>() {}).getBody();
LOG.debug("Found {} recommendations for a product with id: {}", recommendations.size(), productId);
return recommendations;
}catch (Exception ex){
LOG.warn("Got an exception while requesting recommendations, return zero recommendations: {}", ex.getMessage());
return new ArrayList<>();
}
}
- 내가 원하는 정보로 MS 에서 받은 정보를 조작하기 위해서 Stream 사용이 자주 일어난다.
- 즉 Lambda 도 많이 사용된다는 것이다.
private ProductAggregate createProductAggregate(Product product, List<Recommendation> recommendations, List<Review> reviews, String serviceAddress) {
//1. 프로덕트 정보를 세팅합니다.
int productId = product.getProductId();
String name = product.getName();
int weight = product.getWeight();
//2. RecommendationSummary 로 맵핑하는 과정
List<RecommendationSummary> recommendationSummaries = (recommendations == null) ? null :
recommendations.stream()
.map(r -> new RecommendationSummary(r.getRecommendationId(),r.getAuthor(),r.getRate()))
.collect(Collectors.toList());
//3. ReviewSummary 로 맵핑하는 과정
List<ReviewSummary> reviewSummaries = (reviews == null) ? null :
reviews.stream()
.map(r -> new ReviewSummary(r.getReviewId(),r.getAuthor(),r.getSubject()))
.collect(Collectors.toList());;
//4. 마이크로 서비스의 어드래스를 가져오는 과정
String productAddress = product.getServiceAddress();
String reviewAddress = (reviews != null && reviews.size() > 0) ? reviews.get(0).getServiceAddress() : "";
String recommendationAddress = (recommendations != null && recommendations.size() > 0) ? recommendations.get(0).getServiceAddress() : "";
ServiceAddresses serviceAddresses = new ServiceAddresses(serviceAddress, productAddress, reviewAddress, recommendationAddress);
return new ProductAggregate(productId,name, weight, recommendationSummaries, reviewSummaries, serviceAddresses);
}
- API 단의 인터페이스를 작성하는 방법은 구현과 정의를 분리 시키는 아주 좋은 방법이다.
public interface RecommendationService {
/**
* Sample usage: curl $HOST:$PORT/recommendation?productId=1
*
* @param productId
* @return
*/
@GetMapping(
value = "/recommendation",
produces = "application/json")
List<Recommendation> getRecommendations(@RequestParam(value = "productId", required = true) int productId);
}
- SpEL 사용은 굉장히 유연하며, 사용하기에 따라 정말 유용해진다.
- 생성자 의 파라미터에서 굳이 맞는 파라미터만이 아니라, 다른 파라미터를 가져와 유연하게 가져가는 것을 기억하자.
@Component
public class ProductCompositeIntegration implements ProductService, ReviewService, RecommendationService {
private static final Logger LOG = LoggerFactory.getLogger(ProductCompositeIntegration.class);
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private final String productServiceUrl;
private final String recommendationServiceUrl;
private final String reviewServiceUrl;
// SpEL 로 서버 포트와 호스트를 가져옴 이를 조합하여 Url 생성
@Autowired
public ProductCompositeIntegration(
RestTemplate restTemplate,
ObjectMapper objectMapper,
@Value("${app.product-service.host}") String productServiceHost,
@Value("${app.product-service.port}") int productServicePort,
@Value("${app.recommendation-service.host}") String recommendationServiceHost,
@Value("${app.recommendation-service.port}") int recommendationServicePort,
@Value("${app.review-service.host}") String reviewServiceHost,
@Value("${app.review-service.host}") int reviewServicePort
) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
this.productServiceUrl = "http://" + "http://" + productServiceHost + ":" + productServicePort + "/product/";
this.recommendationServiceUrl = "http://" + recommendationServiceHost + ":" + recommendationServicePort + "/recommendation?productId=";
this.reviewServiceUrl = "http://" + reviewServiceHost + ":" + reviewServicePort + "/review?productId=";
}
- 기본 Domain 을 수정할 필요없이 내가 원하는 Domain 클래스를 생성해 이를 통합시키는 방법도 고려된다.
public class ProductAggregate {
private final int productId;
private final String name;
private final int weight;
private final List<RecommendationSummary> recommendations;
private final List<ReviewSummary> reviews;
private final ServiceAddresses serviceAddresses;
public ProductAggregate(
int productId,
String name,
int weight,
List<RecommendationSummary> recommendations,
List<ReviewSummary> reviews,
ServiceAddresses serviceAddresses) {
this.productId = productId;
this.name = name;
this.weight = weight;
this.recommendations = recommendations;
this.reviews = reviews;
this.serviceAddresses = serviceAddresses;
}
public int getProductId() {
return productId;
}
public String getName() {
return name;
}
public int getWeight() {
return weight;
}
public List<RecommendationSummary> getRecommendations() {
return recommendations;
}
public List<ReviewSummary> getReviews() {
return reviews;
}
public ServiceAddresses getServiceAddresses() {
return serviceAddresses;
}
}
전역 컨트롤러 예외처리를 위해 @RestControllerAdvice 를 사용할때에 @ResponseStatus 를 채용하는 것을 생각해보자.
- @ResponseStatus 로 응답할 상태코드를 정의한다.
- HttpErrorInfo 등의 원하는 정보를 담은 객체를 만들어 Json 응답으로 넘겨주는 것도 좋은 방법이다.
@RestControllerAdvice
public class GlobalControllerExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(GlobalControllerExceptionHandler.class);
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NotFoundException.class)
HttpErrorInfo handleNotFoundExceptions(ServerHttpRequest request, Exception ex) {
return createHttpErrorInfo(NOT_FOUND, request, ex);
}
@ResponseStatus(UNPROCESSABLE_ENTITY)
@ExceptionHandler(InvalidInputException.class)
public @ResponseBody HttpErrorInfo handleInvalidInputException(ServerHttpRequest request, Exception ex) {
return createHttpErrorInfo(UNPROCESSABLE_ENTITY, request, ex);
}
private HttpErrorInfo createHttpErrorInfo(HttpStatus httpStatus, ServerHttpRequest request, Exception ex) {
final String path = request.getPath().pathWithinApplication().value();
final String message = ex.getMessage();
LOG.debug("Returning HTTP status: {} for path: {}, message: {}", httpStatus, path, message);
return new HttpErrorInfo(httpStatus, path, message);
}
}
마이크로 서비스를 하나의 멀티모듈로 작게 구현해보고 있지만, 구현하면서 드는 생각은 마치 종합선물세트 같다는 것이다.
**종합 선물 세트는 받는 입장에서는 굉장히 좋으나, 만드는 입장에서는 거시적인 관점, 유기적관계를 파악하는 방법, 사고의 유연성, 필요한 기술들로 가득 차 있는 듯 하다. 그 점이 아주아주 흥미로운데, 결코 쉽지는 않은 것 같다.