프로그래밍 언어 비교
Java, Go, Python, JavaScript/TypeScript를 중심으로 언어가 서로 다른 지점 — 실행 방식, 타입, 메모리, 동시성, 오류 처리, 패키지 도구 — 을 같은 축 위에 놓고 비교한다. 이벤트 루프와 고루틴 스케줄러는 직접 돌려 본 단계 그림으로 따라간다.
목차 (14)
언어를 비교하는 축
"어느 언어가 더 좋은가"는 답이 없는 질문이지만, "이 두 언어는 어디서 갈리나"에는 답이 있다. 언어는 몇 가지 설계 결정의 묶음이고, 그 결정마다 무언가를 얻고 무언가를 내준다. 이 노트는 다섯 축으로 본다. 소스가 언제 기계어가 되는지(실행 방식), 타입 오류를 언제 잡는지(타입), 메모리를 누가 돌려주는지(메모리 관리), 여러 일을 어떻게 겹치는지(동시성), 무엇을 가져다 쓸 수 있는지(생태계)다.
축은 서로 얽혀 있다. GC가 있으면 런타임이 필요하고, 런타임이 있으면 시작이 느려질 수 있으며, 동적 타입이면 JIT이 실행 중 타입을 보고 특수화할 여지가 커진다. 그래서 언어 하나를 고르면 여러 축의 값을 한꺼번에 고르게 된다.
실행 방식: 네이티브, 바이트코드 + JIT, 인터프리터 + JIT
네이티브 컴파일(C·C++·Rust·Go)은 빌드 때 운영체제와 CPU에 맞는 기계어 실행 파일을 만든다. 실행할 때 따로 설치할 런타임이 없고 시작이 빠르다. 대신 OS·CPU 조합마다 따로 빌드해야 한다. Go는 런타임(스케줄러·GC)을 실행 파일 안에 함께 넣어 파일 하나로 배포된다.
바이트코드 + JIT(Java·Kotlin·C#)은 빌드 때 가상 기계의 명령(JVM 바이트코드, .NET IL)으로 컴파일하고, 실행할 때 VM이 이를 해석하다가 자주 도는 부분을 기계어로 바꾼다. 같은 산출물이 어느 OS에서든 돈다. 인터프리터 + JIT(Python·JavaScript)은 소스를 그대로 배포하고 실행할 때 파싱부터 한다. V8은 여러 단계의 JIT으로 JavaScript를 기계어로 바꾸고, CPython은 주로 바이트코드를 해석한다.
렉서·파서·바이트코드 VM·JIT이 안에서 어떻게 도는지는 컴파일러 노트의 "인터프리터, 바이트코드 VM, JIT" 절에, JVM·CLR·WebAssembly 같은 "없는 기계를 만드는" 언어 VM은 가상화 노트의 "언어 가상 머신" 절에 있다. 이 노트에서는 그 결과가 쓰는 사람에게 어떻게 보이는지만 다룬다.
타입: 정적·동적, 강·약, 추론, 제네릭, null
정적 타입은 실행 전에 컴파일러가 타입을 검사한다. 오타 난 필드 이름, 잘못된 인자 개수가 빌드에서 걸리고, 편집기가 자동 완성과 이름 바꾸기를 정확히 해 준다. 동적 타입은 값에 타입이 붙어 있고 실행할 때 검사한다. 코드가 짧고 실험이 빠르지만, 오류는 그 줄이 실제로 돌 때 드러난다.
이와 따로 강·약은 서로 다른 타입을 섞을 때 언어가 알아서 바꿔 주는 정도다. Python에서 "1" + 1은 TypeError지만 JavaScript는 "11"을 돌려준다. 둘 다 동적 타입이지만 Python 쪽이 더 강하다. 정적·동적이 "언제"라면 강·약은 "얼마나 엄격하게"다.
| 언어 | 검사 | 타입 추론 | 제네릭 | null 안전성 |
|---|---|---|---|---|
| Java | 정적 · 강함 | 지역 변수 var | 있음(실행 때 타입 인자가 지워짐) | 모든 참조가 null일 수 있음. Optional은 관례 |
| Kotlin | 정적 · 강함 | 넓음 | 있음 | 타입에 표시: String과 String?를 구분 |
| Go | 정적 · 강함 | := 로 지역 변수 | 1.18부터 타입 매개변수 | 포인터·맵·인터페이스는 nil 가능 |
| Rust | 정적 · 강함 | 함수 안에서 거의 전부 | 있음(타입마다 코드 생성) | null이 없음. 없을 수 있으면 Option |
| TypeScript | 정적(컴파일 때만) | 넓음 | 있음 | strictNullChecks를 켜면 검사 |
| Python | 동적 · 강함 | — | 타입 힌트로 표기만 | None이 어디든 올 수 있음(검사기는 선택) |
| JavaScript | 동적 · 약함 | — | — | null과 undefined 둘 다 있음 |
- 타입 추론: 정적 타입이라고 타입을 다 적어야 하는 것은 아니다. TypeScript는
let name = "Alice"에서string을, Go는x := 3에서int를 알아낸다. 추론이 넓을수록 정적 타입의 장황함이 줄어든다. - 제네릭: "무엇이든 담는 리스트"를 타입 안전하게 쓰는 장치다. Go는 오랫동안 없다가 1.18에서 타입 매개변수가 들어왔다. Java는 컴파일 때만 타입 인자를 검사하고 실행 때는 지운다.
- null 안전성: null 참조는 가장 흔한 실행 중 오류 가운데 하나다. Kotlin은
String에 null을 넣으면 컴파일 오류이고, null이 될 수 있는 값은String?로 적은 뒤?.로 안전하게 부른다. TypeScript도strictNullChecks를 켜면 같은 검사를 하고, Rust는 null 자체가 없이Option으로 "없을 수 있음"을 표현한다.
타입 검사기가 안에서 하는 일(타입 규칙과 추론)은 컴파일러 노트의 "타입 시스템" 절에 있다.
메모리 관리: 수동, 참조 카운트, GC, 소유권
힙에 만든 객체를 언제 돌려줄지는 언어마다 다르다. C·C++은 프로그래머가 직접 해제한다(C++은 소멸자와 스마트 포인터로 상당 부분을 자동화한다). 빠르고 예측 가능하지만, 해제한 메모리를 다시 쓰거나 두 번 해제하는 버그가 보안 취약점으로 이어진다. Java·Go·C#·JavaScript는 추적 GC가 루트에서 닿지 않는 객체를 찾아 회수한다. CPython은 참조 카운트로 대부분을 즉시 해제하고, 서로를 가리키는 순환은 따로 도는 순환 수집기가 치운다. Rust는 소유권 규칙으로 해제 시점을 컴파일 때 정한다.
GC라고 다 같지 않다. Java는 JDK 9부터 G1이 기본이고 멈춤 시간을 줄이는 데 무게를 둔다. Go의 GC는 객체를 옮기지 않는 동시 마크 앤 스윕이라 프로그램과 나란히 돌고, GOGC로 메모리를 더 쓰는 대신 GC를 덜 돌릴지 정한다(1.19부터는 GOMEMLIMIT로 메모리 상한도 준다). 마크 앤 스윕이 한 단계씩 어떻게 도는지, 세대별 GC가 왜 효과적인지, 소유권과 빌림이 어떻게 해제 코드를 정하는지는 컴파일러 노트의 "가비지 컬렉션"과 "메모리 관리 방식 비교" 절에 단계 그림으로 있다.
동시성 모델 비교
서버는 요청 수천 개를 동시에 다룬다. 대부분의 시간은 데이터베이스·다른 서비스의 응답을 기다리는 데 쓰인다. 언어마다 이 "기다리는 동안 다른 일 하기"를 다르게 표현한다.
- OS 스레드(전통적인 Java): 요청마다 스레드 하나. 코드는 위에서 아래로 읽히고 막히는(blocking) 호출을 그냥 쓴다. 대신 스레드마다 스택 메모리와 커널 문맥 교환 비용이 들어 수만 개로 늘리기 어렵다. JDK 21에서 확정된 가상 스레드는 JDK가 많은 가상 스레드를 적은 플랫폼 스레드(캐리어) 위에 올려 돌린다. 가상 스레드가 입출력에서 막히면 캐리어에서 내려오고 그 자리를 다른 가상 스레드가 쓴다.
- 고루틴(Go):
go f()한 줄로 시작하는 가벼운 실행 흐름이다. 스택이 작게 시작해 필요할 때 자라므로 싸고, 런타임이 여러 OS 스레드에 다중화해 하나가 입출력에서 막혀도 나머지가 계속 돈다. 고루틴끼리는 채널로 값을 주고받는다. - async/await + 이벤트 루프(JavaScript, Python asyncio): 스레드 하나가 이벤트 루프를 돌며 준비된 콜백을 하나씩 끝까지 실행한다.
await는 "여기서 기다리는 동안 루프에 자리를 내준다"는 표시다. 락이 거의 필요 없고 연결 수만 개를 가볍게 다루지만, 콜백 하나가 CPU를 오래 쓰면 모두가 멈춘다.
스크립트 자체도 하나의 작업이다. setTimeout(…, 0)도 "지금 당장"이 아니라 타이머 큐에 줄을 선다.
Node.js의 이벤트 루프는 실제로는 타이머, 대기 중인 콜백, poll(새 입출력 이벤트), check(setImmediate), close 콜백 같은 여러 단계를 돈다. process.nextTick 콜백은 단계와 상관없이 지금 작업이 끝나는 즉시 돈다. 위 그림은 그 가운데 "매크로태스크 하나, 그다음 마이크로태스크 전부"라는 뼈대만 남겼다. 브라우저에서 태스크·마이크로태스크와 화면 갱신이 어떻게 맞물리는지, 긴 태스크가 반응성(INP)을 어떻게 해치는지는 웹 개발 노트의 이벤트 루프 절에 있다. Python asyncio도 같은 모양의 루프를 돌리되, 콜백 대신 코루틴이 await에서 멈췄다가 이어진다.
Go 런타임 스케줄러는 세 가지를 구분한다. G는 고루틴, M은 실제 OS 스레드, P는 Go 코드를 실행하는 데 필요한 자원(논리 프로세서)이다. M은 P를 쥐어야 Go 코드를 돌릴 수 있고, P마다 실행할 G의 큐가 있다. 할 일이 떨어진 P는 다른 P의 큐에서 일을 가져온다.
G1~G5 다섯 고루틴, P 둘. P마다 OS 스레드(M)가 하나씩 붙어 실제로 돈다. P1에 일이 몰려 있다.
GIL(전역 인터프리터 락)은 CPython에서 한 번에 한 스레드만 바이트코드를 실행하게 하는 락이다. 딕셔너리 같은 내장 객체가 따로 락 없이도 안전해 구현이 단순해지는 대신, CPU만 쓰는 스레드를 늘려도 코어를 나눠 쓰지 못한다. 입출력을 기다릴 때는 GIL을 놓으므로 입출력이 많은 프로그램에서는 스레드가 여전히 쓸모 있다. 그래서 Python은 CPU 병렬이 필요하면 프로세스를 여러 개 띄우거나(multiprocessing), GIL을 놓고 계산하는 C 확장(NumPy 등)에 맡겨 왔다.
3.13에서 GIL을 끈 free-threaded 빌드(PEP 703)가 들어왔고, 3.14(2025년 10월 7일 출시)에서는 PEP 779에 따라 공식 지원 빌드가 됐다. 다만 여전히 선택해서 설치하는 별도 빌드이고 기본은 GIL이 있는 빌드다. 3.14 문서는 단일 스레드 코드의 성능 손해를 플랫폼과 컴파일러에 따라 대략 5~10%로 적는다. free-threading을 지원한다고 표시하지 않은 C 확장 모듈을 불러오면 GIL이 다시 켜질 수 있다. 병렬과 동시성의 차이, 락과 원자적 연산은 병렬 컴퓨팅 노트와 운영체제 노트의 동기화 절에 있다.
같은 일을 네 언어로: HTTP 요청 여러 개를 동시에
주소 목록 urls의 내용을 동시에 받아 길이를 찍는 코드다. 요청 넷이 각각 120, 300, 80, 200 ms 걸린다면, 하나씩 보내면 700 ms, 동시에 보내면 가장 느린 것 하나인 300 ms면 끝난다. 네 언어 모두 이 "기다림 겹치기"를 하지만 표현이 다르다.
Java(JDK 21 이상): 요청마다 가상 스레드를 하나씩 만든다. 코드는 막히는 호출(client.send) 그대로인데, 막히는 동안 캐리어 스레드를 다른 가상 스레드가 쓴다.
var client = HttpClient.newHttpClient();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = urls.stream()
.map(url -> executor.submit(() -> client.send(
HttpRequest.newBuilder(URI.create(url)).build(),
HttpResponse.BodyHandlers.ofString()).body()))
.toList();
for (var future : futures) {
System.out.println(future.get().length());
}
}Go: 요청마다 고루틴을 띄우고 sync.WaitGroup으로 모두 끝나기를 기다린다. 결과는 각자 자기 칸(sizes[i])에만 써서 락이 필요 없다. 오류를 반환값으로 받아 직접 확인하는 것도 Go답다.
var wg sync.WaitGroup
sizes := make([]int, len(urls))
for i, url := range urls {
wg.Add(1)
go func(i int, url string) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
sizes[i] = len(body)
}(i, url)
}
wg.Wait()Python: 표준 라이브러리의 urllib은 막히는 방식이라, 비동기 HTTP 클라이언트(여기서는 서드파티 httpx)와 asyncio를 쓴다. asyncio.gather가 코루틴 여럿을 한꺼번에 기다린다.
import asyncio
import httpx
async def main(urls):
async with httpx.AsyncClient() as client:
responses = await asyncio.gather(*(client.get(url) for url in urls))
for response in responses:
print(len(response.text))
asyncio.run(main(urls))JavaScript(브라우저, Node.js 18 이상): fetch가 프라미스를 돌려주니 Promise.all로 모아 기다리면 끝이다. 언어 자체가 처음부터 이벤트 루프 위에서 돌기 때문에 가장 짧다.
const responses = await Promise.all(urls.map((url) => fetch(url)));
for (const response of responses) {
const text = await response.text();
console.log(text.length);
}실무에서는 여기에 두 가지를 더한다. 동시에 보내는 수의 상한(세마포어, 워커 풀)과 요청마다의 시간 제한(Go의 context, JavaScript의 AbortController)이다. 상한 없이 만 개를 한꺼번에 보내면 상대 서버나 내 연결 풀이 먼저 넘어진다.
오류 처리: 예외 vs 값 반환
Java·Python·JavaScript·C#은 예외를 던진다. 오류가 난 곳에서 throw하면 호출 스택을 거슬러 올라가 처음 만나는 catch로 날아간다. 정상 흐름 코드가 깔끔해지는 대신, 함수 시그니처만 봐서는 무엇이 날아올지 모르고(Java의 checked exception은 예외), 중간 층은 오류가 지나가는 줄도 모른다.
Go와 Rust는 오류를 값으로 반환한다. Go 함수는 관례상 마지막 반환값으로 error(문자열을 돌려주는 Error() 메서드 하나짜리 인터페이스)를 돌려주고, 호출한 쪽이 if err != nil로 확인한다. 장황하다는 평이 많지만 오류가 어디서 생겨 어디로 가는지가 코드에 그대로 보인다. 감쌀 때는 %w로 원래 오류를 품어 둔다.
user, err := repo.Find(id)
if err != nil {
return fmt.Errorf("find user %d: %w", id, err)
}Rust의 Result는 "성공 값 또는 오류 값" 하나를 담는 타입이라, 확인하지 않고 성공 값을 꺼내 쓸 수 없다. ? 연산자가 "오류면 그대로 위로 돌려준다"를 한 글자로 줄인다. 배열 범위를 넘는 것처럼 고칠 수 없는 버그는 panic!으로 프로그램을 멈춘다. Rust에는 예외가 없다.
fn load(path: &str) -> Result<Config, io::Error> {
let text = fs::read_to_string(path)?;
Ok(parse(&text))
}어느 쪽이든 원칙은 같다. 오류는 처리할 수 있는 층에서 처리하고, 위로 넘길 때는 맥락(어떤 id로 무엇을 하다가)을 덧붙이며, 삼키지 않는다. API가 오류를 바깥에 알리는 형식은 API 설계 노트의 오류 응답 절에 있다.
패키지와 빌드 도구
생태계의 뼈대는 비슷하다. 프로젝트가 원하는 라이브러리와 버전 범위를 선언 파일에 적고, 도구가 의존성의 의존성까지 풀어 버전을 정한 뒤, 그 결과를 잠금 파일에 정확한 버전과 해시로 남기고, 중앙 저장소에서 내려받는다. 잠금 파일을 버전 관리에 함께 올려야 오늘 내 노트북과 다음 달 CI가 같은 것을 받는다.
- Java·Kotlin: Maven(
pom.xml, XML 선언)과 Gradle(Kotlin·Groovy 스크립트)이 Maven Central에서 받는다. 빌드 도구가 컴파일·테스트·패키징까지 맡는 큰 도구다. - Go:
go mod가 언어 도구에 들어 있다. 버전 고르기는 최소 버전 선택(MVS)이다. 요구된 버전 가운데 가장 높은 것, 즉 모든 요구를 만족하는 최소 버전을 고르므로 새 버전이 나와도 저절로 올라가지 않는다. - Python: 오래 쓰인 pip +
requirements.txt에, 프로젝트 정보는pyproject.toml로 모였다. Rust로 쓴 uv는 pip·pip-tools·pipx·poetry·pyenv·virtualenv 등을 한 도구로 묶고, 운영체제·파이썬 버전을 가리지 않는 잠금 파일uv.lock을 만든다. - JavaScript·TypeScript: npm이
package.json을 읽고package-lock.json에 만들어진 트리를 정확히 적어 다음 설치가 같은 트리를 만들게 한다. 의존성이 깊고 많기로 유명해서 공급망 공격의 표적이 되기도 한다.
의존성을 고정하고 갱신하는 운영(자동 업데이트 봇, 취약점 경보)은 소프트웨어 공학 노트와 CI/CD 노트로 이어진다.
성능 특성과 시작 시간
"어느 언어가 몇 배 빠르다"는 숫자는 벤치마크마다 크게 다르고 코드 품질에 더 크게 좌우되므로 여기서는 경향만 적는다. 대체로 이렇다.
- 시작 시간: 네이티브 실행 파일(Go·Rust)이 가장 빠르게 첫 요청을 받는다. JVM은 VM을 띄우고 클래스를 읽는 시간이 있고, Python·Node.js는 그 중간쯤이다. 서버리스 함수나 CLI 도구처럼 자주 켜고 끄는 곳에서는 이 차이가 체감된다.
- 데워진 뒤의 처리량: JIT은 실행 중에 실제로 들어온 타입과 자주 도는 경로를 보고 최적화하므로, 오래 도는 서버에서 Java·C#은 네이티브 언어에 가깝게 따라붙는다. 그 전의 워밍업 구간에는 느리다.
- 순수 Python 루프는 위 언어들보다 확실히 느리다. 그래도 데이터 처리에서 Python이 쓰이는 것은 무거운 계산을 NumPy·PyTorch 같은 C·C++·CUDA 라이브러리가 하고 Python은 그것을 엮기 때문이다.
- 메모리: GC 언어는 회수를 미루는 만큼 여유 메모리를 더 쓴다. 컨테이너 메모리 한도를 정할 때 이 여유를 셈에 넣어야 한다.
대부분의 웹 서비스에서 응답 시간을 정하는 것은 언어보다 데이터베이스 쿼리, 네트워크 왕복, 캐시 적중률이다. 언어를 바꾸기 전에 먼저 프로파일러로 시간이 어디에 쓰이는지 재 보는 것이 순서다(성능 노트).
쓰임새 지도
분야마다 사실상의 기본 언어가 있다. 대개 기술적 우열보다 그 분야의 핵심 라이브러리·플랫폼이 어느 언어로 먼저 나왔느냐가 정했다.
- 웹 프런트엔드는 브라우저가 JavaScript를 실행하므로 선택지가 사실상 JavaScript와 그 위의 TypeScript다. 무거운 계산은 WebAssembly로 다른 언어를 컴파일해 넣기도 한다(웹 개발 노트).
- 백엔드는 가장 선택지가 넓다. 큰 조직의 오래된 시스템은 Java·C#, 클라우드 인프라와 마이크로서비스는 Go, 프런트엔드와 언어를 맞추려면 TypeScript(Node.js), 데이터·ML과 가까우면 Python이 흔하다.
- 모바일은 플랫폼이 언어를 정한다. Android는 Kotlin, iOS는 Swift가 공식 언어다(모바일 개발 노트).
- 시스템·임베디드는 런타임 없이 하드웨어 가까이에서 도는 C·C++이 오래 지배했고, 메모리 안전성을 이유로 Rust가 들어오고 있다(임베디드 노트).
언어 순위 지표의 한계
언어 순위는 자주 인용되지만 무엇을 세느냐에 따라 전혀 다른 목록이 나온다. TIOBE 지수는 매달 갱신되며, 검색 엔진에서 "언어 이름 + programming" 같은 검색어의 결과로 그 언어를 다루는 숙련 엔지니어·강좌·업체의 양을 어림한다. TIOBE 스스로 "가장 좋은 언어"나 작성된 코드의 양을 재는 것이 아니라고 밝힌다. 2026년 10월 순위는 Python, C, C++, Java, C# 순이다.
Stack Overflow 개발자 설문은 응답자에게 직접 묻는다. 2026년 설문은 열여섯 번째로 응답 30,903건을 모았고, "쓴 언어" 질문에서는 JavaScript, SQL, HTML/CSS, Python, Bash/Shell이 위에 있다. 하지만 응답자는 스스로 참여한 사람들이고(Stack Overflow를 쓰는 사람에 치우친다), 질문마다 답한 사람 수가 다르다.
두 지표 모두 "어느 언어를 배우면 일자리가 있나"나 "우리 팀에 무엇이 맞나"에는 간접적인 단서일 뿐이다. SQL·HTML·셸이 설문 위쪽에 있는 것처럼, 많이 쓰인다는 것은 다른 언어와 함께 곁들여 쓴다는 뜻이기도 하다. 순위가 한두 칸 오르내리는 것은 측정 방식의 잡음일 때가 많다.
언어 고르는 법
언어는 맨 앞의 강한 제약부터 걸러 내면 후보가 금방 좁혀진다. 실행 환경이 언어를 정하는 경우(브라우저, 모바일 OS), 런타임이나 GC를 둘 수 없는 경우(커널, 마이크로컨트롤러, 실시간 제어), 분야의 핵심 라이브러리가 한 언어에 몰린 경우(ML의 Python)다. 이 단계를 지나면 남는 것은 대개 "서버나 도구를 무엇으로 짤까"이고, 여기서는 여러 언어가 다 충분히 좋다.
마지막 단계에서 따질 것: 팀이 이미 잘 아는 언어인가(가장 큰 요인), 필요한 라이브러리와 SDK가 성숙한가, 채용할 수 있는가, 배포 환경이 무엇을 좋아하는가(시작 시간·메모리·단일 실행 파일), 10년 뒤에도 유지보수할 수 있는가. 이미 돌아가는 시스템을 다른 언어로 다시 쓰는 비용은 거의 언제나 예상보다 크다.
흔한 오해
- "인터프리터 언어는 느리다": 언어가 아니라 구현의 성질이다. JavaScript는 문법상 동적 언어지만 V8의 JIT이 기계어로 바꿔 돌리고, Python도 PyPy 같은 JIT 구현이 있다. 반대로 C도 인터프리터로 돌릴 수 있다. "CPython의 순수 Python 루프는 느리다"가 정확한 말이다.
- "GC가 있으면 메모리 누수가 없다": GC는 닿지 않는 객체만 치운다. 전역 캐시나 해제하지 않은 이벤트 리스너가 계속 붙잡고 있는 객체는 영원히 산다. 파일·소켓 같은 메모리 밖 자원은 GC가 제때 닫아 주지 않으니
try-with-resources,defer,with로 직접 닫는다. - "정적 타입이면 버그가 없다": 타입 검사는 "문자열 자리에 숫자"류의 오류를 막을 뿐, 논리 오류는 그대로다. TypeScript의 타입은 실행 때 지워지므로 서버에서 받은 JSON이 선언한 타입과 다르면 아무도 막지 않는다. 바깥에서 들어오는 데이터는 실행 중에 검증한다.
- "async를 쓰면 빨라진다": async는 기다리는 시간을 겹쳐 처리량을 늘릴 뿐, 계산 하나를 빠르게 하지 않는다. CPU를 쓰는 작업을 이벤트 루프에서 돌리면 오히려 모든 요청이 그동안 멈춘다.
- "Python은 GIL 때문에 스레드가 쓸모없다": 입출력을 기다리는 동안은 GIL을 놓으므로 입출력이 많은 일에는 스레드가 효과적이다. CPU 병렬은 프로세스, GIL을 놓는 확장 모듈, 3.13부터의 free-threaded 빌드로 얻는다.
- "고루틴은 공짜다": 싸지만 공짜는 아니다. 끝나지 않고 채널을 영원히 기다리는 고루틴은 그대로 메모리에 남는다(고루틴 누수). 시작한 쪽이 끝낼 방법(
context취소)도 함께 준다.
핵심 자료
언어 구현의 안쪽은 컴파일러 노트, 동시성의 바탕은 병렬 컴퓨팅 노트와 운영체제 노트에 있다.
| 자료 | 종류 | 요지 | 링크 |
|---|---|---|---|
| Effective Go | 공식 문서 | Go다운 코드의 관례. 고루틴이 여러 OS 스레드에 다중화된다는 설명과 error 인터페이스 관례가 여기 있다. | go.dev |
| JEP 444: Virtual Threads | JDK 제안서 | JDK 21에서 확정된 가상 스레드. JDK가 많은 가상 스레드를 적은 플랫폼 스레드(캐리어)에 M:N으로 올리는 방식을 설명한다. | OpenJDK |
| The Node.js Event Loop | 공식 문서 | 타이머·poll·check 등 이벤트 루프 단계와 process.nextTick이 어디서 도는지. | nodejs.org |
| Python support for free threading | 공식 문서 | GIL을 끈 CPython 빌드를 확인하고 쓰는 법. 확장 모듈이 지원하지 않으면 GIL이 다시 켜질 수 있다는 점까지. | docs.python.org |
| The Rust Programming Language — 9장 Error Handling | 공식 교재 | 복구 가능한 오류는 Result, 복구할 수 없는 오류는 panic!. Rust에 예외가 없는 이유. | rust-lang.org |
| A Guide to the Go Garbage Collector | 공식 문서 | 옮기지 않는 동시 마크 앤 스윕 GC와 GOGC·GOMEMLIMIT 손잡이. 추적 GC의 비용을 숫자로 생각하는 법. | go.dev |
- Effective Go — go.dev
- JEP 444: Virtual Threads — OpenJDK
- The Node.js Event Loop — nodejs.org
- Python support for free threading — docs.python.org
- The Rust Programming Language — 9장 Error Handling — rust-lang.org
- A Guide to the Go Garbage Collector — go.dev
- PEP 779 – Criteria for supported status for free-threaded Python — peps.python.org
- What's new in Python 3.14 — docs.python.org
- Glossary (global interpreter lock, reference count) — docs.python.org
- asyncio — Asynchronous I/O — docs.python.org
- runtime/proc.go (G·M·P 스케줄러 주석) — go.dev
- Go 1.18 Release Notes — go.dev
- Go Modules Reference — go.dev
- JEP 248: Make G1 the Default Garbage Collector — OpenJDK
- Null safety — kotlinlang.org
- Everyday Types — TypeScript Handbook
- uv — Astral
- Project structure and files (uv.lock) — Astral
- package-lock.json — npm Docs
- TIOBE Index — TIOBE
- 2026 Developer Survey — Stack Overflow
- 2026 Developer Survey: Technology — Stack Overflow