가비지 컬렉터가 뭐길래
Go로 서버를 만들다 보면 메모리 관리를 직접 할 일이 거의 없죠. new나 make로 만든 객체를 다 쓰고 나면 알아서 치워주는 친구가 있으니까요. 이게 바로 가비지 컬렉터(GC)인데요, 쉽게 말하면 '더 이상 아무도 안 쓰는 메모리를 찾아서 회수하는 청소부'예요. 그런데 이 청소부도 일하는 동안 CPU를 쓰기 때문에, 청소를 얼마나 효율적으로 하느냐가 프로그램 전체 성능에 꽤 큰 영향을 주거든요.
최근 Go 팀이 'Green Tea'라는 이름의 새 가비지 컬렉터를 내놨는데, 한 개발자가 기존 GC와 새 GC가 실제로 힙(heap, 프로그램이 동적으로 할당한 메모리 영역)을 어떻게 돌아다니는지 시각화해서 비교한 글을 올렸어요. 그림으로 보면 두 방식의 차이가 정말 극적으로 드러나거든요.
기존 GC의 문제: 메모리를 '점프'하며 다닌다
Go의 기존 GC는 마크-스윕(mark-sweep) 방식이에요. 이게 뭐냐면, 먼저 '지금 살아있는 객체'를 전부 표시(mark)하고, 표시 안 된 나머지를 쓸어담는(sweep) 방식이에요. 살아있는 객체를 찾으려면 포인터를 따라가야 하는데요. A 객체가 B를 가리키고, B가 C를 가리키면 그 순서대로 쫓아가면서 하나씩 표시하는 거죠.
문제는 이 객체들이 메모리상에서 서로 멀리 떨어져 있다는 거예요. 포인터를 따라가다 보면 힙의 이쪽 끝에서 저쪽 끝으로 정신없이 점프하게 되는데, 이게 CPU 캐시 입장에서는 최악의 패턴이거든요. CPU는 메모리를 읽을 때 근처 데이터를 한꺼번에 캐시에 올려두는데, 매번 엉뚱한 곳으로 점프하면 애써 올려둔 캐시가 전부 무용지물이 돼요. 시각화를 보면 기존 GC는 힙 전체에 흩뿌리듯 무작위로 접근하는 모습이 그대로 보여요.
Green Tea의 아이디어: 뭉텅이로 모아서 순서대로 훑기
Green Tea GC의 핵심 아이디어는 의외로 단순해요. 객체 하나하나를 쫓아다니는 게 아니라, 스팬(span)이라고 부르는 연속된 메모리 블록 단위로 일을 모으는 거예요. 스캔해야 할 객체를 발견하면 바로 달려가는 게 아니라 '이 블록에 스캔할 게 있다'고 메모만 해두고, 나중에 그 블록 차례가 되면 블록 안의 객체들을 메모리 순서대로 한꺼번에 처리해요.
비유하자면 기존 방식은 택배 기사님이 주문 들어온 순서대로 동네 이쪽저쪽을 왔다 갔다 하는 거고, Green Tea는 같은 아파트 단지 물량을 모아뒀다가 한 번에 도는 거예요. 이동 거리, 그러니까 캐시 미스가 확 줄어들죠. 실제로 시각화에서도 새 GC는 힙을 거의 순차적으로, 물결이 지나가듯 훑는 모습이 보이는데요. 이런 접근 패턴은 CPU 캐시와 메모리 프리페처(다음에 읽을 데이터를 미리 가져다 놓는 하드웨어 기능)가 제일 좋아하는 형태거든요. Go 팀 벤치마크 기준으로 GC에 쓰는 CPU 시간이 워크로드에 따라 10~40%까지 줄었다고 해요. 게다가 블록 단위로 일을 모으니까 SIMD(한 번의 명령으로 여러 데이터를 동시에 처리하는 CPU 기능) 같은 최적화를 얹을 여지도 생겼고요.
다른 언어들과 비교하면
Java 진영은 ZGC나 Shenandoah처럼 객체를 옮겨가며 메모리를 압축하는 GC로 진화해왔고, '새로 만든 객체는 금방 죽는다'는 통계에 기대는 세대별(generational) GC 전략도 오래 다듬어왔어요. 반면 Go는 객체를 옮기지 않는 단순한 구조를 그대로 유지하면서, 하드웨어 친화적인 접근 순서라는 전혀 다른 각도로 성능을 끌어올린 셈이에요. 요즘 성능 이야기에서 '알고리즘 복잡도보다 메모리 접근 패턴이 진짜 병목'이라는 말이 자주 나오는데, Green Tea는 그 흐름을 GC 설계에 적용한 대표 사례라고 볼 수 있어요.
우리한테 어떤 의미냐면
Go로 API 서버나 데이터 처리 파이프라인을 운영 중이라면, 특히 객체를 많이 만들었다 버리는(할당이 많은) 서비스라면 직접 체감할 수 있는 변화예요. 최신 Go 버전에서 GOEXPERIMENT=greenteagc 플래그로 켜볼 수 있으니, 스테이징 환경에서 pprof로 GC 시간을 전후 비교해보는 것만으로도 좋은 공부가 되거든요. 그리고 GC를 직접 만들 일이 없더라도, '데이터를 어떤 순서로 접근하느냐가 성능을 좌우한다'는 교훈은 일반 애플리케이션 코드에도 그대로 적용돼요. 슬라이스를 순차 순회하는 코드가 맵을 무작위로 뒤지는 코드보다 빠른 이유도 결국 같은 원리거든요.
정리하면, Go의 새 GC는 '포인터 따라 점프'에서 '메모리 순서대로 행진'으로 청소 전략을 바꿔서 현대 하드웨어를 제대로 활용하게 된 거예요. 여러분 서비스에서는 GC 오버헤드가 어느 정도인가요? Green Tea로 바꿨을 때 효과를 볼 만한 워크로드인지, pprof 결과를 서로 공유해보면 재밌을 것 같아요.
🔗 출처: Hacker News