GROWTH LOG STORIES

함께 만든 성장을,
기록으로 연결합니다.

AI와 함께 배우고, 직접 만들고, 서로 나눈
그로스로그 멤버들의 생생한 성장 기록을 만나보세요.

성장 기록
5기+이어진 커뮤니티
HEALTHIO
PROJECT

사진 한 장으로 시작하는 건강 루틴, Health IO

대표 이야기 읽기

ALL STORIES

성장의 순간을
한곳에서 만나보세요.

프로젝트의 시작부터 모임에서 나눈 인사이트까지,
그로스로그가 함께 만든 기록을 모았습니다.
개의 성장 기록

성장일지

[GL 3기] 싱글 스레드가 비동기를 해내는 방법, 그리고 자바가 메모리를 치우는 방식 (성장일지 2회차 큐레이션 ①) 🌱

GROWTH LOG🌱 2026. 8. 7. 05:21

안녕하세요! 그로스로그 입니다!😊 🌱

한 번에 한 가지만 하는 언어가 어떻게 여러 일을 동시에 해내는지, 그리고 아무도 안 부탁했는데 메모리가 정리되는 원리. 3기 2회차 첫 번째입니다.

성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 매 회차 운영진이 모든 글을 읽고, 그중 특히 마음에 남은 글을 함께 골라 큐레이션합니다.

이번 회차는 큐레이션이 네 편이라 두 번에 나눠 소개합니다. 먼저 언어가 대신 해주는 일의 내부를 파고든 두 편입니다.

 

01

🔄 자바스크립트 런타임

이종경 님

이종경 님 원문에 실린 이미지
이런 내용이에요
자바스크립트는 한 번에 하나만 처리하는 싱글 스레드 언어입니다. 그런데 우리는 매일 비동기 코드를 씁니다. 어떻게 가능한 걸까요.
글은 엔진과 호스트 환경을 갈라 설명하는 데서 시작합니다. 그리고 콜 스택, 힙, Web API, 두 종류의 큐, 이벤트 루프를 하나씩 짚어요.
마지막에 네 줄짜리 예제로 실행 순서를 검증합니다. 개념이 코드로 확인되는 구조입니다.

"자바스크립트는 싱글 스레드다"와 "자바스크립트로 비동기 처리를 한다"는 말이 나란히 있으면 이상하게 들립니다. 이 글이 그 틈을 메웁니다.

조금 더 들어가 보면

글이 가장 먼저 잡는 구분이 엔진과 호스트 환경입니다. 여기가 핵심이에요.

엔진(V8, SpiderMonkey, JavaScriptCore)은 자바스크립트 언어 자체만 압니다. 함수가 어떻게 동작하는지, Promise가 무엇인지 알고, 콜 스택을 관리하고 힙에 메모리를 할당합니다.

그런데 글의 이 문장이 정확합니다.

"엔진은 function이나 Promise가 어떻게 동작하는지 알고 있지만, document.querySelectorsetTimeout이 무엇인지는 알지 못합니다."

setTimeout은 자바스크립트 문법이 아닙니다. 브라우저가 제공하는 기능이에요. 언어 명세서를 아무리 뒤져도 안 나옵니다.

이 구분을 잡으면 여러 게 정리됩니다. Node.js에서 document가 없는 이유, 브라우저에서 파일을 못 읽는 이유가 전부 여기서 나와요. 엔진은 같은데 호스트가 붙여주는 도구가 다른 겁니다.

⏱️ 그래서 비동기는 누가 하는가

여기서 답이 나옵니다. 비동기 작업을 실제로 처리하는 건 자바스크립트가 아닙니다.

setTimeout(fn, 1000)을 부르면, 자바스크립트는 그 요청을 브라우저에게 넘기고 바로 다음 줄로 갑니다. 1초를 세는 일은 브라우저가 백그라운드에서 합니다. 글이 짚은 것처럼 "브라우저가 멀티스레드로 처리하거나 OS 커널의 도움을 받아" 처리하는 거죠.

그러니까 정확히 말하면 "자바스크립트가 비동기를 한다"가 아니라 "자바스크립트가 남에게 시키고 결과만 받는다"입니다. 싱글 스레드인데 여러 일이 되는 이유가 이거예요.

받는 통로가 이고, 큐에서 꺼내 실행하는 관리자가 이벤트 루프입니다. 이벤트 루프가 하는 일은 단순합니다. 콜 스택이 비었는지 계속 확인하다가, 비면 큐에서 하나 꺼내 올립니다.

