왜 Flutter가 Reactive Programming에 적합한지?

SEUNGHWANLEE·2022년 1월 1일
2

Flutter

목록 보기
3/6
post-thumbnail

Flutter가 2.8로 upgrade되면서 바뀐 점을 알아보다가 Dart VM에서 Dart Garbage Collector가 궁금해 알아보았다.

"by explicitly decoupling the user interface from its underlying state."

공식문서에서 해당 문구를 보고 Dart 가비지 콜렉터(GC)에 대해 알아보고자 한다.

The Dart Garbage Collector(GC)

Dart Garbage Collector는 두 가지(단계)의 조합으로 이루어진다.

  • Young Space Scavenger
  • Parallel mark-sweep collector

위 두가지에 대해 알아보기 전에 어떤 방식으로 GC가 어떻게 작동하는지 알아보자.

Scheduling

GC는 앱과 UI 성능에 영향을 최소화하기 위해서 Flutter engine에게 hook들을 제공한다.

c.f) hook

engine에 의해 앱이 idle하면서 유저 상호작용(user interaction)이 없을 때 감지해 알려주는 것을 의미한다.

이로써 GC가 성능에 영향을 끼치지 않은채 수집 단계(collection phase)들을 진행할 수 있도록 기회의 창을 마련해준다. 또한, GC는 앱이 idle한 동안 sliding compaction을 수행할 수 있다. 이는 메모리 파편화(fragmentation)을 감소시키면서 메모리 오버헤드를 최소화해준다.

c.f) sliding

다음 position을 결정하는 것을 의미한다.


Young Space Scavenger

번역을 하자니 마땅한게 떠오르지 않았다.. 글을 읽다보니 짧은 생명주기를 갖는 객체들을 청소하기 위해 고안된 모델이니 짧은 공간 청소부(?) 정도로 이해하면 될 듯 하다!

Stateless Widget과 같이 짧은 생명 주기를 가진 객체들을 해치우기(clean up) 위해서 고안된 단계이다. Blocking 중 일때는 두번째 단계인 mark/sweep보다 빠르다. 그리고 스케줄링과 함께 결합되어 사용되면 앱을 실행할 때 볼 수 있는 정지된 상태(렉)들을 거의 없앨 수 있다.

다른 말로 객체들은 메모리에 연속적으로 적재(할당)된다. 그리고 객체들이 생성되면서 할당될 수 있는 메모리가 모두 꽉 찰 때까지 다음으로 사용가능한 빈 공간에 할당되게 된다. Dart는 bump pointer allocation을 사용해 새로운 공간을 빠르게 찾아 프로세스를 굉장히 빠르게 만든다.

c.f) bump pointer allocation

할당하는데 빠르지만 제약이 있는 방법이다. 객체를 할당할 때 여유 공간이 있는지 빠르게 확인하고 포인터를 객체의 사이즈에 따라 할당해주는 방법이다.


새로운 객체가 할당될 새로운 공간에는 Semi space 라고 불리는 곳에 저장되는데, 이는 두개의 공간으로 나뉜다.



활성화된 상태(Active space)와 비활성화된 상태(Inactive space)로 나뉘어 오직 한 쪽만 시기와 상관없이 사용할 수 있다. 새로운 객체들은 활성화된 공간에 저장되며 이 공간이 모두 찼을 때에는 살아있는 객체들은 죽은 객체들을 무시한채 활성화된 공간에서 비활성화된 공간으로 복사된다. 비활성화된 공간이 이제 활성화되면서 위 프로세스가 반복된다.

반복 프로세스

Active space와 Inactive space 중 한 쪽만 아무때나 상관없이 사용가능

  1. 새로운 객체들이 활성화된 공간에 저장된다.
  2. 모든 공간이 사용되었다면 살아있는 객체들은 비활성화된 공간으로 복사된다.
  3. 비활성화된 공간을 활성화시킨다.

그렇다면 죽은 객체는 어떻게 판단할까?
객체가 죽었는지 살았는지를 비교하기위해서는 collector는 스택(메모리)에 저장된 변수와 루트(root) 객체부터 그것이 무엇을 참조하고 있는지 살펴본다.

