Spring

Spring WebFlux - 리액티브 프로그래밍 2

RangA 2023. 6. 13. 23:49

리액티브 프로그래밍

리액티브 프로그래밍 구조

명령형 프로그래밍 vs 선언형 프로그래밍

명령형 프로그래밍

public class ImperativeProgrammingExample {
    public static void main(String[] args){
        // List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
        List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
        int sum = 0;

        for(int number : numbers){
            if(number > 4 && (number % 2 == 0)){
                sum += number;
            }
        }

        System.out.println(sum);
    }
}

  • List에 포함된 숫자들을 for문을 이용해서 순차적으로 접근한 후, if 문으로 특정 조건에 맞는 숫자들만 sum 변수에 더해서 합계를 구하고 있음
  • 명령형 프로그래밍 방식의 경우, 코드가 어떤 식으로 실행되어야 하는지에 대한 구체적인 로직들이 코드 안에 그대로 드러남

선언형 프로그래밍

public class DeclarativeProgramingExample {
    public static void main(String[] args){
        // List에 있는 숫자들 중에서 4보다 큰 짝수의 합계 구하기
        List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);

        int sum =
                numbers.stream()
                        .filter(number -> number > 4 && (number % 2 == 0))
                        .mapToInt(number -> number)
                        .sum();

        System.out.println("# 선언형 프로그래밍: " + sum);
    }
}

  • Java에서 선언형 프로그래밍 방식을 이해하기 위한 가장 적절한 예는 Java 8부터 지원하는 Stream API
  • List에 포함된 숫자들을 처리하는 것 명령형 프로그래밍과 동일하지만 처리 방식은 전혀 다름
  • Java Stream API를 사용하기 때문에 코드 상에 보이지 않는 내부 반복자가 명령형 프로그래밍 방식에서 사용하는 for문을 대체하고 있고, filter() 메서드(Operation)가 if 문을 대신해서 조건에 만족하는 숫자를 필터링하고 있음
  • Stream API를 사용한 코드는 numbers.stream().filter().mpaToInt().sum()과 같은 메서드 체인이 순차적으로 실행이 되는 것이 아님
  • 선언형 프로그래밍 방식은 하나부터 열까지 개발자가 일일이 로직을 모두 작성하지 않고 정말 필요한 동작들을 람다 표현식으로 정의(선언)하고 구체적인 동작 수행은 Operation(연산) 메서드 체인에 위임함

리액티브 프로그래밍의 구조

import reactor.core.publisher.Mono;

// 리액티브 프로그래밍 기본 구조
public class HelloReactiveExample01 {
    public static void main(String[] args) {
        // (1) Publisher의 역할
        Mono<String> mono = Mono.just("Hello, Reactive");

        // (2) Subscriber의 역할
        mono.subscribe(message -> System.out.println(message));
    }
}

  • 새로운 프로그래밍 언어를 배울 때 가장 기본이 되는 “Hello World”를 콘솔에 출력하는 프로그램으로 리액티브 스트림즈의 구현체인 Reactor를 통해 “Hello, Reactive”를 출력함
  • Puhlisher의 역할을 하는 것이 Mono
    • Publisher는 데이터를 emit하는 역할을 하며, Subsciber는 Publisher가 emit한 데이터를 전달받아서 소비하는 역할을 함
  • Subscriber의 역할을 하는 것은 subscribe() 메서드 내부에 정의된 람다 표현식인 message -> System.out.println(message)
  • Java의 Stream에서 메서드 체인 형태로 사용할 수 있는 것처럼 리액티브 프로그래밍 역시 메서드 체인을 구성할 수 있음

import reactor.core.publisher.Mono;

// 리액티브 프로그래밍 기본 구조
public class HelloReactiveExample02 {
    public static void main(String[] args) {
        Mono
            .just("Hello, Reactive")
            .subscribe(message -> System.out.println(message));
    }
}

  • HelloReactiveExample01 클래스의 코드를 하나의 메서드 체인 형태로 표현한 코드

리액티브 프로그래밍에서 사용되는 용어 정의

import reactor.core.publisher.Flux;

import java.util.List;

public class ReactiveGlossaryExample {
    public static void main(String[] args) {
        Flux
            .fromIterable(List.of(1, 3, 6, 7, 8, 11))
            .filter(number -> number > 4 && (number % 2 == 0))
            .reduce((n1, n2) -> n1 + n2)
            .subscribe(System.out::println);

    }
}

  • Publisher
    • 리액티브 스트림즈 사양에서도 확인한 것처럼 Publisher는 데이터를 내보내는 주체를 의미함
    • Flux가 Publisher
  • Emit
    • Publisher가 데이터를 내보내는 것을 Emit이라 함
  • Subscriber
    • Subscriber는 Publisher가 emit한 데이터를 전달받아서 소비하는 주체를 의미함
    • subscribe(System.out::println) 중에서 System.out::println이 Subscriber에 해당됨
    • 람다 표현식을 메서드 레퍼런스로 축약하지 않았다면 람다 표현식 자체가 Subscriber에 해당됨
  • Subscribe
    • Subscribe는 구독을 의미함
    • subscribe() 메서드를 호출하면 구독을 하는 것
  • Signal
    • Signal은 Publisher가 발생 시키는 이벤트를 의미함
    • subscribe() 메서드가 호출되면 Publisher인 Flux는 숫자 데이터를 하나씩 하나씩 emit하는데, 이때 숫자 데이터를 하나씩 emit하는 자체를 리액티브 프로그래밍에서는 이벤트가 발생하는 것으로 간주하며, 이 이벤트 발생을 다른 컴포넌트에게 전달하는 것을 Signal을 전송한다라고 표현함
  • Operator
    • Operator는 리액티브 프로그래밍에서 어떤 동작을 수행하는 메서드를 의미함
    • fromIterable(), filter(), reduce() 등 메서드 하나하나를 Operator라고 함
    • 우리말로 연산자라는 표현을 사용함
  • Sequence
    • Sequence는 Operator 체인으로 표현되는 데이터의 흐름을 의미함
    • Operator 체인으로 작성된 코드 자체를 하나의 Sequence라고 함
  • Upstream / Downstream
    • Sequence 상의 특정 Operator를 기준으로 위쪽의 Sequence 일부를 Upstream이라고 하며, 아래쪽 Sequence 일부를 Downstream이라고 표현함
    • filter() Operator를 기준에서 보면 filter() Operator 위 쪽의 fromIterable()은 Upstream이 됨
    • filter() Operator 아래쪽의 reduce() Operator는 Downstream이 됨