어제 오늘 내일

[WebFlux] "컨트롤러가 없다고?" 함수형 엔드포인트(Router & Handler) 작성법 본문

IT/SpringBoot

[WebFlux] "컨트롤러가 없다고?" 함수형 엔드포인트(Router & Handler) 작성법

hi.anna 2026. 4. 2. 00:16

 

지난 시간에는 @RestController를 이용한 익숙한 방식을 배웠습니다. 하지만 WebFlux의 창시자들은 조금 더 Java 8+ 람다(Lambda)스러운 방식을 제안합니다.

바로 함수형 엔드포인트(Functional Endpoints)입니다.
이 방식은 "요청을 어디로 보낼지(Routing)""어떻게 처리할지(Handling)"를 완벽하게 분리합니다.


1. 두 가지 핵심: Router & Handler

마치 지하철 노선도와 같습니다.

  1. RouterFunction (지도): "이 URL로 오면 저기로 가세요." (길 안내)
  2. HandlerFunction (목적지): "오셨군요. 제가 처리해 드릴게요." (실제 업무)

MVC에서는 컨트롤러 안에 매핑(@GetMapping)과 로직이 섞여 있었지만, 여기서는 완전히 분리됩니다.


2. 핸들러(Handler) 만들기

먼저 실제 비즈니스 로직을 수행할 일꾼, 핸들러를 만듭니다.
MVC의 Controller 메서드와 비슷하지만, 리턴 타입이 무조건 Mono<ServerResponse>여야 합니다.

@Component
public class UserHandler {

    private final UserRepository userRepository;

    public UserHandler(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 1. 단건 조회
    public Mono<ServerResponse> getUser(ServerRequest request) {
        // @PathVariable 대신 request.pathVariable() 사용
        Long id = Long.parseLong(request.pathVariable("id"));

        return userRepository.findById(id)
                .flatMap(user -> ServerResponse.ok().bodyValue(user)) // 찾으면 200 OK + Body
                .switchIfEmpty(ServerResponse.notFound().build());    // 없으면 404 Not Found
    }

    // 2. 전체 조회
    public Mono<ServerResponse> getAllUsers(ServerRequest request) {
        return ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(userRepository.findAll(), User.class); // Flux<User>를 바디에 넣음
    }
}

특징:

  • 입력은 ServerRequest, 출력은 ServerResponse로 고정됩니다.
  • ok(), notFound(), badRequest() 등 빌더 패턴으로 응답을 만듭니다.

3. 라우터(Router) 만들기

이제 핸들러를 URL과 연결해 줄 지도를 그립니다. 이건 설정 파일(@Configuration)에 빈(Bean)으로 등록합니다.

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> route(UserHandler userHandler) {
        return RouterFunctions
                // "/users/{id}"로 GET 요청이 오면 -> userHandler.getUser 실행
                .route(GET("/users/{id}"), userHandler::getUser)

                // "/users"로 GET 요청이 오면 -> userHandler.getAllUsers 실행
                .andRoute(GET("/users"), userHandler::getAllUsers);
    }
}

특징:

  • 모든 API 엔드포인트가 한 눈에 보입니다. (MVC에서는 컨트롤러 파일을 다 뒤져야 했죠.)
  • andRoute()로 계속 체이닝해서 연결할 수 있습니다.

4. 어노테이션 방식 vs 함수형 방식 비교

"그럼 뭘 써야 하나요?" 정답은 없습니다. 팀의 취향 차이입니다.

특징 어노테이션 방식(@RestController) 함수형 방식 (RouterFunction)
익숙함 MVC와 똑같아서 매우 익숙함 처음엔 낯설고 어려움
구조 매핑과 로직이 컨트롤러에 섞임 라우팅과 로직이 분리됨
가독성 API가 많아지면 파악하기 힘듦 모든 URL이 한곳에 모여있어 파악 쉬움
성능 리플렉션 사용 (미세한 오버헤드) 리플렉션 없음 (빠른 구동 속도)

 

추천:

  • 기존 팀원들이 MVC에 익숙하다면 -> 어노테이션 방식
  • 새로운 마이크로서비스를 작게 만들거나, 명확한 라우팅이 필요하다면 -> 함수형 방식

5. 중첩 라우팅 (Nested Route)

함수형 방식의 가장 큰 장점은 "경로 그룹화"가 쉽다는 것입니다.
/users로 시작하는 모든 요청을 묶어볼까요?

@Bean
public RouterFunction<ServerResponse> nestedRoute(UserHandler handler) {
    return RouterFunctions.route()
            .path("/users", builder -> builder
                .GET("/{id}", handler::getUser)
                .GET("", handler::getAllUsers)
                .POST("", handler::createUser)
            )
            .build();
}

코드 구조만 봐도 URL 구조가 보입니다. 정말 깔끔하죠?


마치며

오늘의 결론입니다.

  1. 함수형 엔드포인트@Controller 없이 API를 만드는 모던한 방식이다.
  2. Handler는 로직을 처리하고, Router는 길을 안내한다.
  3. RouterFunction을 쓰면 모든 API 목록을 한눈에 관리할 수 있다.

이제 여러분은 WebFlux의 두 가지 얼굴(어노테이션, 함수형)을 모두 다룰 수 있게 되었습니다.
다음 단계는 "남의 서버"를 호출하는 것입니다. RestTemplate은 이제 잊으세요.

다음 포스팅에서는 "RestTemplate은 이제 Deprecated!" 비동기 HTTP 요청의 정석, WebClient 사용법에 대해 알아보겠습니다. (이게 진짜 물건입니다!)

도움이 되셨다면 좋아요와 댓글 부탁드립니다! 😊

반응형
Comments