MVC에서 WebFlux로, 트러블슈팅과 성능 비교

full-boram·2025년 5월 6일
0
post-thumbnail

배경

앞서 난 Spring Cloud Gateway를 활용해 비동기 인증 시스템을 설계했고, Gateway는 WebFlux 기반으로 안정적인 요청 처리를 수행하고 있었다. 하지만 인증 서버는 여전히 Spring MVC 기반으로 구성되어 있었고, 이는 시스템 전반의 병목을 유발하는 주요 원인이었다.

특히 인증 요청이 증가함에 따라 Gateway → Auth 서버 간 통신에서 발생하는 블로킹 처리는 전체 서비스의 응답 속도 저하로 이어졌고, 이는 사용자 경험에도 영향을 미치기 시작했다. 이러한 문제를 해결하고자 인증 서버를 완전한 WebFlux 기반 비동기 구조로 리팩토링하게 되었다.

이번 글에서는 MVC → WebFlux로 전환하며 마주한 문제들과 그 해결 방식, 그리고 구조적으로 어떤 변화가 있었는지를 상세히 정리해본다.


내용

주요 변경 사항

1. MVC 기반 한계 제거

기존에는 @Transactional, JpaRepository, WebMvcConfigurer 등 MVC 전용 기술 스택을 사용하고 있었다. 하지만 WebFlux 환경에서는 다음과 같은 제약이 존재했다:

  • @Transactional은 동기 방식에서만 작동 → Reactor의 TransactionalOperator 등으로 대체 필요
  • JpaRepository는 동기 처리 방식 → ReactiveCrudRepository로 대체
  • WebMvcConfigurer는 작동하지 않음 → WebFluxConfigurer로 전환 필요

이러한 구조적 제약들을 해결하며 모든 계층을 비동기로 재구성했다. 처음엔 마치 전체 코드를 다시 짜야 하는 듯한 막막함이 있었지만, 핵심적인 로직을 흐름 단위로 쪼개고 Mono, Flux로 구성하니 오히려 더 명확하게 구조가 정리되었다.




2. FeignClient → WebClient 전환

Kakao 로그인은 기존에 FeignClient 기반으로 구현되어 있었으나, 이는 blocking 방식이기 때문에 WebFlux 구조에 맞지 않았다. 따라서 WebClient를 활용한 비동기 방식으로 전면 교체하였다.

public Mono<KakaoUserResponse> getUserInfo(String accessToken) {
    return webClient.get()
        .uri("/v2/user/me")
        .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
        .retrieve()
        .bodyToMono(KakaoUserResponse.class);
}



3. 서비스 전반에 Mono 적용

  • @Controller, @Service, @Repository 전 계층에서 모든 리턴 타입을 Mono<T> 로 통일
  • DB 저장 및 조회, 사용자 인증, 토큰 발급 등 모든 로직을 비동기 스트림으로 처리
  • 기존 MVC의 ResponseEntity<T>Mono<ResponseEntity<T>>로 변환
public Mono<ResponseEntity<UserInfo>> getUserInfo(@CurrentUser UserInfo userInfo) {
    return Mono.just(ResponseEntity.ok(userInfo));
}

이 과정에서 가장 신경 썼던 건 예외 처리였다. try-catch가 아닌 .onErrorResume()이나 .switchIfEmpty()를 활용해 흐름 안에서 에러를 유연하게 처리하는 방식은 리액티브 스타일에 익숙해지는 데 큰 도움이 되었다.




4. JWT 인증 필터 재구현

Spring MVC에서는 OncePerRequestFilter를 활용했지만, WebFlux에서는 WebFilter로 대체해야 한다.

  • JwtAuthenticationFilterWebFilter로 전면 재작성
  • 인증 로직을 Reactor Context에 주입 (ReactiveSecurityContextHolder 활용)
  • ReactiveAuthenticationManager, ServerAuthenticationConverter도 함께 구현
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    String token = extractToken(exchange);
    ...
    return chain.filter(exchange)
             .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
}

리액티브 환경에서는 인증 컨텍스트를 명시적으로 contextWrite를 통해 넘겨줘야 한다는 점이 새로웠고, 초반엔 생소했지만 체계적으로 접근하니 이해할 수 있었다.




5. @CurrentUser 커스텀 어노테이션도 리액티브하게 전환

WebFlux에서는 기존 MVC의 HandlerMethodArgumentResolver가 동작하지 않는다. 이를 해결하기 위해 WebFlux 전용 인터페이스로 전환했다:

  • HandlerMethodArgumentResolverorg.springframework.web.reactive.result.method.HandlerMethodArgumentResolver 로 변경
  • resolveArgument() 메서드를 Mono 기반으로 리팩토링