Flutter는 build시 Widget들이 트리 구조로 되어있다는 점과 reference를 확인할 때는 hashCode로 확인할 수 있다.

이 다음에 참조된 객체들을 옮겨준다. 비워진 객체들이 어디를 가리키는지(point to) 알아내면서 참조된 객체들을 옮겨준다. 살아있는 객체들이 모두 옮겨질때까지 아래 그림과 같이 진행된다.

죽은 객체들은 참조된 것이 없으니 남은 것이 더이상 없다. 살아있는 객체들은 머지않아 GC이벤트에 의해 복제된다.


Parallel Marking and Concurrent Sweeping

어떤 객체가 특정 생명 주기를 달성했을 때 해당 객체는 두번째 단계인 mark-sweep에 의해 새로운 메모리 공간으로 옮겨진다.

위 GC 기술(mark-sweep)은 두 가지 단계로 나뉘어진다. 먼저 객체 그래프가 탐색되면서 객체는 '아직 사용 중'으로 마킹(표시)된다. 그 다음은 전체 메모리가 스캔되고 마킹되지 않은 객체들은 재활용된다. 마지막으로 모든 플래그(그동안 마킹된)들은 삭제된다.

이러한 형태의 garbage colllection(쓰레기 수집)은 마킹 단계에서 차단된다. 그 어떤 메모리 변화는 일어날 수 없으며 UI 스레드는 차단된다(blocked). 이 수집(collection)은 Young Scavenger에 의해 처리되는 짧은 생명 주기를 가진 객체들과 같이 더 드물지만, 때로는 Dart 런타임 때 이러한 형태의 garbage collection을 하기 위해서 멈출 수도 있다. Flutter가 가진 collection 스케줄링 능력으로 봤을 때 이로 인한 영향(멈추고 수집하는 작업)은 최소화 되어야한다.

반드시 앱이 "거의 모든 객체들이 짧은 생명주기를 갖는 것 처럼" 약한 세대 가설(weak generational hypothesis)에 붙지 않도록 주의해야한다. 그렇지 않으면 위와 같은 collection이 더욱 빈번하게 일어날 것이다. Flutter의 Widget이 구현되는 방법을 보면 자주 일어날 상황은 아니지만 명심해야할 일이다.


Isolates

Dart isolate는 다른 isolate와 메모리를 공유하지 않고 스스로의 private heap(고유한 메모리)를 갖고 있다는 것을 알고 있을 필요가 있다. 각 isolate는 분리된 스레드 상에서 작동되며 GC 이벤트 또한 다른 isolate에 영향을 끼쳐서는 안된다. Isolate를 사용하는 것이 UI를 차단하고 중요한 프로세스를 off-load하는 것을 방지할 수 있다.

같이 읽어보면 좋은 포스트 👉
플러터를 위한 다트 프로그래밍 - 서준수


글을 마치면서

Flutter 2.8 updates를 보다가 부팅 지연속도를 개선했다는 점에서 어떻게 달라졌는지 알아보다 다트 가비지 콜렉터까지 오게 되었다. 자바 가비지 콜렉터만 생각하면서 글을 읽다가 알고 있는 내용과 다른 내용이 있어 정리해두면 좋을 거 같아 적게 되었는데, 생각보다 모르는 점이 많았다..
다트를 1년 넘게 사용하면서 isolate 개념을 처음알게 되었고 왜 플러터가 반응형 프로그래밍에 적합한 프레임워크인지 다시한번 확인할 수 있었다. Isolate이 사용되면서 메모리 공유 문제를 신경쓰지 않아도 되고 서로 영향을 끼치지 않게 되면서 자연스레 내부레이어들이 분리되는 것 같았다. 다트 가비지 콜렉터의 역할을 세부적으로 다뤄보면서 함수형 프로그래밍에도 굉장히 적합한 언어라고 느껴졌다.

profile
잡동사니 😁

2개의 댓글

comment-user-thumbnail
2022년 1월 12일

안녕하세요 승환님! 사이드 프로젝트 개발 포지션 제안드리려고 메일 보냈습니다! 확인 부탁드려요:)

1개의 답글