테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

프로그래밍 언어 비교

Java, Go, Python, JavaScript/TypeScript를 중심으로 언어가 서로 다른 지점 — 실행 방식, 타입, 메모리, 동시성, 오류 처리, 패키지 도구 — 을 같은 축 위에 놓고 비교한다. 이벤트 루프와 고루틴 스케줄러는 직접 돌려 본 단계 그림으로 따라간다.

목차 (14)
  1. 언어를 비교하는 축
  2. 실행 방식: 네이티브, 바이트코드 + JIT, 인터프리터 + JIT
  3. 타입: 정적·동적, 강·약, 추론, 제네릭, null
  4. 메모리 관리: 수동, 참조 카운트, GC, 소유권
  5. 동시성 모델 비교
  6. 같은 일을 네 언어로: HTTP 요청 여러 개를 동시에
  7. 오류 처리: 예외 vs 값 반환
  8. 패키지와 빌드 도구
  9. 성능 특성과 시작 시간
  10. 쓰임새 지도
  11. 언어 순위 지표의 한계
  12. 언어 고르는 법
  13. 흔한 오해
  14. 핵심 자료

언어를 비교하는 축

"어느 언어가 더 좋은가"는 답이 없는 질문이지만, "이 두 언어는 어디서 갈리나"에는 답이 있다. 언어는 몇 가지 설계 결정의 묶음이고, 그 결정마다 무언가를 얻고 무언가를 내준다. 이 노트는 다섯 축으로 본다. 소스가 언제 기계어가 되는지(실행 방식), 타입 오류를 언제 잡는지(타입), 메모리를 누가 돌려주는지(메모리 관리), 여러 일을 어떻게 겹치는지(동시성), 무엇을 가져다 쓸 수 있는지(생태계)다.

실행 방식, 타입, 메모리, 동시성, 생태계 다섯 축마다 질문과 대표 언어를 나란히 적은 표실행 방식소스가 언제 기계어가 되나미리(C·Rust·Go) · 실행 중 JIT(Java·JS) · 해석(CPython)타입타입 오류를 언제 잡나컴파일 때(Java·Go·TS·Rust) · 실행할 때(Python·JS)메모리쓴 메모리를 누가 언제 돌려주나직접(C·C++) · GC(Java·Go·JS) · 소유권(Rust)동시성여러 일을 어떻게 겹쳐 하나OS 스레드(Java) · 고루틴(Go) · 이벤트 루프(JS)생태계무엇을 가져다 쓸 수 있나Maven Central · Go 모듈 · PyPI · npm
다섯 축. 한 언어가 모든 축에서 한쪽 끝에 있지는 않다. Go는 네이티브로 컴파일되지만 GC가 있고, TypeScript는 정적 타입이지만 실행은 동적 언어인 JavaScript가 한다.

축은 서로 얽혀 있다. 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으로 바꾸며, Python과 JavaScript는 실행할 때 파싱부터 한다C · C++ · Rust · Go소스 코드컴파일러(빌드 때)기계어 실행 파일OS·CPU별로 따로CPU가 바로 실행Java · Kotlin · C#소스 코드컴파일러(빌드 때)바이트코드.class · .NET ILVM: 해석 → JITJVM · CLRPython · JavaScript소스 코드그대로 배포파서(실행할 때)바이트코드메모리 안에서VM: 해석 → JITCPython · V8프로그램을 실행하는 도중에 일어나는 일
실행 방식 세 갈래. 색칠한 칸이 프로그램을 실행하는 도중에 일어나는 일이다. 오른쪽으로 갈수록 배포는 쉽고, 실행 중에 하는 일이 많다.

렉서·파서·바이트코드 VM·JIT이 안에서 어떻게 도는지는 컴파일러 노트의 "인터프리터, 바이트코드 VM, JIT" 절에, JVM·CLR·WebAssembly 같은 "없는 기계를 만드는" 언어 VM은 가상화 노트의 "언어 가상 머신" 절에 있다. 이 노트에서는 그 결과가 쓰는 사람에게 어떻게 보이는지만 다룬다.

타입: 정적·동적, 강·약, 추론, 제네릭, null

정적 타입은 실행 전에 컴파일러가 타입을 검사한다. 오타 난 필드 이름, 잘못된 인자 개수가 빌드에서 걸리고, 편집기가 자동 완성과 이름 바꾸기를 정확히 해 준다. 동적 타입은 값에 타입이 붙어 있고 실행할 때 검사한다. 코드가 짧고 실험이 빠르지만, 오류는 그 줄이 실제로 돌 때 드러난다.

