[일단 박죠] O2O Object Detection 스프링 서버 webClient 적용

jeyong·2024년 3월 5일
0

공부 / 생각 정리  

목록 보기
91/120
post-custom-banner


이번 게시글에서는 Spring Framework를 사용하여 서버를 개선하는 과정에서 RestTemplate에서 WebClient로의 전환과 관련된 리팩토링 작업을 설명한다.

1. webClient 적용 과정

1-1. RestTemplate이 아닌 webClient를 사용하도록 수정

기존 코드

 public List<Snack> analyzeSnacks(List<MultipartFile> images) {
        String baseUrl = flaskBaseUrl + "/detect";
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);

        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        images.forEach(image -> {
            try {
	@@ -66,22 +62,24 @@ public String getFilename() {
            }
        });

        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
        ResponseEntity<List<SnackDto>> responseEntity;
        try {
            responseEntity= restTemplate.exchange(baseUrl, HttpMethod.POST, requestEntity, new ParameterizedTypeReference<List<SnackDto>>() {});
        } catch (ResourceAccessException e) {
            throw new NullResponseFromApiException();
        }
        List<SnackDto> snackDtos = responseEntity.getBody();
        List<Snack> snacks = snackDtos.stream()
                .map(dto -> Snack.builder()
                        .filename(dto.getFilename())
                        .objectName(dto.getObject_name())
                        .position(new Position(dto.getPosition().getX1(), dto.getPosition().getX2(), dto.getPosition().getY1(), dto.getPosition().getY2()))
                        .build())
                .collect(Collectors.toList());
        snackRepository.saveAll(snacks);
        return snacks;
    }
}

바뀐 코드

   public List<Snack> analyzeSnacks(List<MultipartFile> images) {
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        images.forEach(image -> {
            try {
                Resource resource = new ByteArrayResource(image.getBytes()) {
                    @Override
                    public String getFilename() {
                        return image.getOriginalFilename();
                    }
                };
                body.add("images", resource);
            } catch (IOException e) {
                throw new ImageReadException(e);
            }
        });
        List<SnackDto> snackDtos = webClient.post()
                .uri("/detect")
                .contentType(MediaType.MULTIPART_FORM_DATA)
                .body(BodyInserters.fromMultipartData(body))
                .retrieve()
                .bodyToMono(new ParameterizedTypeReference<List<SnackDto>>() {})
                .onErrorResume(WebClientRequestException.class, e -> Mono.error(new NullResponseFromApiException()))
                .block();

        List<Snack> snacks = snackDtos.stream()
                .map(dto -> Snack.builder()
                        .filename(dto.getFilename())
                        .objectName(dto.getObject_name())
                        .position(new Position(dto.getPosition().getX1(), dto.getPosition().getX2(), dto.getPosition().getY1(), dto.getPosition().getY2()))
                        .build())
                .collect(Collectors.toList());
        snackRepository.saveAll(snacks);
        return snacks;
    }

webClient를 구현함에 있어 Database 부분에서 block 되므로 비동기가 아닌 동기작업을 수행하였다. R2DBC로의 전환도 고려해보았지만, 아직 R2DBC의 기술측면에서의 숙련도가 높지 않기 때문에 도입하지 않았다.
간단한 작업이었지만 webClient 사용해보며 리액티브 프로그래밍에 대해서 공부 할 수 있어서 좋은 시간이었다.

이러한 개선 사항들은 GitHub에서 확인할 수 있으며, 관련 커밋은 다음과 같다.

https://github.com/joon6093/O2O-Automatic-Store_Object-Detection_Demo/commit/15891802438f848738d4643c6a42963b5b8dc01e

1-2. webClient를 이용한 통신 로직을 추상화를 이용한 의존성이 역전 되도록 변경

주요 리팩토링 과정에는 통신 로직을 추상화를 이용한 의존성이 역전 되도록 변경하는 리팩토링도 수행하였다.
해당 과정은 다른 게시글을 통해 기술하였으므로 아래 링크를 첨부하고 넘어가겠다.

[되새김질] 추상화에 대한 고찰

2. 앞으로 진행할 것

webClient를 이용하였지만, spring MVC 환경의 블로킹 문제로 비동기성 달성에 어려움으로 비동기 방식으로의 전환은 실패했다. 또한 리액티브 프로그래밍에 대해서 공부하며 spring Webflux에 대한 기술적 갈증이 생기게 되었다. 그래서 자연스럽게 kotlin을 이용하여 springwebflux로 구현하면 어떨까라는 생각을 하게되었고 해당 프로젝트를 진행하려고 한다. 물론 이미 잘 동작하고 있는 MVC 프로젝트는 무리 해서 바꿀 필요는 없다고 하지만 학생 신분이니 지식을 쌓는다는 생각으로 도전할 생각이며, 성능향상에 초점에 맞추어 집중해보려고 한다. 아래에는 해당 프로젝트 진행할 링크이다.