여기서 중요한 게 "콜 스택이 완전히 비어야 한다"는 조건입니다. 동기 코드가 다 끝나기 전에는 아무리 오래 기다린 콜백도 실행되지 않아요. 무거운 반복문이 돌면 그 안에 있는 setTimeout 콜백은 계속 대기합니다.

📝 원문에서는 이렇게 확인합니다

네 줄로 전부 검증합니다.

console.log("start");

setTimeout(() => {          // macro task queue
  console.log("timeout callback");
}, 0);

Promise.resolve().then(() => {   // micro task queue
  console.log("promise callback");
});

console.log("end");

출력은 이렇습니다.

start
end
promise callback
timeout callback

setTimeout에 0을 줬는데도 맨 마지막입니다. 여기에 두 가지 사실이 들어 있어요.

첫째, 0ms도 즉시가 아닙니다. 큐를 반드시 한 번 거칩니다. "지금 당장"이 아니라 "동기 코드가 다 끝난 다음 가장 빨리"라는 뜻이에요.

둘째, promise callbacktimeout callback보다 먼저 나옵니다. 큐가 두 종류이기 때문입니다.

· 마이크로태스크 큐Promise.then, MutationObserver

· 매크로태스크 큐setTimeout, setInterval, 이벤트 콜백

글이 짚은 규칙이 정확합니다. 이벤트 루프는 매크로태스크를 꺼내기 전에 마이크로태스크 큐를 전부 비웁니다. 그래서 Promise가 항상 먼저예요.

이 우선순위를 알면 실무에서 쓸 데가 있습니다. 마이크로태스크가 계속 자기를 다시 등록하면 매크로태스크는 영원히 실행되지 않습니다. Promise 안에서 Promise를 무한히 만드는 코드가 화면을 완전히 멈추게 만드는 이유죠. 반복문이 없는데도 브라우저가 죽는 경우입니다.

큐레이터 노트

개념을 코드로 검증한 구조라서 뽑았습니다.

이벤트 루프는 설명 자료가 정말 많은 주제입니다. 그런데 대부분 그림만 보여주고 끝나요. 이 글은 네 줄짜리 예제를 두고 한 단계씩 따라갑니다. "이 시점에 콜 스택이 비었고, 그래서 이벤트 루프가 마이크로태스크부터 확인한다"는 식으로요. 읽고 나면 다른 코드에도 같은 추적을 해볼 수 있습니다.

엔진과 호스트 환경을 먼저 가른 것도 좋은 선택이었습니다. 여기를 안 나누면 "setTimeout은 자바스크립트 기능"이라는 오해가 계속 남거든요. 순서가 잘 잡힌 정리입니다.

이 저자는 2기에서도 브라우저 렌더링 과정을 다뤘었죠. "내가 매일 쓰는 환경이 실제로 어떻게 돌아가는가"를 계속 파고들고 있습니다.

원문 읽으러 가기  ↗velog.io/@jong-kyung/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%…
 

02

🧹 [JAVA] 가비지 컬렉션 (Garbage Collection, GC)

박지수B 님

박지수B 님 원문에 실린 이미지
이런 내용이에요
1회차의 JVM 메모리 구조 글에 이어지는 두 번째 편입니다. 이번엔 Heap을 정리하는 GC를 다뤄요.
Stop-The-World가 왜 생기는지, Mark and Sweep이 어떤 순서로 도는지, 그리고 Serial부터 ZGC까지 일곱 가지 GC 알고리즘을 각각의 적용 상황과 JVM 옵션까지 붙여 정리했습니다.
개념 소개를 넘어 "우리 서비스엔 뭘 써야 하나"에 답할 수 있는 형태로 만들어졌습니다.

앞 회차의 글을 읽고 나면 자연히 따라오는 질문에 답하는 글입니다. "그래서 그 Heap은 누가 언제 치우나."

조금 더 들어가 보면

GC를 배울 때 가장 먼저 잡아야 할 게 비용입니다. 글이 이걸 먼저 짚습니다.

자바가 메모리를 자동으로 관리해주는 건 분명한 장점입니다. C/C++에서 손으로 해제하다 실수하면 메모리 누수나 이중 해제 같은 사고가 나니까요. 그런데 공짜가 아닙니다.

