[리액티브 프로그래밍, 코틀린] 코틀린의 리액티브 프로그래밍

리액티브 프로그래밍

리액티브 프로그래밍(reactive programming)이란 비동기, 논블로킹 방식을 기반으로 애플리케이션의 리소스 효율을 높이고 역압(backpressure)이라는 개념을 통해 애플리케이션에 확장성과 유연성을 부여하는 프로그래밍 구현 또는 패러다임이다.

스프링 프레임워크에서는 웹플럭스(Webflux) 프로젝트를 통해 리액티브 스트림(reactive stream) 명세를 따르는 구현체인 프로젝트 리액터(Project Reactor)를 사용하여 리액티브 프로그래밍을 구현할 수 있다.


비동기 프로그래밍의 단점

웹플럭스를 통한 리액티브 및 비동기 프로그래밍을 구현하다보면 절차적 및 명령형 스타일 보다는 선언적 및 함수형 스타일로 코드를 작성하는 것이 요구된다. 이러한 코드 스타일 및 코드 작성 방법은 기존 명령형(또는 절차형) 프로그래밍과는 매우 다른 형태이며 적지 않은 학습을 필요로 한다.

비동기 및 리액티브 프로그래밍 코드 구현은 크게 가독성 측면, 디버깅 측면에서 다음과 같은 단점이 있다.

1) 가독성 및 유지 보수성

비동기로 실행되는 코드는 순차적, 절차적으로 실행되지 않기 때문에 실행 흐름을 파악하기 어렵다. 여러 코드들이 비동기적으로 동시 다발적으로 실행되며 완료 순서를 정확히 알기 힘들다.

비동기 코드가 실행 완료된 후 이후 동기적으로 실행할 코드는 콜백으로 실행한다. 이후 동기적 실행 절차가 많으면 콜백 함수도 많아져 코드가 계층 구조가 되는 콜백 지옥(callback hell) 상황이 발생할 수 있다.


2) 디버깅

비동기로 실행되는 코드는 코드 실행 흐름이 하나더라도 여러 스레드에 걸쳐 실행될 수 있으므로 코드 실행 흐름 추적 및 에러 디버깅이 힘들다.

연속적인 함수(또는 메서드) 실행을 기록하고 관련 정보를 보여주는 콜 스택(call stack)은 디버깅에 사용된다. 동기적으로 실행되는 코드의 경우 오류 발생 시 콜 스택 위로 전파되므로 오류가 발생한 코드 및 컨텍스트를 쉽게 파악할 수 있지만 비동기적으로 실행되는 코드의 경우 컨텍스트 손실로 인해 문제 파악이 어렵다.

코드 실행 흐름(스레드)이 변경되는 경우 실행 도중 콜 스택이 변경될 수 있기 때문에 오류가 발생한 코드를 파악하기가 힘들다. 이러한 이유로 코드 실행 도중 발생 가능한 예외 및 에러를 런타임에 즉시 발견하기 쉽지 않다.


자바와 코틀린

자바는 익명 객체 및 람다식을 사용하여 동작의 파라미터화 및 함수형 프로그래밍이 가능하지만 메서드를 변수로 할당하거나 메서드의 인자로 전달할 수 없으며 메서드의 반환값으로 반환할 수 없는 제약이 존재한다. 이러한 이유로 자바 프로그래밍에서 메서드는 1급 시민(first-class citizen)이 아니다.

반면 코틀린에서는 함수를 변수로 할당하거나 함수의 인자로 전달할 수 있으며 함수의 반환값으로 반환도 가능하다. 함수의 1급 시민 지원 기능으로 인해 함수형 프로그래밍을 보다 쉽게 구현할 수 있으며 코드를 읽기 쉽고 유연하게 작성할 수 있다.

또한 함수의 변수 할당 및 동시성 코드 작성에 도움을 주는 코틀린의 코루틴(coroutine) 기능은 비동기 및 리액티브 프로그래밍의 가독성과 코드 구현 측면에서 많은 장점을 제공한다. 코루틴이란 비동기적으로 실행하려는 코드를 보다 쉽게 작성할 수 있게 도와주는 동시 실행 설계 패턴이다. 스레드를 통한 동시성 구현을 보다 쉽게 할 수 있게 해준다. 동시성(concurrent) 코드를 마치 동기적(synchronous) 코드처럼 명령형 방식으로 작성할 수 있게 도와준다. 따라서 코루틴을 사용하면 비동기 프로그래밍을 절차적인 방식으로 할 수 있다.


리액티브 프로그래밍 vs 코틀린 코루틴

