개요
RxJs에서 일반적으로 subscribe를 호출한 후 unsubscribe를 하지 않으면 메모리 누수(memory leak)가 발생합니다.
이는 애플리케이션의 성능 저하와 메모리 부족으로 이어질 수 있으므로 subscribe 이 후 반드시 unsubscribe를 해줘야 합니다.
이런 문제와 관련해서 subscription을 효과적으로 관리하는 방법에 대해 기술하려 합니다.
observable의 subscription과 memory leak에 대한 내용은 아래 링크에서 확인할 수 있습니다.
문제
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를 호출하는 것과 약간의 성능 차이가 있을 수도 있다고 합니다.
하지만 다른 이점에 비해 아주 사소하여 신경 쓸 필요가 없다는 의견입니다.