Stop-The-World — GC가 도는 동안 GC 스레드를 뺀 모든 스레드가 멈춥니다. 사용자 요청을 처리하던 스레드도 멈춰요. 그 순간 서비스는 응답하지 않습니다.

이게 왜 무서운지는 숫자로 보면 분명합니다. GC가 1초 걸린다면, 그 1초 동안 들어온 모든 요청이 1초씩 늦어집니다. 초당 100건을 받는 서비스라면 100건이 한꺼번에 밀립니다. "평소엔 빠른데 가끔 확 느려진다"는 증상의 흔한 원인이에요.

그래서 글의 이 문장이 핵심입니다. "GC가 너무 자주 실행되어도 소프트웨어의 성능에 문제가 될 수 있다." GC 튜닝이 존재하는 이유죠.

🔁 Mark and Sweep, 그리고 age

GC가 실제로 도는 방식은 세 단계입니다.

· Mark — 살아 있는(reachable) 객체를 표시

· Sweep — 표시되지 않은(unreachable) 객체를 해제

· Compaction — 흩어진 빈 공간을 앞으로 몰아 정리

여기서 "살아 있다"의 기준이 중요합니다. "쓰이고 있다"가 아니라 "닿을 수 있다"예요. GC 루트(스택의 지역 변수, static 필드 등)에서 참조를 따라가 도달 가능하면 살아 있는 겁니다. 그래서 아무도 안 쓰는데 리스트에 담겨만 있어도 절대 안 치워집니다. 자바 메모리 누수가 생기는 지점이 대부분 여기예요.

그리고 1회차 글에서 나왔던 Survivor 규칙의 이유가 이 글에서 밝혀집니다.

"Survivor 0 또는 Survivor 1 둘 중 하나는 꼭 비어 있어야 한다. 이는 GC 발생 시 Eden + 사용 중인 Survivor 영역의 살아있는 객체들을 다른 비어 있는 Survivor 영역으로 복사하는 객체 복사(Copying) 방식 때문이다."

빈 공간을 항상 하나 준비해두고 거기로 통째로 옮기는 방식입니다. 옮기고 나면 원래 영역은 통째로 비니까, 하나씩 지울 때 생기는 메모리 조각이 아예 안 생겨요. 앞 글에서 규칙만 알았다면 이 글에서 이유를 얻습니다.

age 개념도 실용적입니다. Survivor에서 살아남을 때마다 1씩 오르고, 임계값을 넘으면 Old로 승격(promotion)됩니다. "얼마나 오래 살아남았는가"를 숫자로 세는 거예요.

🧭 일곱 가지 GC를 고르는 기준

이 글에서 가장 값진 부분입니다. 알고리즘마다 적용 상황과 JVM 옵션을 붙여뒀어요.

정리해보면 하나의 축이 보입니다. 처리량(throughput)이냐 응답 시간(latency)이냐.

· Serial GC — 싱글 스레드. Stop-The-World가 가장 김. 실무에선 거의 안 씀

· Parallel GC — Young 영역을 멀티스레드로. 응답 시간보다 처리량이 중요할 때

· Parallel Old GC — Old 영역까지 멀티스레드 + Compaction. 대용량 배치에 적합

· CMS GC — 애플리케이션과 동시 실행으로 멈춤을 줄임. Java 9에서 deprecated, JDK 14에서 제거

· G1 GCJava 9부터 기본값. Heap을 region으로 쪼개 유연하게 씀

· Shenandoah GC — JDK 12. 힙 크기와 무관하게 일정한 응답 시간

· ZGC — JDK 11. 수십 GB~TB 힙에서 멈춤을 1~2ms로 유지

여기서 G1 GC의 설계가 특히 눈여겨볼 만합니다. 기존 GC는 Young과 Old의 위치가 물리적으로 고정돼 있었어요. G1은 Heap을 여러 region으로 나누고, 각 region의 역할을 상황에 따라 바꿉니다. Eden이던 자리가 Old가 될 수도 있죠.

그리고 글이 짚은 대로 객체 이동도 순차적이지 않습니다. Survivor1의 객체가 Eden으로 가는 게 낫다고 판단되면 그쪽으로 보냅니다. "정해진 길"이 아니라 "그때그때 나은 길"로 바뀐 거예요.

또 하나 실용적인 단서가 있습니다. "Heap이 너무 작을 경우 G1 사용을 권장하지 않는다." 새 GC가 항상 좋은 게 아니라는 뜻이에요. region 관리 자체에도 비용이 들거든요. 작은 힙에는 단순한 GC가 낫습니다.

