Rxjs

[RxJs] Subscription을 효과적으로 관리하는 방법

Cottonwood__ 2023. 7. 13. 18:22

개요

RxJs에서 일반적으로 subscribe를 호출한 후 unsubscribe를 하지 않으면 메모리 누수(memory leak)가 발생합니다. 이는 애플리케이션의 성능 저하와 메모리 부족으로 이어질 수 있으므로 subscribe 이 후 반드시 unsubscribe를 해줘야 합니다.

 

이런 문제와 관련해서 subscription을 효과적으로 관리하는 방법에 대해 기술하려 합니다.

observable의 subscription과 memory leak에 대한 내용은 아래 링크에서 확인할 수 있습니다.

 

 

Why We Have To Unsubscribe An Observable In An Angular Application?

Unsubscribe An Observable: In Angular applications, it's always recommended to unsubscribe the observables to gain benefits like: Avoids Mem...

www.learmoreseekmore.com

 

문제

subscribe를 해제하는 방법은 몇 가지가 있는데 대표적으로 옵저버블 객체를 구독하고 unsubscribe를 원하는 시점에서 호출해 구독을 해제하는 방법이 있습니다. 이 방법은 subscription을 관리하는 측면에서 문제를 야기할 수 있다고 지적 받는 방법이기도 합니다. 아래 코드를 살펴보겠습니다.

 

  constructor(private util: UtilService) {}

  ngOnInit() {
    this.dataSubscribtion = this.dataSubscriber.subscribe((value) => {
      console.log(value);
    });

    this.conditionSubscribtion = this.conditionSubscriber.subscribe((condition) => {
      if (condition === 'stop') {
        this.dataSubscribtion.unsubscribe();
      }
    });

    this.conditionSubscribtion2 = this.conditionSubscriber2.subscribe((condition2) => {
      if (condition2 === 3) {
        this.dataSubscribtion.unsubscribe();
      }
    });
  }

  ngOnDestroy() {
    if(this.dataSubscribtion) {
      this.dataSubscribtion.unsubscribe();
    }

    if(this.conditionSubscribtion) {
      this.conditionSubscribtion.unsubscribe();
    }

    if(this.conditionSubscribtion2) {
      this.conditionSubscribtion2.unsubscribe();
    }
  }
}

 

이 방식은 여러 가지 문제를 가지고 있습니다. 가장 큰 문제는 관리 복잡성으로, 각 subscription을 개별적으로 관리해야 하기 때문에 코드가 복잡해지고 실수하기 쉬워집니다. 특히 subscription을 추가할 때마다 ngOnDestroy에서 각 subscription에 대해 개별 구독 해제에 대한 코드가 필요합니다. 또한 같은 패턴의 보일러플레이트 코드가 반복되며, 구독이 많아질수록 스케일링 문제가 심각해집니다.

 

개선

많은 사람들이 unsubscribe 호출 보다 rxJs의 takeUntil operator를 사용하여 구독을 관리하는 것을 채택하고 있습니다.

takeUntil은 특정 Observable이 값을 방출할 때까지만 소스 Observable의 값을 방출하는 연산자입니다. 쉽게 말해 "종료 신호"를 받으면 자동으로 구독을 해제하는 역할을 합니다. 이를 통해 명시적인 unsubscribe 호출 없이도 안전하게 구독을 관리할 수 있습니다.

constructor(private util: UtilService) {}

  ngOnInit() {
    this.dataSubscribtion = this.dataSubscriber
    .pipe(takeUntil(merge(this.destroy$, this.untilCondition1('stop'), this.untilCondition2(3))))
    .subscribe((value) => {
      console.log(value);
    });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

  untilCondition1(condition: string): Observable<string> {
    return this.myCondition.asObservable().pipe(
      filter((value) => value === condition)
    )
  }

  untilCondition2(condition: number): Observable<number> {
    return this.myCondition2.asObservable().pipe(
      filter((value) => value === condition)
    )
  }

 

takeUntil 연산자는 pipe의 마지막에 위치해야 합니다.

 

일단 코드의 길이가 줄었지만 이건 그리 중요한 이점이 아닙니다.

중요한 건 관리해야 할 구독 개체가 하나로 줄었다는 점과 구독 해제를 위한 이벤트 스트림을 구성한다는 것입니다. 다른 구독 해제 조건을 추가해야 하는 상황이 생긴다면 takeUntil에 전달되는 매개변수에 새로운 옵저버블 항목을 간단히 merge 해주면 됩니다.

이 방식의 또 다른 장점은 핸들러를 통해 구독 해제에 대한 이벤트를 호출할 수 있다는 점입니다.

 

this.dataSubscribtion = this.dataSubscriber
    .pipe(takeUntil(merge(this.destroy$, this.untilCondition1('stop'), this.untilCondition2(3))))
    .subscribe({
      next: (value) => {
        console.log(value);
      }
    }).add(() => {
      console.log('완료')
    })

 

unsubscribe는 호출되는 시점에서 이벤트를 직접 제어하는 방법 이외엔 구독 해제에 대한 이벤트를 호출할 수 있는 방법이 없습니다. 또한, 여러 옵저버블 객체를 하나의 subscription이 연결하고 있기 때문에 구독 지점이 로직 상 중요한 부분이 되며 이는 효과적인 관리가 될 수 있는 근거가 됩니다.

 

The only real advantage to using this approach would be performance. Since you’re using fewer abstractions to get the job done, it’s likely to perform a little better. This is unlikely to have a noticeable effect in the majority of web applications however, and I don’t think it’s worth worrying about.

 

다만, 명령적으로 unsubscribe를 호출하는 것과 약간의 성능 차이가 있을 수도 있다고 합니다.

하지만 다른 이점에 비해 아주 사소하여 신경 쓸 필요가 없다는 의견입니다.