본문으로 건너뛰기

1. DispatcherHandler 소개

  • Spring WebFlux의 DispatcherHandler는 Spring MVC의 DispatcherServlet과 유사한 역할을 하는 핵심 컴포넌트입니다.
  • 두 프레임워크 모두 Front Controller 패턴을 기반으로 설계되었으며, 중앙 집중형 컴포넌트를 통해 요청을 처리합니다.
Front Controller 패턴이란?

Front Controller 패턴은 모든 웹 요청을 단일 진입점에서 처리하는 디자인 패턴입니다. 이 패턴의 주요 특징은:

  • 모든 요청이 하나의 컨트롤러를 통과
  • 요청에 대한 공통 처리 로직 중앙화
  • 보안, 로깅, 라우팅 등의 횡단 관심사를 효율적으로 처리
  • 뷰나 비즈니스 로직으로의 제어 흐름을 일관되게 관리

1.1 Front Controller 패턴 구현

  • Spring MVC: DispatcherServlet이 모든 웹 요청의 진입점 역할
  • Spring WebFlux: DispatcherHandler가 reactive 스택에서 같은 역할 수행

1.2 주요 특징

  • 공유 알고리즘을 통한 요청 처리
    • DispatcherHandler는 모든 요청에 대해 일관된 처리 알고리즘을 제공합니다.
  • 위임 컴포넌트를 통한 실제 작업 수행
    • 실제 비즈니스 로직의 처리는 각각의 전문화된 컴포넌트들에게 위임됩니다.
    • 예를 들어, HandlerMapping은 요청 경로 분석을, HandlerAdapter는 실제 핸들러 실행을, HandlerResultHandler는 결과 처리를 담당합니다.
  • 유연한 워크플로우 지원
    • 각 단계별로 다양한 구현체를 제공하고 있어, 개발자는 자신의 요구사항에 맞는 컴포넌트를 선택하거나 직접 구현하여 사용할 수 있습니다.
    • 예를 들어 @RequestMapping 기반의 핸들러뿐만 아니라 함수형 엔드포인트도 동일한 구조 내에서 처리할 수 있습니다.
  • Spring 설정을 통한 위임 컴포넌트 자동 발견
    • ApplicationContext를 통해 필요한 컴포넌트들을 자동으로 찾아 구성합니다.
    • 개발자는 원하는 컴포넌트를 Bean으로 등록하기만 하면 되며, DispatcherHandler가 이를 자동으로 찾아 적절한 시점에 사용합니다.

2. 기본 아키텍처

2.1 WebFlux 애플리케이션의 주요 구성요소

Spring WebFlux 애플리케이션은 다음과 같은 주요 컴포넌트들로 구성됩니다

  • webHandler라는 빈 이름을 가진 DispatcherHandler: 모든 웹 요청의 진입점으로, 전체 요청 처리 과정을 조율합니다.
  • WebFilter와 WebExceptionHandler
    • WebFilter는 요청이 DispatcherHandler에 도달하기 전에 가장 먼저 동작합니다
    • 인증, 로깅, CORS 처리 등 공통적인 전처리 작업을 수행합니다
    • 필터 체인을 통해 순차적으로 실행되며, 각 필터는 다음 필터로 요청을 전달합니다
    • WebExceptionHandler는 전체 처리 과정에서 발생하는 예외를 캐치하여 적절한 오류 응답을 생성합니다
  • DispatcherHandler 전용 빈들의 협력 과정
    • HandlerMapping이 요청 URL과 매칭되는 핸들러를 찾습니다
    • 찾은 핸들러는 HandlerAdapter를 통해 실행됩니다
    • 핸들러의 실행 결과는 HandlerResultHandler가 받아 최종 응답으로 변환합니다
    • 자세한 내용은 아래에서 다루겠습니다.

2.2 요청 처리 체인 구성

ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();

이렇게 구성된 HttpHandler는 서버 어댑터와 함께 사용할 준비가 완료됩니다.