큐레이터 노트

앞 글에서 남긴 질문에 스스로 답했기 때문에 뽑았습니다.

1회차 글에서 "Survivor 하나는 비어 있어야 한다"는 규칙이 나왔는데, 그때는 이유가 없었어요. 이 글에서 복사 방식 때문이라는 답이 나옵니다. 한 편을 쓰고 끝내는 게 아니라, 남은 궁금증을 다음 편에서 메우는 방식입니다.

선택 기준까지 만들어둔 것도 좋았습니다. GC 알고리즘 소개 글은 많은데, 대부분 나열에서 끝납니다. 이 글은 각각에 "어떤 상황에 적합한지"JVM 옵션을 붙였어요. 실제로 성능 문제를 만났을 때 펼쳐볼 수 있는 형태입니다.

그리고 CMS가 제거됐다는 것까지 적어둔 게 정확합니다. 인터넷에 남아 있는 자료 상당수가 CMS를 권하고 있거든요. 최신 상태를 확인해 적어두면, 읽는 사람이 사라진 옵션을 붙들고 헤매지 않습니다.

원문 읽으러 가기  ↗zisooya.tistory.com/25

💡 두 편을 겹쳐 읽으며

한 편은 자바스크립트, 한 편은 자바입니다. 언어도 다르고 다루는 층위도 다릅니다.

그런데 두 글은 같은 질문을 합니다. "내가 안 시켰는데 알아서 되는 일들, 실제로는 누가 하고 있는가."

비동기 코드를 쓸 때 우리는 그냥 setTimeout을 부릅니다. 객체를 만들 때 해제를 신경 쓰지 않고요. 둘 다 "알아서 되는 일"입니다.

그런데 두 글이 열어보니 실제로는 이렇습니다. 타이머는 브라우저가 백그라운드에서 세고 있었고, 메모리는 GC 스레드가 모든 걸 멈춰 세우고 치우고 있었어요.

그리고 둘 다 대가가 있습니다. 콜 스택이 안 비면 콜백은 영원히 실행되지 않고, GC가 도는 동안 서비스는 응답하지 않습니다. 편의는 공짜가 아니라 뒤로 미뤄둔 비용이에요.

이걸 아는 사람과 모르는 사람의 차이는 문제가 생겼을 때 드러납니다. "화면이 왜 멈추지", "왜 가끔 응답이 튀지" 같은 상황에서 볼 곳을 아는가가 갈리거든요.

두 저자 모두 자기 언어의 밑바닥을 열어봤습니다. 결이 잘 맞는 회차였어요.

이번 회차에서 가져갈 것

1. setTimeout(fn, 0)은 "즉시"가 아니다. 큐를 반드시 한 번 거칩니다. 동기 코드가 전부 끝난 뒤에 실행돼요. "0인데 왜 나중에 실행되지"의 답입니다.

2. Promise가 항상 setTimeout보다 먼저. 마이크로태스크 큐가 매크로태스크 큐보다 우선입니다. 이벤트 루프는 마이크로태스크를 다 비운 다음에야 매크로태스크로 넘어가요.

3. GC의 "살아 있다"는 "쓰인다"가 아니라 "닿을 수 있다". 아무도 안 쓰는 객체도 리스트에 담겨만 있으면 안 치워집니다. 자바 메모리 누수는 대개 여기서 생겨요.

4. 응답이 가끔 튀면 GC를 의심하기. 평소엔 빠른데 주기적으로 느려진다면 Stop-The-World일 수 있습니다. GC 로그를 켜서 멈춤 시간을 재보세요.

5. 새 GC가 항상 정답은 아니다. G1은 Java 9부터 기본값이지만 힙이 작으면 오히려 손해입니다. 알고리즘은 힙 크기와 목표(처리량 vs 응답 시간)를 보고 고르는 겁니다.


좋은 글 남겨주신 이종경 님, 박지수B 님께 감사드립니다! 😊

2회차 두 번째 큐레이션으로 이어집니다! 🌱

- 에디터 · 성장일지 큐레이터 -


※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.

WRITE YOUR GROWTH

오늘의 배움을
내일의 성장으로.

기록은 나를 돌아보게 하고, 공유는 우리를 연결합니다.
당신의 다음 성장 이야기도 그로스로그에서 시작해보세요.

커뮤니티 둘러보기