이와 따로 강·약은 서로 다른 타입을 섞을 때 언어가 알아서 바꿔 주는 정도다. Python에서 "1" + 1은 TypeError지만 JavaScript는 "11"을 돌려준다. 둘 다 동적 타입이지만 Python 쪽이 더 강하다. 정적·동적이 "언제"라면 강·약은 "얼마나 엄격하게"다.

정적과 동적, 강한 타입과 약한 타입으로 나눈 네 칸에 언어를 놓은 그림정적 (컴파일 때 검사)동적 (실행할 때 검사)강함약함Java · Kotlin · C#Go · Rust · Swift컴파일 때 잡고, 섞으면 오류Python실행할 때 잡고, 섞으면 오류C · C++TypeScript (실행은 JS)컴파일 때 잡지만 암시적 변환 많음JavaScript"1" + 1 → "11" 처럼 알아서 바꿈강·약은 정도의 문제다. 칸의 경계보다 "어느 쪽에 가깝나"로 읽는다
두 축으로 나눈 네 칸. TypeScript는 정적 검사기를 JavaScript 위에 얹은 것이라, 컴파일이 끝나면 타입 정보가 지워지고 실행은 JavaScript 그대로다.
언어검사타입 추론제네릭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, 소유권 네 방식의 대표 언어, 해제 방법, 대가수동C · C++해제free · delete를직접 부른다대가해제 후 사용이중 해제 · 누수참조 카운트Swift · CPython해제가리키는 수가0이 되면 즉시대가순환 참조는따로 끊어야추적 GCJava · Go · C# · JS해제루트에서 닿지않으면 회수대가회수 시점 모름멈춤 · 여유 메모리소유권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를 오래 쓰면 모두가 멈춘다.
자바스크립트 코드 다섯 줄과 마이크로태스크 큐, 매크로태스크 큐, 출력이 단계마다 바뀌는 그림스크립트console.log('A')setTimeout(T1, 0) // D, M2 걸기promise.then(M1) // CsetTimeout(T2, 0) // Fconsole.log('B')지금 실행마이크로태스크 큐 (프라미스)매크로태스크 큐 (타이머)출력스크립트 (스크립트)M1T1T2ABM1 (마이크로태스크)T1T2ABCT1 (매크로태스크)M2T2ABCDM2 (마이크로태스크)T2ABCDET2 (매크로태스크)ABCDEF
1 / 6

스크립트 자체도 하나의 작업이다. 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의 큐에서 일을 가져온다.

P0과 P1이 각자 로컬 큐에서 고루틴을 꺼내 돌리고, 입출력으로 막힌 고루틴과 일 훔치기가 틱마다 바뀌는 그림P0 ← M0 (OS 스레드)실행 중로컬 큐P1 ← M1 (OS 스레드)실행 중로컬 큐전역 큐입출력 대기끝남쉼쉼G1G2G3G4G5시작: P1의 큐에만 일이 몰려 있다틱 0G1G2G3G4G5G1G1 입출력 대기, P0를 내놓음틱 1G4G2G5G3G1G2P0가 P1에서 G4·G5 훔침틱 2G4G3G5G1G2 · G4틱 3G5G3G1G2 · G4 · G3G1 깨어나 전역 큐로틱 4G5G1G2 · G4 · G3 · G5틱 5쉼G1G2 · G4 · G3 · G5 · G1
1 / 7

G1~G5 다섯 고루틴, P 둘. P마다 OS 스레드(M)가 하나씩 붙어 실제로 돈다. P1에 일이 몰려 있다.

고루틴 스케줄링(G·M·P)의 개념 모형. 실제 Go 런타임은 시간 조각 선점, 시스템 콜 중인 M과 P의 분리, 네트워크 폴러 등이 더 있다.

GIL(전역 인터프리터 락)은 CPython에서 한 번에 한 스레드만 바이트코드를 실행하게 하는 락이다. 딕셔너리 같은 내장 객체가 따로 락 없이도 안전해 구현이 단순해지는 대신, CPU만 쓰는 스레드를 늘려도 코어를 나눠 쓰지 못한다. 입출력을 기다릴 때는 GIL을 놓으므로 입출력이 많은 프로그램에서는 스레드가 여전히 쓸모 있다. 그래서 Python은 CPU 병렬이 필요하면 프로세스를 여러 개 띄우거나(multiprocessing), GIL을 놓고 계산하는 C 확장(NumPy 등)에 맡겨 왔다.

