책 공부

프로젝트 빌드 구조에서 느낀 점

Jungsoomin :) 2020. 12. 15. 20:58
  • 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);
    }
}

 

마이크로 서비스를 하나의 멀티모듈로 작게 구현해보고 있지만, 구현하면서 드는 생각은 마치 종합선물세트 같다는 것이다.

 

**종합 선물 세트는 받는 입장에서는 굉장히 좋으나, 만드는 입장에서는 거시적인 관점, 유기적관계를 파악하는 방법, 사고의 유연성, 필요한 기술들로 가득 차 있는 듯 하다. 그 점이 아주아주 흥미로운데, 결코 쉽지는 않은 것 같다.