2.3 ServerWebExchange 이해하기

  • ServerWebExchange는 Spring WebFlux에서 웹 요청-응답 교환을 나타내는 인터페이스입니다.
    • 쉽게 말해 "HTTP 요청/응답과 관련된 모든 정보를 담고 있는 컨테이너"입니다.
  • Spring MVC의 ServletRequest/ServletResponse와 유사한 역할을 하지만, 리액티브 스택에 맞게 설계되었습니다.

주요 특징과 역할

  • 요청-응답 컨텍스트
    • HTTP 요청과 응답에 대한 접근을 제공합니다
    • 요청 처리에 필요한 추가 컨텍스트 정보를 포함합니다
    • 세션 관리, 요청 속성, 경로 변수 등을 처리합니다
  • 불변성 보장
    • 모든 mutating 메서드는 새로운 ServerWebExchange 인스턴스를 반환합니다
    • 이는 리액티브 프로그래밍의 불변성 원칙을 따릅니다
    • 여러 스레드에서 안전하게 접근할 수 있습니다

주요 메서드와 기능

public interface ServerWebExchange {
// 요청 관련
ServerHttpRequest getRequest();

// 응답 관련
ServerHttpResponse getResponse();

// 세션 관리
Mono<WebSession> getSession();

// 요청 속성 관리
Map<String, Object> getAttributes();

// 경로 변수 접근
Map<String, String> getPathVariables();

// 폼 데이터 접근
Mono<MultiValueMap<String, String>> getFormData();

// 멀티파트 데이터 접근
Mono<MultiValueMap<String, Part>> getMultipartData();
}

실제 사용 예시

@RestController
public class UserController {
@GetMapping("/users/{id}")
public Mono<User> getUser(ServerWebExchange exchange) {
// 경로 변수 접근
String userId = exchange.getPathVariables().get("id");

// 요청 헤더 접근
String authorization = exchange.getRequest()
.getHeaders()
.getFirst("Authorization");

// 요청 속성 설정
exchange.getAttributes().put("user-id", userId);

// 비즈니스 로직 수행
return userService.findById(userId);
}
}
ServerWebExchange vs ServletRequest/Response

Spring MVC의 ServletRequest/Response와 비교했을 때 ServerWebExchange의 주요 차이점:

  • 리액티브 스트림 지원: 비동기-논블로킹 처리 가능
  • 불변성 보장: 모든 수정 작업이 새 인스턴스 생성
  • 통합된 컨텍스트: 요청/응답 관련 정보를 하나의 객체로 관리
  • 함수형 스타일: 명령형이 아닌 선언적 프로그래밍 지원

3. 특수 빈 타입

  • DispatcherHandler는 요청을 처리하고 적절한 응답을 만들기 위해 특별한 빈들에게 작업을 위임합니다.
  • 이러한 빈들은 보통 기본 구현체가 제공되지만, 속성을 커스터마이징하거나, 확장하거나, 완전히 다른 구현체로 교체할 수 있습니다.
  • DispatcherHandler는 다음과 같은 특수 빈들에게 작업을 위임합니다
    • HandlerMapping: 요청을 적절한 핸들러에 매핑
    • HandlerAdapter: 핸들러를 실행하고 결과를 HandlerResult로 래핑
    • HandlerResultHandler: 핸들러 실행 결과를 처리하고 응답을 완성

3.1 HandlerMapping

  • HandlerMapping은 요청을 적절한 핸들러에 매핑하는 역할을 합니다.

3.1 HandlerMapping 인터페이스

public interface HandlerMapping {
Mono<Object> getHandler(ServerWebExchange exchange);
}
  • getHandler(): 파리미터로 입력 받은 ServerWebExchange에 매칭되는 핸들러를 반환합니다.
  • 주요 구현체로는:
    • RequestMappingHandlerMapping: @RequestMapping 어노테이션이 붙은 메서드용
    • RouterFunctionMapping: 함수형 엔드포인트 라우트용
    • SimpleUrlHandlerMapping: URI 패턴과 WebHandler 인스턴스의 명시적 등록용