CPU 작업 네 개가 GIL 아래에서는 한 줄로 이어져 8초, GIL 없이는 네 줄로 나란히 2초가 걸리는 막대 그림GIL 있는 CPython스레드 4개작업 1작업 2작업 3작업 48초free-threaded 빌드또는 프로세스 4개작업 1작업 2작업 3작업 42초012345678초 (코어 4개 기준)
CPU만 쓰는 2초짜리 작업 넷. GIL이 있으면 스레드가 넷이어도 차례로 돌고, GIL이 없는 빌드나 프로세스 넷은 코어 넷에 나란히 퍼진다. 시간은 도움 함수로 계산한 이상적인 값이다.

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면 끝난다. 네 언어 모두 이 "기다림 겹치기"를 하지만 표현이 다르다.

응답 시간이 다른 요청 네 개를 하나씩 보내면 700밀리초, 동시에 보내면 300밀리초가 걸리는 막대 그림하나씩12030080200700 ms동시에12030080200300 ms0100200300400500600700밀리초
요청 넷을 하나씩 보낼 때와 동시에 보낼 때. 기다리는 시간이 겹치므로 전체 시간은 합이 아니라 가장 느린 요청으로 정해진다.

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))
}
세 층 호출에서 예외는 맨 아래에서 맨 위 catch로 바로 날아가고, 값 반환은 층마다 오류를 확인해 위로 넘기는 그림예외 (Java · Python · JS · C#)handler()service()repository()값 반환 (Go error · Rust Result)handler()service()repository()try … catchthrow중간 층은 오류가 지나가는 줄 모른다return errif err != nil · ?if err != nil · ?층마다 오류가 반환값에 보인다
같은 세 층 호출에서 오류가 올라가는 길. 예외는 가운데 층을 건너뛰고, 값 반환은 층마다 손에 쥐었다가 넘긴다.

어느 쪽이든 원칙은 같다. 오류는 처리할 수 있는 층에서 처리하고, 위로 넘길 때는 맥락(어떤 id로 무엇을 하다가)을 덧붙이며, 삼키지 않는다. API가 오류를 바깥에 알리는 형식은 API 설계 노트의 오류 응답 절에 있다.

패키지와 빌드 도구

생태계의 뼈대는 비슷하다. 프로젝트가 원하는 라이브러리와 버전 범위를 선언 파일에 적고, 도구가 의존성의 의존성까지 풀어 버전을 정한 뒤, 그 결과를 잠금 파일에 정확한 버전과 해시로 남기고, 중앙 저장소에서 내려받는다. 잠금 파일을 버전 관리에 함께 올려야 오늘 내 노트북과 다음 달 CI가 같은 것을 받는다.

선언, 해석, 잠금, 내려받기 네 단계와 Java, Go, Python, JavaScript가 각 단계에 쓰는 파일과 저장소선언: 원하는 범위해석기가 버전 고름잠금: 정확한 버전저장소에서 받기생태계선언 파일잠금 · 검증저장소Javapom.xml · build.gradleGradle 잠금(선택)Maven CentralGogo.modgo.sum (체크섬)모듈 프록시Pythonpyproject.tomluv.lockPyPIJS · TSpackage.jsonpackage-lock.jsonnpm 레지스트리잠금 파일을 저장소에 함께 올리면 누가 언제 받아도 같은 의존성 트리가 된다
선언 → 해석 → 잠금 → 내려받기. Go는 잠금 파일 대신 go.mod에 적힌 최소 버전을 그대로 쓰고, go.sum에 체크섬을 남겨 받은 내용이 바뀌지 않았는지 확인한다.
  • 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 언어는 회수를 미루는 만큼 여유 메모리를 더 쓴다. 컨테이너 메모리 한도를 정할 때 이 여유를 셈에 넣어야 한다.
시간에 따라 처리 속도가 변하는 개념도. 네이티브는 처음부터 일정하게 빠르고, JIT은 처음에 느리다가 데워지면 비슷해지며, 해석 위주는 낮게 일정하다빠름느림실행한 시간 →데우는 구간(워밍업)AOT 네이티브 (Go · Rust · C++)JIT (Java · C# · JS의 V8)해석 위주 (CPython)
개념도(수치 없음). 시간이 지나며 처리 속도가 어떻게 바뀌는지의 모양만 그렸다. 실제 곡선은 프로그램과 런타임 설정에 따라 다르다.

대부분의 웹 서비스에서 응답 시간을 정하는 것은 언어보다 데이터베이스 쿼리, 네트워크 왕복, 캐시 적중률이다. 언어를 바꾸기 전에 먼저 프로파일러로 시간이 어디에 쓰이는지 재 보는 것이 순서다(성능 노트).

쓰임새 지도

분야마다 사실상의 기본 언어가 있다. 대개 기술적 우열보다 그 분야의 핵심 라이브러리·플랫폼이 어느 언어로 먼저 나왔느냐가 정했다.

웹 프런트엔드, 백엔드, 데이터와 ML, 시스템, 모바일, 인프라 도구, 게임 분야별로 자주 쓰는 언어를 적은 지도웹 프런트엔드JavaScript · TypeScriptWebAssembly로 다른 언어도백엔드 서버Java · Kotlin · Go · C#Python · Node.js(TS)데이터 · MLPython · SQLScala · Java(Spark) · R시스템 · 임베디드C · C++ · Rust모바일Kotlin (Android) · Swift (iOS)Dart · React Native(TS)인프라 · CLI 도구Go · RustPython · 셸게임C++ · C#엔진의 스크립트 언어
분야별로 자주 쓰는 언어. 색칠한 칸이 그 분야의 기본값이고, 오른쪽은 함께 자주 보이는 언어다. 경계는 늘 움직인다.
  • 웹 프런트엔드는 브라우저가 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를 쓰는 사람에 치우친다), 질문마다 답한 사람 수가 다르다.

TIOBE 지수와 Stack Overflow 개발자 설문의 상위 다섯 언어를 나란히 놓은 막대 그림. 두 목록에 함께 든 언어는 Python뿐이다TIOBE 지수 2026년 10월% (검색 결과 비중)1. Python19.22. C11.33. C++8.94. Java8.45. C#3.8Stack Overflow 설문 2026% (응답자 중 쓴 사람)1. JavaScript62.02. SQL58.43. HTML/CSS58.24. Python58.05. Bash/Shell51.3막대 길이는 목록마다 1위를 기준으로 맞췄다. 두 목록의 %는 서로 다른 것을 센다
같은 해 두 지표의 상위 다섯. 겹치는 언어는 Python 하나다. TIOBE는 검색 결과로 본 언어의 '존재감', 설문은 응답자가 '쓴다고 답한 비율'이라 단위부터 다르다.

두 지표 모두 "어느 언어를 배우면 일자리가 있나"나 "우리 팀에 무엇이 맞나"에는 간접적인 단서일 뿐이다. SQL·HTML·셸이 설문 위쪽에 있는 것처럼, 많이 쓰인다는 것은 다른 언어와 함께 곁들여 쓴다는 뜻이기도 하다. 순위가 한두 칸 오르내리는 것은 측정 방식의 잡음일 때가 많다.

언어 고르는 법

언어는 맨 앞의 강한 제약부터 걸러 내면 후보가 금방 좁혀진다. 실행 환경이 언어를 정하는 경우(브라우저, 모바일 OS), 런타임이나 GC를 둘 수 없는 경우(커널, 마이크로컨트롤러, 실시간 제어), 분야의 핵심 라이브러리가 한 언어에 몰린 경우(ML의 Python)다. 이 단계를 지나면 남는 것은 대개 "서버나 도구를 무엇으로 짤까"이고, 여기서는 여러 언어가 다 충분히 좋다.

실행 환경, 성능 제약, 분야, 팀의 기존 언어 순서로 묻고 예라고 답한 곳의 언어를 고르는 흐름도브라우저 안에서 도나?예TypeScript (JavaScript)아니오안드로이드·iOS 앱인가?예Kotlin · Swift아니오GC 멈춤이나 런타임을 못 두나?예Rust · C · C++아니오데이터 분석 · ML이 중심인가?예Python아니오이미 쓰는 언어 · 라이브러리가 있나?예그 언어를 계속아니오서버 · 도구라면 Java · Kotlin · Go · C# · TypeScript · 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 ThreadsJDK 제안서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