@Override
public Mono<Object> resolveArgument(...) {
    String userJson = exchange.getRequest().getHeaders().getFirst("X-Passport-User");
    return Mono.just(objectMapper.readValue(userJson, UserInfo.class));
}

처음에는 WebFlux에서 이런 커스텀 어노테이션을 그대로 쓸 수 있을지 걱정이 많았지만, 다행히도 Spring에서의 확장성과 모듈화가 잘 되어 있어 이 방식으로 무리 없이 이식이 가능했다.




6. 예기치 않은 응답 포맷과 YML 설정 누락 트러블슈팅

초기에는 API 응답이 예상한 형태가 아닌, 아래 이미지처럼 단순 JSON 포맷으로 내려오는 문제가 발생했다.

scanAvailable 응답 예시

이는 내가 커스텀한 응답 구조가 아닌, WebFlux 기본 JSON 렌더링 방식이 활성화된 결과였다. 디버깅 과정에서 알게 된 핵심 원인은 application.yml 내 WebFlux 관련 설정이 누락되어 있었던 점이었다.

spring:
  main:
    web-application-type: reactive

이 설정을 추가한 후, 기대한 형태의 커스텀 응답 포맷이 정상적으로 동작하였고, 기존 필터 및 예외 처리 로직도 예상대로 반영되었다. 이 경험을 통해 WebFlux에서는 YML 설정 하나가 전체 응답 흐름에 큰 영향을 미친다는 사실을 다시 한번 체감할 수 있었다.




트러블슈팅 및 고려사항

✅ Redis는 여전히 Reactive 미지원

  • Spring Data Redis는 완전한 리액티브를 지원하지 않음
  • 따라서 TokenRepository는 기존 CrudRepository로 유지 (단, 호출부는 Mono 흐름 내에서 wrapping 처리)

✅ Swagger

  • 기존 springdoc-openapi-starter-webmvc-uispringdoc-openapi-starter-webflux-ui 로 변경
  • WebFlux 환경에서는 일부 어노테이션의 동작 방식이 달라지므로 문서화 시 주의가 필요함

✅ Spring Security

  • SecurityFilterChain 설정도 WebFlux 방식 (ServerHttpSecurity)으로 전환
  • AccessDeniedHandler, AuthenticationEntryPoint도 Reactive 방식으로 재작성
  • 기존 HttpSecurity 설정들과 충돌이 발생하지 않도록 설정 분리 필요


성능 비교

WebFlux 구조 전환 후 성능 분석

Test tool: K6, 환경: AWS EC2(t2.micro), RDB, 테스트 API: /api/v1/user/me

테스트 시나리오

  • Warm-up: 1분 (VU 50)
  • 증가 단계 1: 2분 (VU 200)
  • 증가 단계 2: 1분 (VU 300)
  • 종료 단계: 1분

주요 결과 요약 (3회 테스트 평균 기준)

항목Spring MVCWebFlux개선 여부
총 요청 수14,10819,742+39.8%
평균 응답 시간2.32초1.60초-31.4%
p95 응답 시간11.6초5.4초-52.9%
최대 응답 시간평균 38.6초평균 25.5초
iteration 평균 시간2.94초2.11초
경고 발생다수13회 이하

결론

  • WebFlux 전환은 단순 리팩토링 그 이상으로, 성능 최적화와 안정성 확보에 실질적인 효과를 가져왔다.
  • 특히 평균 처리량 40% 증가, 응답 속도 30% 개선은 인증 서버와 같이 요청이 집중되는 서비스에서 큰 의미를 가진다.( 최대치로는 평균 처리량 48% 증가, 응답 속도 38% 개선되었다.)

마치며

이번 리팩토링은 단순히 MVC 코드를 Mono로 바꾸는 작업이 아니었다. 구조적인 전환, 기술 스택의 교체, API 응답 구조의 통일 등 전반적인 비동기 아키텍처로의 재설계였다.

리액티브 프로그래밍은 분명 러닝 커브가 존재했다. 오류 메시지 한 줄, 디버깅 한 단계에 몇 시간을 소비하기도 했다. 하지만 그 과정을 거치며 얻은 것은 단순한 코드 최적화가 아니라 서비스 구조 전체에 대한 깊은 고민이었다.

특히 MSA 구조에서 인증 서버는 병목을 유발하기 쉬운 중요한 지점이기 때문에, 이 구조적 전환은 시스템 전반의 확장성과 안정성을 확보하는 데 큰 의미가 있었다.

profile
공유로 가득찬 사람이 되고싶습니다.

0개의 댓글