준호씨의 블로그

Spring - 본격 WebFlux는 다음 기회에 본문

개발이야기

Spring - 본격 WebFlux는 다음 기회에

준호씨 2020. 6. 19. 23:13
반응형

새로운 프로젝트를 하면서 기존에 프로젝트 환경 세팅해둔 프로젝트를 이어받았는데요. API 서버 프로젝트이고 Spring Boot WebFlux로 되어 있었습니다.

프로젝트를 이어받으면서 WebFlux 쓰지 말고 MVC로 다시 만드는 게 좋겠다는 의견을 들었는데요. 디버깅도 어렵고 익숙한 방식도 아니고 더욱이 비동기 구현해서 퍼포먼스를 끌어올려야 될 이유도 없다는 이유들이었습니다.

맞는 말들이었긴 하지만 그래도 괜히 WebFlux로 한번 계속 진행해 보고 싶더군요. 그래서 한 동안 WebFlux 공부를 하면서 진행해 보았습니다.

MVC와 WebFlux

MVC와 WebFlux를 간단한 Controller코드로 구현해 보았습니다. 

// MVC
@GetMapping("/members")
public Member getMember(@RequestParam("id") String id) {
    return memberService.getMember(id);
}

// WebFlux
@GetMapping("/members")
public Mono<Member> getMember(@RequestParam("id") String id) {
    return memberService.getMember(id);
}

별다른 차이는 없어 보입니다. 자세히 보면 리턴 타입이 Member에서 Mono<Member>로 바뀌었습니다. 그런데 리턴 타입이 바뀌었으니 memberService.getMember 메서드도 리턴 타입이 다른 게 필요하겠죠?

구현을 하다 보니 memberService.getMember에서 MemberServiceException이 발생한 경우 ApiException으로 바꿀 일이 생겼습니다. 그러면 아래처럼 구현을 하게 됩니다.

// MVC
@GetMapping("/members")
public Member getMember(@RequestParam("id") String id) {
    try {
        return memberService.getMember(id);
    } catch (MemberServiceException e) {
        throw new ApiException(e);
    }
}

// WebFlux
@GetMapping("/members")
public Mono<Member> getMember(@RequestParam("id") String id) {
    return memberService.getMember(id)
        .onErrorMap(e -> {
            if (e instanceof MemberServiceException e) {
                return new ApiException(e);
            }
            return e;
        });
}

구현 스타일이 많이 달라집니다. MVC방식은 일반적인 Java 프로그래밍 방식과 비슷합니다. WebFlux에서는 onErrorMap이라는 특이한 메서드에서 함수형 방식으로 코드를 구현합니다. 이벤트 핸들러를 미리 지정해 두고 이벤트가 발생하면 동작할 방식을 구현하게 됩니다.

여기서 onErrorMap은 operator(연산자)라고 하는데요. onErrorResume, onComplete, reduce, map, flatMap, concatMap 등등 다양한 연산자들이 존재합니다. 용도에 맞게 사용할 수 있도록 미리 숙지 해 두는 것이 좋습니다. 다만 이러한 연산자들이 꽤 많습니다.

WebFlux에서는 이러한 연산자들을 구현한 Reactor라는 라이브러리를 사용하고 있습니다. Reactor 라이브러리에 대한 설명은 https://projectreactor.io/ 에서 볼 수 있습니다.

연산자들에 대한 설명은 아래 페이지에서 확인할 수 있습니다.

 

Mono (reactor-core 3.3.6.RELEASE)

static  Mono using(Callable  resourceSupplier, Function > sourceSupplier, Consumer  resourceCleanup, boolean eager) Uses a resource, generated by a supplier for each individual Subscriber, while streaming the value from a Mono derived from the same re

projectreactor.io

Mono에서 사용할 수 있는 연산자들인데요. 꽤 많은 데다가 사용할 만큼 이해하려면 공부를 좀 해야 됩니다. Mono는 0개나 1개의 값에 대한 처리를 위한 용도인데요. Flux라고 0개-N개의 값이 대한 처리를 하는 녀석이 있는데 이건 연산자가 더 많습니다.

WebFlux방식으로 잘만 구현하면 좀 더 통일성 있는 코드를 구현하는데 좋을 거 같긴 합니다만 아무래도 아직 익숙하지 않다 보니 삽질을 많이 하게 되더군요. 게다가 디버깅도 익숙하지 않아서 쉽지 않았습니다.