비동기 요청, 스레드 논블로킹 방식으로 비동기적 데이터 스트림을 처리하고 적은 수의 스레드로 많은 데이터를 처리할 수 있는 리액티브 프로그래밍 패러다임은 코드 구현을 위해 리액티브 스트림이라는 명세를 따르고 그 구현체를 사용한다.


코틀린 코루틴 리액티브

스프링은 리액터라는 리액티브 스트림 구현체를 사용하여 리액티브 지원을 한다. 비동기 데이터 스트림을 처리하고 논블로킹 방식으로 동작하는 코드를 작성하기 위해 Mono, Flux라는 리액터 타입과 리액터 타입이 제공하는 다양한 연산자(operator)를 사용한다. 따라서 메서드의 반환 타입이 리액터 타입이 되도록 정의하고, 처리하려고 하는 데이터 객체의 타입을 리액터 타입의 제네릭 타입으로 선언하는 등의 과정이 필요하다. 데이터베이스 접근이나 메시징 처리와 같은 작업을 수행하기 위해 외부 라이브러리를 사용할 때에도 해당 라이브러리가 리액터 타입을 지원하는 경우에만 완전한 리액티브 프로구래밍 구현이 가능하며 그렇지 않다면 블로킹 작업을 별도로 처리하여

또한 데이터 항목을 배열이나 리스트 객체와 같은 자료구조에 담아 초기화한 후 데이터가 준비되면(메모리에 모든 데이터를 로드한 후 처리) 사용하는 동기적, 단계적 방식 대신 리액터 타입을 사용하여 데이터를 비동기적으로 시간이 지남에 따라 흐르는(flow) 비동기 데이터 스트림(asynchronous data stream)의 형태로 처리한다. 데이터 항목에 변화가 발생하거나 데이터 처리 도중 에러가 발생하였을 때 이에 반응(react)하는 방식의 코드 작성이 이루어진다.

자바 대신 코틀린을 사용하면 이러한 코드 작성 방식을 반드시 따를 필요가 없어진다. 크게 두 가지의 코드 변화가 가능하다. 첫 번째로 비동기적 코드 실행을 통한 동시성을 절차적, 명령형 방식의 코드 작성으로 구현할 수 있다. 이를 위해서는 코틀린의 중지 함수(suspend function)와 코루틴 사용이 필요하다. 두 번째로 리액터 타입 선언 및 리액터 연산자 사용을 하지 않아도 된다. 이를 위해서는 리액터 타입을 이에 대응되는 코틀린의 타입으로 변환하는 과정이 필요하다. 리액터 타입을 사용하지 않는다면 리액터 타입이 제공하는 다양한 연산자의 기능을 모두 사용할 수는 없다. 위와 같이 필요한 과정만 거치면 코틀린을 사용하여 리액티브 프로그래밍이 가능하고 비동기, 논블로킹 코드를 명령형 방식으로 보다 간결하게 작성할 수 있게 된다.

스프링 프레임워크 5.2 버전부터 코틀린의 코루틴을 사용할 수 있게 되었다. 코틀린 언어가 제공하는 중지 함수는 비동기 작업에 대한 추상화를 제공하며 kotlinx.coroutines 라이브러리는 다양한 코루틴 빌더 함수, 리액터 타입 뿐만 아니라 Flux와 동일한 개념의 Flow 타입을 제공한다. 이들을 사용하여 리액티브 프로그래밍을 위해 중지 함수 및 코루틴을 생성하고 실행할 수 있다. 코루틴은 논블로킹 코드를 명령형 방식으로 작성할 수 있게 도와주므로 kotlinx.coroutines 라이브러리를 사용하면 리액티브 프로그래밍을 명령형 방식으로 구현할 수 있다.

중지 함수는 실행 도중 중지가 가능한 함수이다. 해당 함수를 실행한 스레드에서 실행 도중 임시로 중지하고 나중에 다른 스레드에서 중지된 함수를 다시 재개할 수 있다. 중지 함수를 사용함으로써 함수 내 코드 실행 스레드를 블로킹하지 않는 논블로킹 방식의 코드 구현이 가능하다. 또한 중지 함수는 또다른 중지 함수 내에서 절차적으로 호출이 가능하다.

자바를 사용하여 데이터 스트림에 대한 연산 처리를 하는 경우 데이터 스트림이 완료되었을 때 그다음 수행될 순차적 작업들을 메서드 체이닝(method chaining) 형태로 작성하였다. 자바에서 동작을 파라미터화할 수 있으므로 작업을 메서드의 인자로 전달하면 된다. 코틀린의 중지 함수를 사용하면 메서드 체이닝 형태로 수행될 순차적 작업들을 인자로 전달하는 대신 명령형 방식으로 순차적으로 호출하면 된다.

