티스토리 뷰

728x90

리액티브 프로그래밍 (Reactive Programming)

개요

리액티브 프로그래밍은 데이터 스트림과 변화의 전파에 중점을 둔 프로그래밍 패러다임입니다. 데이터 처리를 위한 파이프라인을 구축하고, 이를 통해 지속적으로 들어오는 데이터를 자동으로 처리하는 방식을 채택합니다.

기존 프로그래밍과의 비교

명령형 프로그래밍

  • 순차적인 프로세스 실행
  • 현재 시점의 상태 처리
  • 데이터 변경 시 수동 업데이트 필요
  • "요리 레시피"와 유사한 접근 방식
// 명령형 프로그래밍 예시
let price = 100;
let quantity = 5;
let total = price * quantity;
console.log(total); // 500

price = 200; // 가격이 변경됨
// total은 자동으로 업데이트되지 않음 - 여전히 500

// 비동기 처리 예시
fetch('api/data')
  .then(response => response.json())
  .then(data => {
    // 데이터 처리
  })
  .catch(error => {
    // 에러 처리
  });

리액티브 프로그래밍

  • 데이터 스트림 기반 처리
  • 시간에 따른 데이터 흐름 처리
  • 자동화된 변경 전파
  • "자동화된 생산 라인"과 유사한 접근 방식

명령형은 현재 시점 상태의 프로세스를 정의한다면 리액티브 프로그래밍은 데이터를 처리하는 파이프라인들을 만들어두고 이후 지속해서 들어오는 데이터들을 지속적으로 처리

