자바스크립트가 빠르려면 GC가 똑똑해야 한다
자바스크립트 엔진 얘기를 하면 보통 "V8이 빠르다"는 말로 끝나기 쉬운데요, 실제로 V8이 빠른 이유는 여러 겹의 정교한 기술이 쌓여 있기 때문이에요. 그중 하나가 바로 가비지 컬렉션(GC, Garbage Collection) 이에요. 이번에 다시 회자되고 있는 V8의 "Orinoco" 프로젝트, 그중에서도 Parallel Scavenger 라는 컴포넌트가 어떻게 자바스크립트의 멈춤(pause) 시간을 줄였는지 풀어볼게요.
좀 더 풀어 설명하면, GC라는 게 뭐냐면, 우리가 자바스크립트에서 객체를 만들면 메모리를 차지하잖아요. 더 이상 안 쓰는 객체는 누군가 청소해줘야 메모리가 가득 차지 않아요. C나 C++에선 개발자가 직접 free()로 청소하지만, 자바스크립트에선 엔진이 알아서 해줍니다. 이 청소부 역할을 하는 게 GC예요.
문제는 "청소하는 동안 모든 게 멈춘다"는 것
예전부터 GC의 가장 큰 약점은 "Stop-The-World" 현상이었어요. 청소부가 일하는 동안엔 자바스크립트 코드 실행 자체가 멈춰버리는 거예요. 짧으면 모를까, 이게 길어지면 사용자 입장에선 화면이 버벅대는 걸로 느껴집니다. 60fps로 부드럽게 돌아야 할 애니메이션이 갑자기 끊기거나, 게임이 한순간 얼어붙거나 하는 식이죠.
V8 팀이 "Orinoco"라는 프로젝트로 이걸 해결하려고 했어요. Orinoco는 한 가지 기능이 아니라 여러 GC 개선의 묶음이에요. "메인 스레드 멈춤을 최소화하자"는 큰 목표 아래에 점진적(Incremental), 동시(Concurrent), 병렬(Parallel) 기법들을 다 동원하는 거예요.
영 제너레이션과 스캐빈저, 무슨 말이냐면
구체 기술로 들어가기 전에 용어 두 개만 정리할게요.
제너레이셔널 GC라는 아이디어는 "대부분의 객체는 만들어지자마자 금방 죽는다"는 관찰에서 출발해요. 함수 안에서 만든 임시 변수나 짧게 쓰이는 객체가 그렇잖아요. 반대로 오래 살아남는 객체(예: 글로벌 캐시, 장기 보관 데이터)는 비교적 적어요. 그래서 메모리를 두 영역으로 나눠요.
- Young Generation (영 제너레이션): 새로 만든 객체가 들어가는 작은 영역. 자주 청소함.
- Old Generation (올드 제너레이션): 영 제너레이션에서 살아남은 객체가 옮겨가는 큰 영역. 가끔 청소함.
기존 스캐빈저 vs Parallel Scavenger
예전 V8의 스캐빈저는 메인 스레드 하나만 써서 일했어요. 청소하는 동안 자바스크립트는 멈추고요. 영 제너레이션이 작아서 멈춤이 그리 길진 않았지만, 그래도 "GC가 끝날 때까지 기다려야 한다"는 한계는 있었어요.
Parallel Scavenger는 여기서 발상을 바꿔요. 여러 헬퍼 스레드가 동시에 청소를 수행하는 거예요. 메인 스레드도 일을 거들고, 워커 스레드들도 같이 청소를 합니다. 마치 식당에서 혼자 청소하던 알바가 여러 명이 같이 청소하는 것과 같죠.
핵심 알고리즘적 트릭은 "work stealing(작업 훔치기)" 이에요. 각 스레드가 자기 작업 큐를 가지고 일하다가, 자기 큐가 비면 다른 바쁜 스레드의 큐에서 작업을 "훔쳐" 와요. 이렇게 하면 스레드 간 부하가 자동으로 균형이 맞춰져서, 일찍 끝난 스레드가 노는 시간이 거의 없어요.
또 하나 까다로운 부분이 객체 이동의 동기화예요. 스캐빈저는 살아있는 객체를 새 공간으로 복사하는 방식이에요. 여러 스레드가 같은 객체를 동시에 옮기려고 하면 사고가 나죠. V8 팀은 CAS(Compare-And-Swap) 같은 원자적 연산으로 "이 객체는 내가 옮긴다"를 락 없이 표시하는 메커니즘을 만들었어요. 이게 락(lock)을 쓰는 방식보다 훨씬 빠르고 확장성도 좋아요.
실제 효과는 어떨까
V8 팀의 발표에 따르면 Parallel Scavenger 도입 후 메인 스레드의 영 GC 시간이 약 20~50% 감소 했어요. 절대 시간으로 보면 청소 한 번에 10ms 걸리던 게 3~5ms로 줄어드는 식이에요.
10ms가 별거 아닌 것 같죠? 하지만 60fps 애니메이션은 한 프레임이 16.6ms예요. GC가 10ms를 잡아먹으면 그 프레임은 거의 망한 셈인데, 3ms로 줄면 여유가 생깁니다. 모바일 기기, 저사양 노트북, 복잡한 SPA(싱글 페이지 앱)에서 체감 차이가 큽니다.
다른 엔진들과 비교하면
자바스크립트 엔진은 V8만 있는 게 아니에요. 다른 엔진들의 GC 전략과 비교해보면 그림이 더 선명해져요.
SpiderMonkey(Firefox) 도 비슷한 방향으로 가고 있어요. 동시 GC, 점진적 GC, 제너레이셔널 GC를 다 갖췄고, GGC(Generational GC)라고 부르는 시스템에서 비슷한 최적화를 적용해왔어요.
JavaScriptCore(Safari) 는 Riptide라는 동시 GC 시스템을 갖고 있어요. 메인 스레드 멈춤을 최소화하는 같은 목표를 추구하지만 구현 디테일은 좀 달라요.
넓게 보면 JVM(자바 가상머신) 의 G1, ZGC, Shenandoah 같은 GC들이 이미 비슷한 아이디어를 더 일찍 정교하게 발전시켰어요. ZGC는 멈춤 시간을 ms 단위로 잡는 게 목표죠. 자바스크립트 엔진들은 어떤 면에선 자바 진영의 GC 연구를 빠르게 따라잡아 자기 환경에 맞게 적용한 셈이에요.
한국 개발자에게 — 이게 내 코드에 어떤 영향을 주나
솔직히 우리가 자바스크립트 짤 때 GC를 의식할 일은 많지 않아요. 엔진이 알아서 해주니까요. 그래도 몇 가지 실용적인 시사점은 있어요.
첫째, 객체를 많이 만들어도 예전만큼 비용이 안 들어요. 예전엔 "새 객체 만들지 말고 재활용하라"는 최적화 팁이 많았는데, Parallel Scavenger 같은 개선 덕분에 단기 객체 생성 비용이 많이 줄었어요. 그래서 지금은 가독성을 희생해서 객체 풀링을 강제로 하는 건 대부분 과한 최적화예요.
둘째, 성능 문제 디버깅 시 GC를 의심해볼 줄 알아야 해요. Chrome DevTools의 Performance 탭을 열면 GC 활동이 표시돼요. 프레임 드랍이 일어나는 지점에서 보라색 "Minor GC" 막대가 자주 보이면, 단기 객체를 너무 많이 만들고 있다는 신호예요.
셋째, Node.js 서버에선 메모리 패턴이 더 중요해요. 서버는 장기 실행 프로세스라 객체가 영 제너레이션과 올드 제너레이션 사이를 어떻게 이동하는지가 메모리 누수 원인을 찾는 핵심이 됩니다. 큰 캐시, 이벤트 리스너 누수, 클로저 캡처 같은 게 단골 원인이죠.
넷째, 엔진 내부 학습의 좋은 출발점이에요. "내가 쓰는 도구가 어떻게 동작하는지"를 안다는 건 시니어 개발자로 가는 길에서 큰 자산이에요. V8 블로그는 영어지만 그림이 풍부하고, Orinoco 시리즈는 GC 입문용으로 정말 좋아요.
한 줄 정리와 질문
Parallel Scavenger는 "멈추지 않는 자바스크립트"를 향한 V8의 발걸음이에요. 우리가 직접 코드를 안 고쳐도 매년 자바스크립트가 빨라지는 비밀이 이런 곳에 있어요.
여러분은 자바스크립트 성능 튜닝 중에 GC 때문에 골치 아팠던 경험이 있으신가요? 어떤 도구로 진단했고, 어떻게 풀었는지 노하우를 나눠주세요.
🔗 출처: Hacker News
TTJ 코딩클래스 정규반
월급 외 수입,
코딩으로 만들 수 있습니다
17가지 수익 모델을 직접 실습하고, 1,300만원 상당의 자동화 도구와 소스코드를 받아가세요.
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공