티스토리 뷰

반응형

RouterFunction 사용

클라이언트에서 오는 요청을 Controller로 대신 RouterFunction을 사용해 처리할 수 있습니다.

 

@Component
class CustomerRouter {
    
    @Bean
    fun customerRouters(): RouterFunction<*> = router { 
        "/functional".nest { 
            
        }
    }
}

새 컴포넌트로 생성했기 때문에 빈이 노출되면 컴포넌트 스캔을 통해 새로운 방식의 RouterFunction을 만들고 경로를 정의합니다. 위의 코드 같은 경우에는 /functional 경로의 모든 요청을 처리합니다.

 

코드를 아래와 같이 수정하였습니다.

@Component
class CustomerRouter {

    @Bean
    fun customerRouters(): RouterFunction<*> = router {
        "/functional".nest {
            "/customer".nest {
                GET("/") {
                    ServerResponse.ok().body("hello world".toMono(), String::class.java)
                }
            }
        }
    }
}

/functional 경로에 중첩된 GET 요청으로 /customer 경로를 요청하면 200 OK와 "hello world"를 응답합니다.

 

ServerResponse.ok는 응답을 만드는 함수인 ServerResponse.Builder이며, 결국 Mono<ServerResponse>를 만듭니다.

응답에는 Mono<String> 객체가 포함된 또 다른 모노가 들어있습니다. 따라서 게시자가 서버 응답을 위한 약속한 반환 값인 문자열을 가지게 됩니다.

 

 

코틀린 타입 추론을 이용하여 아래와 같이 변경할 수 있습니다.

@Component
class CustomerRouter {

    @Bean
    fun customerRouters(): RouterFunction<*> = router {
        "/functional".nest {
            "/customer".nest {
                GET("/") {
                    ok().body("hello world".toMono(), String::class.java)
                }
            }
        }
    }
}

 

또한 body 함수를 아래와 같이 사용할 수 있습니다.

@Component
class CustomerRouter {

    @Bean
    fun customerRouters(): RouterFunction<*> = router {
        "/functional".nest {
            "/customer".nest {
                GET("/") {
                    // ok().body("hello world".toMono(), String::class.java)
                    ok().body(Customer(1, "임리프").toMono(), Customer::class.java)
                }
            }
        }
    }
}

 

 

 

 

핸들러 만들기

라우터에서 응답을 출력할 람다를 생성해 HTTP 요청에 대한 응답 방법을 정의했는데, 이 코드를 핸들러라고 합니다.

 

람다식에 ServerRequest 클래스의 객체 변수가 하나 있습니다.

파라미터 또는 요청, 요청 본문을 포함한 핸들러로 보낼 모든 세부 정보가 포함됩니다.

 

 

아래와 같은 Handler를 하나 만들었습니다.

@Component
class CustomerHandler {
    fun get(serverRequest: ServerRequest) : Mono<ServerResponse> =
            ok().body(Customer(1, "리프").toMono(), Customer::class.java)
}

 

그리고 라우터의 코드를 아래와 같이 수정하였습니다.

@Component
class CustomerRouter {

    @Autowired
    lateinit var customerHandler: CustomerHandler

    @Bean
    fun customerRouters(): RouterFunction<*> = router {
        "/functional".nest {
            "/customer".nest {
                GET("/") {
                    customerHandler.get(it)
                }
            }
        }
    }
}

Handler를 통하여 값을 받을 수 있습니다.

 

메서드 참조를 사용해 라우터 코드를 아래와 같이 변경할 수 있습니다.

GET("/", customerHandler::get)

 

 

리액티브 서비스 사용

이제 서비스를 사용하겠습니다. Handler에 Service를 연결해줍니다.

 

@Component
class CustomerHandler(private val customerService: CustomerService) {
    fun get(serverRequest: ServerRequest) : Mono<ServerResponse> =
            ok().body(customerService.getCustomer(1), Customer::class.java)
}

하지만 위와 같이 코드를 작성하면 오류가 발생합니다. getCustomer의 반환 값이 nullable이기 때문입니다.

 