리액터 타입을 반환하는 API에 대해 코루틴을 바로 사용할 수는 없다. 코루틴을 사용하기 위해서는 중지 함수를 정의해야 하고, 리액터 타입을 코드에서 제거하면서 코루틴 코드를 구현하기 위해서는 리액터 타입을 이에 대응되는 코틀린의 타입으로 변환해야 한다. 일반 함수(fun)를 중지 함수(suspend fun)로 변환하고 리액터 타입을 이에 대응되는 코틀린의 타입으로 변환하는 것은 코틀린의 확장(extension) 함수에 의해 이루어지며 이 함수는 kotlin-coroutines-reactive 라이브러리에 포함되어 있다. 관례적으로 코루틴 사용을 위한 코틀린 확장 함수(중지 함수)는 await 접두사 또는 AndAwait 접미사를 가지며 리액터 타입에 대응되는 비슷한 이름으로 정의되어 있다. 이와 같이 확장 함수를 호출함으로써 코루틴을 사용할 수 있게 되는 것 또는 이를 위해 사용되는 확장 함수 자체를 코루틴 확장(coroutine extension)이라고 한다.

스프링 프레임워크는 다음과 같이 코틀린의 코루틴을 사용할 수 있도록 지원한다.

  • @Controller를 사용한 스프링 웹플럭스에서 코틀린 확장 함수에 의해 변환된 코루틴의 Deferred 객체 및 Flow 객체와 중지 함수 지원
  • 웹플럭스 클라이언트와 서버의 함수형 API를 위한 확장 함수 지원
  • Webflux.fn의 coRouter() DSL 지원


즉, 스프링 웹플럭스는 WebClient, ServerRequest 또는 ServerResponse와 같은 FluxMono 기반 API에 코루틴 확장을 제공하는 것 외에도 @Controller 클래스에 대해 중지 함수 및 Flow 반환 타입을 지원한다.

코틀린 코루틴 리액티브가 제공하는 Flow 객체를 사용하면 flatMap 메서드가 더 이상 필요 없어진다. 기존 리액터 타입의 map 메서드는 데이터 스트림의 항목 변환 시 순서를 유지하기 위해 동기적으로 처리하고 flatMap 메서드는 순서 보장 없이 비동기적으로 처리하였다. Flowmap 함수는 중지 함수를 인자로 전달받아 변환 작업을 비동기적으로 수행할 수 있으므로 비동기적 데이터 스트림 처리를 위해 flatMap 메서드를 사용하지 않아도 된다.

스프링 데이터 모듈 중 하나인 스프링 데이터 R2DBC는 리액티브 API 중 하나이다. R2DBC는 데이터베이스 쿼리를 비동기, 논블로킹 방식으로 실행하는 DatabaseClient를 제공하며 DatabaseClient의 메서드 반환값에 코틀린 확장 함수를 추가적으로 호출한 후 코루틴 사용이 가능하다.


콜드 스트림과 핫 스트림


기존 리액티브 스트림과 코틀린 코루틴 코드 비교


웹플럭스와 중지 함수, 코루틴

컨트롤러의 핸들러 메서드를 중지 함수로 정의하고 함수가 리액터 타입인 Mono를 반환하도록 하는 경우 data:{"scanAvailable":true}와 같은 데이터가 반환된다. 이는 웹플럭스의 HandlerAdapter 구현체의 subscribe() 메서드 호출 결과 Disposable 객체가 반환되기 때문이다.

Flux를 반환하는 경우 Flux 타입을 Flux 데이터 스트림이 담고 있는 데이터 항목 타입으로 변환할 수 없다는 ClassCastException 예외가 발생하게 된다.

따라서 웹플럭스의 프론트 컨트롤러 메서드 내에서 중지 함수를 호출하기 위해서는 코틀린 확장을 사용하여 리액터 타입을 코틀린이 지원하는 타입으로 변경해야 한다.


블록하운드

블록하운드를 사용하면 리액티브 코드에서 블로킹 코드를 검출할 수 있다. 코루틴 라이브러리의 kotlinx-coroutines-debug 모듈은 블록하운드와 자동 통합 기능을 제공하며 코루틴 컨텍스트 내에서 블로킹 작업이 호출되면 이를 감지하여 알려준다.

블록하운드를 사용하기 위해서는 프로젝트 리액터의 io.projectreactor.blockhound 라이브러리가 필요하다.

테스트 클래스 및 메서드에서 사용하고자 하는 경우 testImplementation으로 의존성을 추가한 후 테스트 초기화 메서드에서 Blockhound.install() 메서드를 호출한다.


참고

Comments