Post

MSA 구현하기(9) - 마이크로 서비스에도 WebFlux 적용하기 (1)

Skala 과정에서 마이크로 서비스 아키텍처 구조에 대해 새롭게 알게 되었습니다.
배운 것을 실제로 구현해보기 위해 이 여정을 시작하기로 했습니다.
처음 글부터 보러가기

아무래도 MSA가 최근(?) 기술이다보니 관련 블로그에서
오래된 기술인 MVC 보다 WebFlux를 사용하는 코드가 더 많이 보였습니다.

동기식과 비동기식으로 동작한다는 것만 알고 있었고, 실제로 MSA 프로젝트에도 적용을 하고자 했는데,
MVC와 WebFlux를 섞어서 사용하면, WebFlux를 사용하는 의미가 없다고…해서…..

그래서 MVC와 WebFlux에 대해 더 자세히 알고 제대로(!!) 적용해 보겠습니다,,


Spring MVC란?

Spring MVC는 Servlet API 기반의 전통적인 웹 프레임워크로, 익숙하고 사용하기 쉬워 대부분의 웹 애플리케이션에서 널리 사용됩니다.

✔️ Blocking 방식
✔️ 동기 처리
✔️ 개발자 친화적인 구조


Spring WebFlux란?

Spring WebFlux는 비동기 웹 프레임워크로, 동시성 처리가 뛰어나고, 대용량 트래픽에 적합합니다.

⚡ Non-blocking 방식
⚡ 비동기 스트림 처리
⚡ 함수형, 선언형 스타일


비교하기

항목Spring MVCSpring WebFlux
처리 방식동기 (Blocking)비동기 (Non-blocking)
기반Servlet API (Tomcat 등)Reactive Streams (Netty 등)
동시성 처리Thread per Request이벤트 루프 기반
성능소규모 트래픽에 빠름대규모 동시 요청에 강함
학습 난이도낮음 (개발자에게 익숙함)높음 (리액티브 개념 필요)
라이브러리 호환성대부분 지원일부 동기 라이브러리와 호환 불가

✅ Spring MVC가 적합한 경우

  • 일반적인 웹 서비스, 관리형 어드민 페이지
  • 개발자가 많고 리액티브에 익숙하지 않을 때
  • 요청/응답이 단순하고 동기 구조가 충분할 때

✅ Spring WebFlux가 적합한 경우

  • 수천~수만 동시 요청 처리 필요
  • 실시간 데이터 스트리밍 처리
  • 외부 API 연동이 많은 서비스
  • 마이크로서비스 간 비동기 통신이 필요한 구조

WebFlux를 사용하면 마이크로서비스 간 통신 시 통신 속도에 굉장한 이점이 생깁니다!!
그래서 저는 WebFlux를 이용하기로 했습니다


WebFlux는 어떻게 적용하나요?

pom.xml

1
2
3
4
5
6
7
8
9
10
.
.
.
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
.
.
.

기존에 spring-boot-starter-web 을 포함하고 있었다면 webflux 로 바꿔주기만 하면 됩니다.


UserController.java (User Server)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    .
    .
    .

    @GetMapping("/list")
    public Flux<UserInfoResponse> getAllUsers(
            @RequestHeader("x-user-id") Long userId,
            @RequestHeader("x-user-role") UserRole role
    ) {
        log.info("user id({}) accessed to find all users", userId);
        RoleCheck.isAdmin(role);

        return Flux.defer(() ->
            Flux.fromIterable(userService.getAll())
        );
    }

    @GetMapping("/{userId}")
    public Mono<UserInfoResponse> getUserById(
            @PathVariable("userId") Long targetId,
            @RequestHeader("x-user-id") Long userId,
            @RequestHeader("x-user-role") UserRole role
    ) {
        log.info("user id({}) accessed to find user({})", userId, targetId);
        RoleCheck.isAdmin(role);

        return Mono.fromSupplier(() -> userService.getUser(targetId));
    }

    .
    .
    .
}

기존 코드는 ResponseEntity<UserInfoResponse>, ResponseEntity<List<UserInfoResponse>> 와 같은 형태로 반환했지만
webflux를 적용하여 List는 Flux, 단건 반환은 Mono 로 바꿔주었습니다.

이렇게 컨트롤러에서 반환할 때 Mono, Flux로 변환되도록 작성하면
UserService 내 코드를 바꾸지 않아도 됩니다.


Mono와 Flux의 호출 방식이 왜 다른가요?

비동기식으로 반환하기 위해 위 방식으로 데이터를 반환했습니다!

Mono와 Flux를 완벽하게 비동기식으로 동작하게 만들기 위해 각각 호출 방식을 정리해보겠습니다.


Mono의 호출 방식