3.2 HandlerMapping 구현체별 설정 예시

RequestMappingHandlerMapping 설정 예시
  • RequestMappingHandlerMapping은 @RequestMapping 어노테이션을 사용한 컨트롤러 메서드를 등록하고 매핑합니다.
@Configuration
@EnableWebFlux
public class WebFluxConfig {

@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
mapping.setOrder(0); // 우선순위 설정
mapping.setUseSuffixPatternMatch(false); // 접미사 패턴 매칭 비활성화
mapping.setUseTrailingSlashMatch(true); // 후행 슬래시 매칭 활성화

// 커스텀 패스매처 설정 (옵션)
PathPatternParser patternParser = new PathPatternParser();
patternParser.setCaseSensitive(false); // 대소문자 구분 비활성화
mapping.setPatternParser(patternParser);

return mapping;
}
}
@RestController
@RequestMapping("/api")
public class UserController {

@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userService.findById(id);
}

@PostMapping("/users")
public Mono<User> createUser(@RequestBody User user) {
return userService.save(user);
}
}
RouterFunctionMapping 설정 예시
  • RouterFunctionMapping은 함수형 엔드포인트 방식으로 라우트를 정의합니다.
@Configuration
public class RoutesConfig {

@Bean
public RouterFunctionMapping routerFunctionMapping() {
RouterFunctionMapping mapping = new RouterFunctionMapping();
mapping.setOrder(-1); // RequestMappingHandlerMapping보다 높은 우선순위
return mapping;
}

@Bean
public RouterFunction<ServerResponse> userRoutes(UserHandler userHandler) {
return route()
.GET("/api/users/{id}", userHandler::getUser)
.POST("/api/users", userHandler::createUser)
.build();
}
}
@Component
public class UserHandler {

private final UserService userService;

public UserHandler(UserService userService) {
this.userService = userService;
}

public Mono<ServerResponse> getUser(ServerRequest request) {
String id = request.pathVariable("id");
return userService.findById(id)
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}

public Mono<ServerResponse> createUser(ServerRequest request) {
return request.bodyToMono(User.class)
.flatMap(userService::save)
.flatMap(savedUser ->
ServerResponse.created(URI.create("/api/users/" + savedUser.getId()))
.bodyValue(savedUser)
);
}
}
SimpleUrlHandlerMapping 설정 예시
  • SimpleUrlHandlerMapping은 URL 패턴을 직접 WebHandler 인스턴스에 매핑합니다.
  • 기본적으로 HTTP 메서드를 구분하지 않습니다.
@Configuration
public class WebHandlerConfig {

@Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
Map<String, Object> urlMap = new HashMap<>();

// 특정 경로에 WebHandler 인스턴스 매핑
urlMap.put("/resources/**", resourceWebHandler());
urlMap.put("/ws/**", webSocketHandler());
urlMap.put("/metrics", metricsHandler());

SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setUrlMap(urlMap);
mapping.setOrder(10); // 우선순위 설정 (값이 클수록 우선순위 낮음)

return mapping;
}

@Bean
public ResourceWebHandler resourceWebHandler() {
ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocations(Arrays.asList(
new ClassPathResource("static/"),
new FileSystemResource("/path/to/resources/")
));
return handler;
}

@Bean
public WebSocketHandler webSocketHandler() {
// WebSocket 핸들러 구현체 반환
return new CustomWebSocketHandler();
}