GitHub: o2o_automatic_store_springwebflux_kotlin

또한 해당 게시글 작성 이전에 O2O 상점 시스템 구축을 위해 스프링 공부 목적으로 Monolithic Architecture 기반의 상점 관리 시스템을 구현하였다. 해당 프로젝트에 대한 내용도 시리즈에 포함시킬 예정이다. 아래는 해당 프로젝트를 진행한 깃허브 주소이다.

GitHub: O2O-Automatic-Store_Store-Management_Demo

아무튼 Monolithic Architecture 기반의 상점 관리 시스템을 구현을 끝마치고, 상점 관리 시스템과 객체 인식 시스템을 통합하는 최적의 방법에 대해 고민했다. 두개의 시스템을 별도로 구현하고 있기도 하였고, O2O 객체 인식 시스템의 springwebflux기반의 성능향상을 염두하고 있기 때문에 시스템간의 의존성이 없을 수 는 없을까 생각하며 공부하였고, 아래 영상을 접하게 되었다.

[우아콘2020] 배달의민족 마이크로서비스 여행기

현재 구현을 진행중인 O2O 상점 시스템과 비슷한 시스템을 가진 배달의 민족에서 Microservices Architecture 적용 과정을 살펴보니, 현재 진행중인 프로젝트에 적용하고 싶었다.
하지만 우아한 형제들에서도 장애를 극복하기 위한 생존의 문제라고 말했는 만큼 현재 트래픽이 발생하지도 않은 O2O 상점 시스템에 적용하기에는 오버엔지니어링이라고 생각한다.

그래서 Monolithic Architecture기반으로 두개의 시스템을 합치려고 하였으나, 연관도 없는 두개의 시스템을 Monolithic Architecture를 적용하면, 객체 인식 시스템의 장애가 상점 관리 시스템으로 전파될 수 있는 문제가 있다. 해당 문제를 해결하기 위해서 두개의 시스템간에 통신에만 메세지 큐 서비스를 이용해서 통신 과정을 구현하여, 독립적으로 작동시키는 방안을 생각해보게 되었다.

또한 앞에서 언급했던 객체인식시스템에서도 메세지 큐를 이용하여 스프링 서버와 플라스크 서버의 의존성이 분리되는 방향도 좋다고 생각한다. 왜냐하면 플라스크 서버의 오류가 발생하여도 스프링 서버에서 메시지 큐를 이용하여 데이터만 전달해놓기만 하면 후에 플라스크 서버가 재가동 되었을때 작동할 수 있기 때문이다. 물론 해당 방법과 springwebflux로 구현한 방법 중 선택을 위해서 O2O 상점 시스템의 목적에 기반하여서 무엇이 더 맞는 설계인지 비교해보아야한다. 지금은 단지 가능성을 기록하고 넘어가고 후에 해당 주제에 대해서 다루어 보도록 하겠다.

현재 해당 구조들에 대해서 고민하고 구현해며 장단점을 따지기에는 Microservices Architecture에 대한 지식이 부족하다. 그래서 일단 Microservices Architecture기반 프로젝트를 진행하고 앞에서 언급했던 궁금점들을 해소해나갈 생각이다. Microservices Architecture기반의 상점 시스템 프로젝트를 진행할 예정이며 아래에는 해당 프로젝트 진행할 링크이다.

GitHub: Microservices-Architecture_Study

3. 글을 마치며

프로젝트를 진행하는 목적은 단순히 기술적인 갈증을 해소하는 것뿐만 아니라, 스프링을 사용한 웹 백엔드 개발에 대한 깊은 흥미와 성장 욕구에서 비롯된다고 생각한다. 학생 신분으로서 다양한 기술적 실험을 통해 경험을 쌓는 것은 미래의 실무에 큰 도움이 될 것이라 생각한다.

이러한 접근 방식은 학습과 개인적인 성장에 있어서는 좋은 방식이지만, 실무 환경에서는 프로젝트의 요구사항, 팀의 역량, 유지보수성 등 다양한 요소를 고려해야 할 필요가 있음을 충분히 인식하고 있다는 것을 알아주면 좋겠다.

profile
노를 젓다 보면 언젠가는 물이 들어오겠지.
post-custom-banner

0개의 댓글