인생마커

개요

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();
    }
  }
}

 

data를 subscribe 하고 있고 condition에 따라서 unsubscribe를 하고 있습니다.

 

아주 사소한 예제임에도 불구하고 unsubscribe가 여러 곳에서 사용되고 있습니다.

 

이 방법은 구독 로직이 복잡할 수록 단점이 명확하게 들어납니다.

 

관리해야 할 subscription이 많아지고 결국 메모리 누수와 함께 성능 저하가 발생할 확률이 생깁니다.

 

개선

takeUntil

https://rxjs.dev/api/operators/takeUntil

 

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

 

takeUntil 은 subject에서 시그널이 전달되면 옵저버블의 구독을 종료합니다.

 

위의 예제를 takeUntil을 사용하는 방식으로 바꿔보겠습니다.

 

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를 호출하는 것과 약간의 성능 차이가 있을 수도 있다고 합니다.

 

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

profile

인생마커

@Cottonwood__

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!