@Bean
public WebHandler metricsHandler() {
// 커스텀 WebHandler 구현체 반환
return new MetricsWebHandler();
}
}
public class MetricsWebHandler implements WebHandler {

@Override
public Mono<Void> handle(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

// 메트릭 데이터 생성
Map<String, Object> metrics = collectMetrics();

// 응답 데이터 설정
DataBufferFactory bufferFactory = response.bufferFactory();
try {
byte[] bytes = new ObjectMapper().writeValueAsBytes(metrics);
DataBuffer buffer = bufferFactory.wrap(bytes);
return response.writeWith(Mono.just(buffer));
} catch (Exception e) {
return Mono.error(e);
}
}

private Map<String, Object> collectMetrics() {
// 애플리케이션 메트릭 수집 로직
Map<String, Object> metrics = new HashMap<>();
metrics.put("memory", Runtime.getRuntime().totalMemory());
metrics.put("threads", Thread.activeCount());
metrics.put("uptime", ManagementFactory.getRuntimeMXBean().getUptime());
return metrics;
}
}

3.2 HandlerAdapter

  • DispatcherHandler가 매핑된 핸들러를 실제로 호출할 수 있도록 도와주는 역할을 합니다.
  • 예를 들어, 어노테이션이 붙은 컨트롤러를 호출하기 위해서는 해당 어노테이션들을 분석해야 하는데, HandlerAdapter가 이러한 세부사항을 DispatcherHandler로부터 숨깁니다.

3.2 HandlerAdapter 인터페이스

public interface HandlerAdapter {
boolean supports(Object handler);

Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler);
}
  • supports(): 핸들러가 해당 어댑터에 의해 처리될 수 있는지 여부를 반환합니다.
  • handle(): 핸들러를 실행하고 결과를 HandlerResult로 래핑합니다.
  • 주요 구현체로는:
    • RequestMappingHandlerAdapter: @RequestMapping 어노테이션이 붙은 메서드용
    • RouterFunctionAdapter: 함수형 엔드포인트 라우트용
    • SimpleHandlerAdapter: WebHandler 인스턴스의 명시적 등록용

3.3 HandlerResultHandler

  • 핸들러 실행 결과를 처리하고 응답을 완성하는 역할을 담당합니다.
  • 주요 구현체들은 다음과 같습니다:
결과 핸들러 타입처리하는 반환값기본 우선순위
ResponseEntityResultHandlerResponseEntity (주로 @Controller에서 사용)0
ServerResponseResultHandlerServerResponse (주로 함수형 엔드포인트에서 사용)0
ResponseBodyResultHandler@ResponseBody 메서드나 @RestController 클래스의 반환값100
ViewResolutionResultHandlerCharSequence, View, Model, Map 등 모델 속성으로 처리되는 객체Integer.MAX_VALUE

4. 요청 처리 프로세스

DispatcherHandler의 요청 처리 과정은 다음과 같습니다:

  1. 각 HandlerMapping에 매칭되는 핸들러를 찾도록 요청
  2. 첫 번째로 매칭된 핸들러를 사용
  3. 적절한 HandlerAdapter를 통해 핸들러 실행
  4. 실행 결과를 HandlerResult로 래핑
  5. HandlerResultHandler를 통해 처리를 완료하고 응답 작성

5. 예외 처리

5.1 예외 처리 메커니즘

경고

WebFlux에서의 예외 처리는 비동기 특성을 고려해야 합니다:

  • HandlerAdapter는 요청 핸들러 실행 중 발생하는 예외를 내부적으로 처리
  • 비동기 값을 반환하는 경우 예외가 지연될 수 있음
  • DispatchExceptionHandler를 통한 예외 처리 지원

5.2 주요 예외 처리 시점

  • 핸들러 매핑 전 발생하는 예외
  • WebFilter 실행 중 발생하는 예외
  • 핸들러 실행 결과 처리 중 발생하는 예외

6. 마치며

  • Spring WebFlux의 DispatcherHandler는 Spring MVC의 DispatcherServlet과 유사한 패턴을 따르지만, reactive 스택에 맞게 재설계되었습니다.
  • 두 프레임워크 모두 Front Controller 패턴을 통해 요청을 중앙에서 관리하며, 실제 처리는 위임 컴포넌트들에게 맡기는 구조를 가지고 있습니다.
  • 이러한 설계는 높은 확장성과 유연성을 제공하며, 각각의 환경에 맞는 최적화된 성능을 제공합니다.