이런저런 영상과 글들을 보면서 공부를 하던 도중 다음 글을 읽게 되었습니다.

 

사용하면서 알게 된 Reactor, 예제 코드로 살펴보기

Reactor는 Pivotal의 오픈소스 프로젝트로, JVM 위에서 동작하는 논블럭킹 애플리케이션을 만들기 위한 리액티브 라이브러리입니다. Reactor는 RxJava 2와 함께 Reactive Stream의 구현체이기도 하고, Spring Fra

tech.kakao.com

과일바구니 예제라고 과일 바구니 3개가 있고 각 종류별 개수를 구하는 문제가 있습니다. 단순히 이전에 짜던 방식으로 구현하면 아래처럼 구현할 수 있습니다.

        for (List<String> basket : baskets) {
            HashMap<String, Integer> temp = new HashMap<>();
            for (String fruit : basket) {
                Integer integer = temp.putIfAbsent(fruit, 1);
                if (integer != null) {
                    temp.put(fruit, integer + 1);
                }
            }
            System.out.println(temp);
        }

Reactor를 이용해서 구현해 보았습니다.

        Flux.fromIterable(baskets)
                .map(basket -> {
                    Flux.fromIterable(basket)
                            .groupBy(fruit -> fruit)
                            .concatMap(gf -> gf.count()
                                    .map(count -> {
                                        final Map<String, Long> fruitCount = new HashMap<>();
                                        fruitCount.put(gf.key(), count);
                                        return fruitCount;
                                    })
                            )
                            .reduce((accumulateMap, currentMap) -> {
                                final Map<String, Long> basketCount = new HashMap<>();
                                basketCount.putAll(accumulateMap);
                                basketCount.putAll(currentMap);
                                return basketCount;
                            })
                            .doOnNext(e -> {
                                System.out.println(e);
                            })
                            .subscribe();
                    return basket;
                })
                .subscribe();

동작은 하지만 제대로 구현한 건지는 모르겠습니다. 좀 더 간단하게 구현할 방법이 있지 않을까 싶은데 제 실력으로는 아직 알 수가 없습니다. 코드를 좀 줄인다고 해도 그냥 이전 방식이 훨씬 간단해 보입니다.

글의 내용을 이해해 보면 더 간단히 구현하는 방법이라던지 이 방법이 어떤 점에서 좋은지 알 수 있지 않을까 싶긴 한데요. 아무튼 제가 주목한 부분은 마지막 부분에 나오는 내용이었습니다.

따라서 Reactor를 사용하여 리액티브 프로그래밍을 해야 하는 상황을 고려한다면, 1) 필요한 이유, 2) 추후 코드 변경 시 유지보수성
, 3) 함께 협업하는 사람들의 숙련도나 관심 등을 고려하여 선택해야 한다고 생각합니다.

아직은 뚜렷이 필요한 이유는 없는 상태입니다. 일부 요청이 많아질 API에는 적용해 볼만 할 거 같지만 당장은 요청이 얼마나 많을지 알 수 없는 상황입니다. 트래픽이 많아지면 그때 변경을 해도 될 거 같습니다. 유지보수성은 현재 숙련도가 떨어지는 상태라 유지보수가 어렵다고 볼 수 있습니다. 숙련되고 나면 좀 나아지겠지만 아직은 숙련되어도 유지보수 하기 얼마나 좋을지 감이 잘 안옵니다. 협업하는 사람들의 숙련도나 관심 부분도 아직은 잘 모르겠습니다. 관심은 어느 정도 있어 보이지만 숙련된 분들이 많은 상태는 아닌 거 같습니다.

일단 WebFlux 본격 도입은 보류하려고 합니다. 당장 제 숙련도가 낮으니 일단 제 숙련도부터 끌어올리고 나서 본격적으로 도입할지 말지 다시 생각해 봐야겠습니다. 그렇다고 아예 안 쓴다는 건 아니고 일단 spring-boot-starter-webflux는 그대로 사용합니다. Http Client로 WebClient도 계속 사용할 것이고요. WebClient 관련해서는 다음 글에 좀 더 적어 보려고 합니다.

반응형
Comments