따라서 아래와 같이 수정해줍니다.

 

interface CustomerService {
    fun getCustomer(id: Int) : Mono<Customer>
    ...
}
@Component
class CustomerServiceImpl : CustomerService {

    ...
    
    override fun getCustomer(id: Int) = customers[id]?.toMono() ?: Mono.empty()

    ...
}

 

Customer의 객체가 없으면 빈 객체를 반환하도록 하였습니다. nullable 객체를 반환하지 않으므로 body에서 클래스를 지정할 필요가 없습니다. 따라서 Handler 코드를 아래와 같이 수정합니다.

import org.springframework.web.reactive.function.server.body

@Component
class CustomerHandler(private val customerService: CustomerService) {
    fun get(serverRequest: ServerRequest) : Mono<ServerResponse> =
            ok().body(customerService.getCustomer(1))
}

 

이제 customer/1과 같은 URL로 요청을 받기 위해 아래와 같이 수정을 합니다.

@Component
class CustomerRouter(private val customerHandler: CustomerHandler) {

    @Bean
    fun customerRouters(): RouterFunction<*> = router {
        "/functional".nest {
            "/customer".nest { GET("/{id}", customerHandler::get) }
        }
    }
}
@Component
class CustomerHandler(private val customerService: CustomerService) {
    fun get(serverRequest: ServerRequest) : Mono<ServerResponse> =
            ok().body(customerService.getCustomer(serverRequest.pathVariable("id").toInt()))
}

 

라우터에서 RequestMapping 결오를 선언하는 것과 유사하게 {id}를 추가합니다.

그리고 Handler에서 serverRequest.pathVariable을 통해 매개 변수를 가져옵니다.

 

따라서 http://localhost:8081/functional/customer/3 링크로 요청을 하면 아래 응답을 얻을 수 있습니다.

{
    "id": 3,
    "name": "영달",
    "telephone": {
        "countryCode": "+82",
        "telephoneNumber": "12341234"
    }
}

 

 

이제 존재하지 않는 고객을 요청할 시에 404 NOT FOUND를 반환하는 코드를 작성하겠습니다.

 

import org.springframework.web.reactive.function.BodyInserters.fromObject

@Component
class CustomerHandler(private val customerService: CustomerService) {
    fun get(serverRequest: ServerRequest) : Mono<ServerResponse> =
            customerService.getCustomer(serverRequest.pathVariable("id").toInt())
                    .flatMap { ok().body(fromObject(it)) }
}

게시자로부터 flatMap 함수를 통해 응답으로 변환할 Mono를 얻기 위해 서비스를 호출했습니다.

이 함수는 Mono를 구독하고 값이 있으면 ok().body() 함수로 Mono<ServerResponse>를 생성합니다.

ok().body() 함수는 Mono가 필요하기 때문에 fromObject 함수로 Mono<Customer>를 생성했습니다.

 

 

과정은 아래와 같습니다.

 

  • 메소드는 Mono<Customer>를 구독하고 flatMap을 사용하는 getCustomer 메서드에 의해 반환됩니다.
  • Mono<Customer>에 값이 있으면 it 매개 변수 안에 있는 Customer 객체를 받습니다.
  • fromObject 메소드를 사용해 새 Mono<Customer>의 값을 변환합니다.
  • 위에서 생성한 Mono<Customer>로 Mono<ServerResponse>를 만듭니다.

 

따라서 최종적으로 Mono<ServerResponse>를 반환하고, 이것을 구독하면 Customer 객체를 받게 됩니다.

 

Mono를 처리하기 위한 코드를 아래와 같이 작성합니다.

@Component
class CustomerHandler(private val customerService: CustomerService) {
    fun get(serverRequest: ServerRequest) : Mono<ServerResponse> =
            customerService.getCustomer(serverRequest.pathVariable("id").toInt())
                    .flatMap { ok().body(fromObject(it)) }
                    .switchIfEmpty(status(HttpStatus.NOT_FOUND).build())
}