항목Mono.just(...)Mono.fromSupplier(...)Mono.fromCallable(...)Mono.defer(...)
🧠 실행 시점즉시 실행됨 (Mono 만들 때)구독 시 실행됨 (Lazy)구독 시 실행됨 (Lazy)구독 시 Mono 자체를 생성 (더 Lazy)
💥 예외 처리예외가 Mono 밖에서 터짐 ❌RuntimeException만 Mono 흐름 안에서 처리 가능 ✅모든 예외를 Mono.error로 처리 가능 ✅모든 예외를 Mono 흐름 안에서 처리 가능 ✅
⚠️ CheckedException 지원❌ 불가❌ 불가✅ 가능 (예: IOException 등)✅ 가능
🧪 @ControllerAdvice 적용❌ 예외 바깥에서 발생 → 안 잡힘✅ 적용 가능✅ 적용 가능✅ 적용 가능
🔁 subscribe할 때 새로 실행❌ 동일한 값✅ 새로 실행✅ 새로 실행✅ 새로 Mono 생성
💡 추천 사용 상황값이 확정돼 있고, 예외 발생 가능성 없음가볍고 단순한 연산, 예외 거의 없음예외 발생 가능성 높은 로직조건에 따라 다른 Mono를 만들거나 예외 처리 분기 필요할 때

제가 호출하는 API는 가볍고 단순한 연산이며, RuntimeException 종류의 예외만 발생하기 때문에 fromSupplier()를 사용했습니다.

Mono.just()는 비동기식으로 동작하지 않고, 예외를 처리할 수 없기 때문에 사용하지 않았습니다.

만약 RuntimeException이 아닌 IOException과 같은 예외가 발생하는 로직을 실행할 때에는 fromCallable()을 사용거나,
분기가 많은(if-else가 긴) 로직을 실행할 때에는 defer()를 사용하는 것이 좋을 것 같습니다.

defer() 사용 시 매번 새로운 Mono 객체가 생성되기 때문에 메모리 낭비 같은 오버헤드가 발생할 수 있습니다.


Flux의 호출 방식

✅ Flux 호출 방식 비교표

항목Flux.just(...)Flux.fromIterable(...)Flux.defer(...)Flux.interval(...)
🧠 실행 시점즉시 실행됨즉시 실행됨 (List 먼저 만들어짐)구독 시 실행됨 (완전 지연 실행)구독 시 실행됨 (스케줄러 기반)
💥 예외 처리Flux 외부에서 발생 ❌Flux 외부에서 발생 ❌Flux 흐름 안에서 Flux.error() 가능 ✅예외보다 타이밍/스레드 이슈 주의
🔁 subscribe 시 새로 실행❌ 동일한 Flux❌ 동일한 결과 반복✅ 매번 새 Flux 생성✅ 계속 emit (무한 스트림 주의)
🚫 진짜 비동기인가?❌ 아님❌ 아님✅ 진짜 비동기 흐름✅ 내부적으로 비동기
💡 추천 사용 상황고정 값 반환 시고정 리스트 흘려보낼 때동적 Flux, 예외 처리, 구독마다 실행할 때실시간 데이터/알람, 주기적 스트림

유저의 정보를 리스트 형태로 반환하는 API이기 때문에
예외처리가 필요하고, 비동기 형태로 동작하는 defer() 를 사용했습니다.

just(), fromIterable()은 Mono의 호출 방식에서 설명했듯이 List 객체가 이미 만들어진 후에 실행되기 때문에 비동기식으로 동작하지 않는 코드입니다.

interval()은 일정 간격으로 계속해서 값을 반환하는 실시간 stream성 호출 방식이기 때문에 현재 쓰임과는 맞지 않아 사용하지 않았습니다.
(GPT가 알림 용으로 사용하는 것이 가장 적절하다고 합니다,,)


이제 진짜 완전 비동기식으로 바뀐건가요?

아니요!

DB에서부터 비동기식으로 불러와야 완전 비동기식이라고…합니다….
그래서 이 글의 제목에 (1) 이 붙어있습니다. ㅎㅎ

이제부터 찬찬히 DB까지 완전히 비동기식으로 동작하도록 만들어보겠습니다,, 스불재
비동기식으로 동작하는 PostgreSQL 을 쓰는 것이 가장 좋지만,
mariaDBR2DBC 공식은 아니지만 드라이버가 있다고 해서 이 방식으로 진행해볼 것 같습니다,,

Controller와 Service, Repository, Entity 코드가 다 바뀝니다..! 화이팅 ㅜㅜ


마치며

뭔가 고난과 역경이 느껴지지만 의외로 간단할 것 같기도 한 디비 설정은 다음 글부터 진행해보겠습니다..!
🤯🚨

다음 글은 며칠이 걸릴 지 모르겠어요



참고 자료

하나, , , , 다섯, 여섯

This post is licensed under CC BY 4.0 by the author.