// 리액티브 프로그래밍 예시 (RxJS 사용)
import { BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

const price = new BehaviorSubject(100);
const quantity = new BehaviorSubject(5);

const total = combineLatest([price, quantity]).pipe(
  map(([p, q]) => p * q)
);

total.subscribe(result => console.log(result)); // 처음에 500 출력
price.next(200); // 가격이 변경되면 자동으로 새로운 total(1000) 계산되어 출력

// 비동기 처리 예시
from(fetch('api/data')).pipe(
  switchMap(response => response.json()),
  retry(3), // 자동 재시도
  share() // 여러 구독자간 공유
).subscribe(
  data => { /* 데이터 처리 */ },
  error => { /* 에러 처리 */ }
);

주요 차이점 분석

1. 데이터 흐름과 상태 관리

  • 명령형:
    • 상태 변경을 직접 관리
    • 데이터 흐름이 명시적이고 순차적
    • 상태 업데이트를 수동으로 처리
  • 리액티브:
    • 상태 변경이 자동으로 전파
    • 데이터 흐름이 선언적이고 반응적
    • 의존성 있는 모든 값이 자동 업데이트

2. 비동기 처리

  • 명령형:
    • Promise 체인으로 처리
    • 에러 처리가 각 단계별로 필요
    • 복잡한 비동기 흐름 제어가 어려움
  • 리액티브:
    • 스트림으로 통합 처리
    • 선언적 에러 처리 및 재시도 로직
    • 복잡한 비동기 흐름을 연산자로 제어

3. 코드 구조와 유지보수

  • 명령형:
    • 절차적인 코드 구조
    • 상태 변경 추적이 어려움
    • 기능 확장 시 코드 수정 필요
  • 리액티브:
    • 선언적인 코드 구조
    • 상태 변경 흐름이 명확
    • 기존 스트림에 연산자 추가로 확장

4. 리소스 관리

  • 명령형:
    • 수동적인 리소스 정리
    • 메모리 관리를 직접 처리
  • 리액티브:
    • 구독 기반의 자동 리소스 정리
    • 메모리 누수 방지를 위한 내장 매커니즘

주요 특징

1. 데이터 스트림 중심

  • 모든 데이터를 스트림으로 처리
  • 연속적인 데이터 흐름 관리
  • 비동기 데이터 처리 용이

2. 자동 전파

  • 데이터 변경 시 자동으로 관련 처리 실행
  • 의존성 있는 모든 컴포넌트에 변경사항 전파
  • Push 기반의 데이터 전달

3. 선언적 프로그래밍

  • 데이터 처리 파이프라인 정의
  • 복잡한 데이터 흐름을 명확하게 표현
  • 코드의 가독성과 유지보수성 향상

활용 사례

1. 실시간 데이터 처리

const priceStream = new Subject();

const analysisStream = priceStream.pipe(
    bufferTime(1000),
    map(prices => ({
        average: calculateAverage(prices),
        volatility: calculateVolatility(prices)
    })),
    filter(analysis => analysis.volatility > threshold)
);

analysisStream.subscribe(analysis => sendAlert(analysis));

2. 사용자 입력 처리

const searchPipeline = fromEvent(searchInput, 'input').pipe(
    map(e => e.target.value),
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(term => fetchSearchResults(term))
);

searchPipeline.subscribe(results => updateUI(results));

3. 실시간 데이터 집계와 처리

const salesStream = new Subject();

const analyticsPipeline = salesStream.pipe(
    bufferTime(60000),
    map(sales => ({
        total: calculateTotal(sales),
        average: calculateAverage(sales)
    }))
);

analyticsPipeline.subscribe(stats => updateDashboard(stats));

장단점

장점

  1. 자동화된 데이터 흐름 관리
    • 데이터 변경에 따른 자동 업데이트
    • 일관된 데이터 처리 보장
  2. 비동기 처리 간소화
    • 복잡한 비동기 로직을 선언적으로 처리
    • 에러 처리와 재시도 로직 통합 용이
  3. 코드 품질 향상
    • 높은 가독성의 선언적 코드
    • 유지보수가 용이한 모듈화된 구조

단점

  1. 학습 곡선
    • 새로운 패러다임 적응에 시간 소요
    • 복잡한 연산자와 개념 이해 필요
  2. 디버깅 어려움
    • 비동기 실행으로 인한 디버깅 복잡성
    • 스택 트레이스 추적이 어려울 수 있음
  3. 오버헤드
    • 단순한 로직에 대한 과도한 복잡성 가능성
    • 초기 설정과 구조화에 시간 소요

구현 예시

기본적인 데이터 스트림 처리

// 데이터 스트림 생성
const dataStream = new Subject();

// 처리 파이프라인 구성
const processedStream = dataStream.pipe(
    filter(data => isValid(data)),
    map(data => transform(data)),
    debounceTime(100)
);

// 결과 구독
processedStream.subscribe(
    result => console.log(result),
    error => console.error(error)
);

// 데이터 입력
dataStream.next(newData);

에러 처리와 재시도

const robustStream = dataStream.pipe(
    retry(3),
    catchError(error => {
        console.error(error);
        return of(defaultValue);
    }),
    finalize(() => cleanup())
);

전통적인 프로그래밍 방식 VS 리액티브 프로그래밍 방식

1. 데이터 흐름 및 실행 방식

기준 전통적인 프로그래밍 방식 리액티브 프로그래밍 방식
데이터 처리 흐름 요청과 응답을 순차적으로 처리 (동기 블로킹) 데이터 스트림을 통해 이벤트가 발생할 때마다 처리 (비동기)
흐름 제어 방식 한 요청이 완료되면 다음 요청을 처리하는 방식 데이터가 준비되면 즉시 처리하며 다른 작업은 계속 진행
자원 활용 각 요청마다 스레드나 자원을 할당하여 동작 하나의 스레드를 여러 요청이 공유하여 효율적 자원 사용

2. 블로킹과 논블로킹 방식의 차이

기준 전통적인 프로그래밍 방식 (블로킹) 리액티브 프로그래밍 방식 (논블로킹)
블로킹 방식 요청이 완료될 때까지 스레드가 대기함 요청과 응답이 비동기적으로 처리되며, 다른 작업을 계속 진행
장점 코드가 직관적이고 이해하기 쉬움 높은 성능과 자원 최적화 가능
단점 많은 스레드가 동시에 대기 상태로 자원 낭비 가능 코드가 복잡해질 수 있으며 디버깅이 어려울 수 있음

3. 성능과 확장성

기준 전통적인 프로그래밍 방식 리액티브 프로그래밍 방식
성능 대량의 동시 요청 처리 시 성능 저하 가능 고성능, 다수의 요청을 논블로킹 방식으로 처리
확장성 수직적 확장 필요 (더 많은 스레드와 자원 필요) 수평적 확장 가능 (스레드와 자원을 효과적으로 활용)
백프레셔 지원 여부 지원하지 않음 백프레셔로 데이터 처리 속도 조절 가능

4. 주로 사용되는 상황

기준 전통적인 프로그래밍 방식 리액티브 프로그래밍 방식
적합한 상황 단일 요청/응답 처리 또는 비교적 간단한 동작을 수행하는 경우 대량의 요청을 처리하거나 실시간 데이터 흐름을 다루는 경우
응용 예시 일반적인 웹 애플리케이션이나 DB 요청 등 순차 처리 시 유용 채팅 시스템, 실시간 알림, IoT 센서 데이터 수집 등

5. 에러 처리 및 백프레셔

리액티브 프로그래밍에서는 에러 처리백프레셔가 중요한 요소로 작용합니다. 리액티브 시스템에서는 이벤트가 발생할 때마다 반응해야 하기 때문에 에러가 발생했을 때 비동기적으로 처리해야 하며, 백프레셔를 통해 데이터가 너무 빨리 들어올 경우 조절할 수 있습니다.
전통적인 방식에서는 한 번에 데이터를 요청하고 받아오는 구조이기 때문에 백프레셔가 필요하지 않지만, 리액티브 방식에서는 데이터 흐름을 제어하기 위해 필수적인 요소입니다.

정리

  • 전통적인 방식: 동기적이고 블로킹 방식으로, 각 요청이 완료될 때까지 기다리며 다음 요청을 처리합니다. 이 방식은 코드가 직관적이지만, 성능 저하와 자원 낭비가 발생할 수 있습니다.
  • 리액티브 방식: 비동기 논블로킹 방식으로, 요청이 완료될 때까지 기다리지 않고 다른 작업을 진행하면서 데이터가 준비되면 바로 처리합니다. 백프레셔 등을 활용하여 성능과 자원 사용을 최적화하며, 높은 동시성 요청을 처리하는 데 적합합니다.
728x90
댓글