값이 비어있으면 구독하기 위해 원래 Mono를 사용합니다.

status 함수를 사용해 404 NOT FOUND 상태로 응답합니다.

응답의 본문이 없으므로 응답을 완료하기 위해 build 메서드를 사용합니다.

 

 

 

 

다중 경로 처리

라우터에서 다중 경로를 만들거나 HTTP 동사를 정의해서 여러 요청을 처리할 수 있습니다.

@Bean
    fun customerRouters(): RouterFunction<*> = router {
        "/functional".nest {
            "/customer".nest {
                GET("/{id}", customerHandler::get)
                POST("/", customerHandler::create)
            }
            "/customers".nest { 
                GET("/", customerHandler::search)
            }
        }
    }

 

기존 핸들러를 호출했지만, 새 핸들러를 만들었습니다.

 

새로운 것을 만들기 전에 애플리케이션에 있는 레이어를 알아야됩니다.

 

  • 라우터 - 리액티브 서비스가 응답하는 경로와 메서드를 처리
  • 핸들러 - 구체적인 요청을 응답으로 변환하는 로직을 수행
  • 서비스 - 도메인의 비즈니스 로직을 캡슐화

위와 같은 별도의 레이어가 있으면 새 기능을 추가할 필요가 있는 곳을 변경할 때 도움이 됩니다.

라우터는 다른 기능을 위해 동일한 핸들러를 호출할 수 있으며, 핸들러는 여러 서비스를 결합할 수 있습니다.

한 레이어를 변경해도 다른 레이어에 영향을 주지 않습니다.

 

 

쿼리 매개 변수 사용 및 JSON 본문 처리

@Component
class CustomerHandler(private val customerService: CustomerService) {

    fun search(serverRequest: ServerRequest) =
            ok().body(customerService.searchCustomer(serverRequest.queryParam("nameFilter").orElse("")), Customer::class.java)
}

핸들러에서 검색 기능을 구현하였습니다. 쿼리 매개 변수를 처리할 때 queryParam을 쓰면 됩니다.

queryParam 함수는 매개 변수가 있든 없든 요청에 대한 검색을 하기 때문에 옵셔널 객체를 반환합니다.

따라서 orElse로 ""을 기본 값으로 반환하도록 하였습니다.

 

 

@Component
class CustomerHandler(private val customerService: CustomerService) {
    
    fun create(serverRequest: ServerRequest) =
            customerService.createCustomer(serverRequest.bodyToMono()).flatMap {
                status(HttpStatus.CREATED).body(fromObject(it))
            }

}

POST 같은 경우는 본문의 JSON을 처리해야 됩니다.

요청 본문에서 Mono를 만들 수 있도록 ServerRequest 클래스의 bodyToMono 함수를 사용해 Mono<Customer>를 만듭니다.

ServerResponse의 create 함수 대신 status 함수를 사용하는 이유는 create 함수는 201 CREATED HTTP 상태를 사용하려면 방금 만든 리소스의 URL을 필요로 하기 때문입니다.

 

만약 create를 사용하고자 하면 아래와 같이 Service에 있는 함수를 수정해야 됩니다.

 

interface CustomerService {
    ...
    fun createCustomer(customerMono: Mono<Customer>) : Mono<Customer>
}
@Component
class CustomerServiceImpl : CustomerService {

    ...
    
    override fun createCustomer(customerMono: Mono<Customer>): Mono<*> =
            customerMono.map {
                customers[it.id] = it
                it
            }
}
@Component
class CustomerHandler(private val customerService: CustomerService) {

    ...
    
    fun create(serverRequest: ServerRequest) =
            customerService.createCustomer(serverRequest.bodyToMono()).flatMap {
                //status(HttpStatus.CREATED).body(fromObject(it))
                created(URI.create("/functional/customer/${it.id}")).build()
            }

}

이전과 같이 호출하면 201 CREATED 응답과 빈 본문을 받습니다. 하지만 생성된 리소스의 위치를 해더로 받습니다.

반응형
댓글