티스토리 뷰

반응형

구독자와 게시자

리액티브 프로그래밍의 핵심 구성 요소에는 구독과 게시가 있습니다.

리액티브 프로그래밍은 이벤트가 감지되면 필요한 사용자에게 전송되는 이벤트 모델을 기반으로 합니다.

UI에서 버튼을 누르는 것과 같은 이벤트를 수신하고자 할 때 정의하는 것이 구독자입니다.

반대로 이벤트를 생성하는 것이 게시자라고 생각하면 됩니다.

 

 

스프링에서는 리액터 프레임워크를 사용해서 리액티브 마이크로서비스를 만듭니다.

컨트롤러는 결과의 게시자가 될 수 있으며, 스프링은 서비스를 사용하는 모든 사용자에게 데이터를 보내기 위해 이벤트를 구독합니다. 이 데이터는 리액티브 스트림 사양에 정의된 대로 리액티브 스트림으로 전송돼 논블로킹의 배압을 제공합니다.

 

 

 

Mono

리액터에서는 모노라는 클래스를 통해 하나의 결과를 보내는 게시자를 정의할 수 있습니다.

 

Mono를 사용하려면 아래와 같은 작업을 수행하면 됩니다.

val customerMono : Mono<Customer> = Mono.just(Customer(1, "Mono"))

 

아래는 코틀린의 고차 함수를 사용한 것과 타입 추론을 사용한 것입니다.

val customerMono : Mono<Customer> = Customer(1, "Mono").toMono()

val customerMono = Customer(1, "Mono").toMono()

 

Mono는 앞으로 얻으려고 하는 데이터에 대한 약속이라고 생각하면 됩니다.

누군가가 해당 게시자를 등록하면 데이터를 얻게 됩니다.

 

 

 

 

서비스에서 Mono 사용하기

 

Service 인터페이스의 반환 값을 아래와 같이 바꿨습니다.

interface CustomerService {
    fun getCustomer(id: Int) : Mono<Customer>?
    ...
}

 

ServiceImpl 클래스를 아래와 같이 변경하였습니다.

@Component
class CustomerServiceImpl : CustomerService {

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

    ...
}

 

마지막으로 Controller를 아래와 같이 수정하였습니다.

@RestController
class CustomerController(@Autowired private val customerService: CustomerService) {

    @GetMapping(value = ["/customer/{id}"])
    fun getCustomer(@PathVariable id: Int) : ResponseEntity<Any> {

        val customer = customerService.getCustomer(id)

        return if (customer != null) ResponseEntity(customerService.getCustomer(id), HttpStatus.OK)
        else ResponseEntity(ErrorResponse("404 NOT FOUND", "'$id'번째 고객 정보가 없습니다."), HttpStatus.NOT_FOUND)
    }
    ...
}

 

ResponseEntity를 통해 Mono를 포함하여 반환하였습니다.

스프링은 결과를 리액티브하게 반환하기 위해 요청이 들어오면 그 상황을 이해하고 게시자를 구독합니다.

 

 

 

Flux로 여러 객체 게시하기

리액터는 0에서 무한대의 요소를 가진 게시자를 만들 수 있는 클래스를 제공합니다.

 

플럭스는 아래와 같이 사용합니다.

val customerFlux = Flux.fromIterable(listOf(Customer(1, "one"), Customer(2, "two")))

 

코틀린 문법을 사용해서 아래와 같이 작성할 수 있습니다.

val customerFlux = listof(Customer(1, "one"), Customer(2, "two")).toFlux()

 

 

서비스에서 Flux 사용하기

아래와 같이 코드를 수정해주었습니다.

interface CustomerService {
    ...
    fun searchCustomer(nameFilter: String) : Flux<Customer>
}
override fun searchCustomer(nameFilter: String) =
    customers.filter {
        it.value.name.contains(nameFilter, true)
    }.map(Map.Entry<Int, Customer>::value).toFlux()
@RestController
class CustomerController(@Autowired private val customerService: CustomerService) {

    ...

    @GetMapping(value = ["/customers"])
    fun getCustomers(@RequestParam(required = false, defaultValue = "") nameFilter: String) = customerService.searchCustomer(nameFilter)
}

 

플럭스도 컨트롤러가 반환하면 스프링은 자동 구성으로 새로운 요청이 들어오면 구독합니다.

 

 

 

리액티브하게 객체 수신

리액티브 마이크로서비스를 만들 때 스프링은 게시자를 구독하면 본문의 객체를 가지는 모노 게시자를 RequestMapping에 보낼 수 있습니다.

 

interface CustomerService {
    ...
    fun createCustomer(customerMono: Mono<Customer>) : Mono<*>
}

Mono<Customer>를 받고 Mono<*>를 반환하는 createCustomer이라는 Customer를 생성하는 인터페이스를 구현하였습니다.

 

@Component
class CustomerServiceImpl : CustomerService {

   ...
   
    override fun createCustomer(customerMono: Mono<Customer>): Mono<*> =
            customerMono.subscribe {
                customers[it.id] = it
            }.toMono()
}

 

ServiceImpl에서 함수 내용을 구현하였습니다.

 

그리고 마지막으로 Controller에 아래와 같이 코드를 작성하였습니다.

@PostMapping(value = ["/customer"])
fun createCustomer(@RequestBody customerMono: Mono<Customer>) =
        ResponseEntity(customerService.createCustomer(customerMono), HttpStatus.CREATED)

 

POST로 데이터를 보내면 아래의 결과를 받을 수 있습니다.

{
    "disposed": false,
    "scanAvailable": true
}

 

subscribe 함수가 Disposable 객체를 반환하고 JSON으로 직렬화되기 때문입니다. 하지만 원하는 것은 빈 데이터 일 것입니다. 빈 결과를 얻기 위해서는 아래와 같이 작성합니다.

 

 

override fun createCustomer(customerMono: Mono<Customer>): Mono<*> =
    customerMono.map {
        customers[it.id] = it
    }

map으로 데이터를 넣어주었습니다. 따라서 빈 객체 { }를 얻게 됩니다.

 

 

만약 아래와 같이 it을 반환해주면 들어간 데이터를 반환받을 수 있습니다.

override fun createCustomer(customerMono: Mono<Customer>): Mono<*> =
    customerMono.map {
        customers[it.id] = it
        it
    }
{
    "id": 5,
    "name": "test",
    "telephone": {
        "countryCode": "+82",
        "telephoneNumber": "12341234"
    }
}

 

 

만약 빈 객체를 명시적으로 반환하고 싶으면 아래와 같이 작성하면 됩니다.

override fun createCustomer(customerMono: Mono<Customer>): Mono<*> =
    customerMono.map {
        customers[it.id] = it
        Mono.empty<Any>()
    }

 

201 CREATED와 함께 빈 객체를 결과로 반환합니다.

반응형
댓글