언어 & 런타임 | Language & Runtime
내가 볼려고 작성한 CS 공부.
⚙️ 언어 & 런타임
✅ 1. 컴파일러 vs 인터프리터
1.1 프로그램은 바로 실행되지 않는다?
우리가 쓰는 Python, Java, C 같은 프로그래밍 언어는 사람이 이해하기 쉬운 언어다.
하지만 컴퓨터는 오직 0과 1 (기계어) 만 이해할 수 있어.
→ 그래서 반드시 “사람이 쓴 코드 → 기계가 이해하는 코드”로 변환하는 과정이 필요함!
이때 등장하는 게 바로:
- 컴파일러(Compiler)
- 인터프리터(Interpreter)
1.2 컴파일러란?
컴파일러는 전체 소스 코드를 한 번에 기계어로 번역한 다음,
그 결과물(실행 파일)을 실행하는 방식이다.
예: C 언어 실행 흐름
- 사람이 작성한
.c파일 - 컴파일러가
.exe또는.out실행 파일로 번역 - 이 실행 파일은 바로 컴퓨터에서 실행 가능
💡 컴파일 과정에서 문법 오류가 있으면 실행조차 안 됨
1.3 인터프리터란?
인터프리터는 소스 코드를 한 줄씩 읽고, 즉석에서 실행하는 방식이다.
“번역과 실행을 동시에 한다”고 생각하면 돼.
예: Python 실행 흐름
print("Hello")입력- 인터프리터가 해당 줄을 즉시 해석하고 실행
- 다음 줄로 넘어감
💡 문법 오류가 있어도, 해당 줄에 도달하기 전까지는 실행됨
1.4 컴파일러 vs 인터프리터 비교
| 항목 | 컴파일러 | 인터프리터 |
|---|---|---|
| 동작 방식 | 전체 소스 → 기계어로 컴파일 후 실행 | 한 줄씩 즉석 해석 & 실행 |
| 실행 속도 | 빠름 (이미 기계어 상태) | 느림 (실시간 해석) |
| 오류 처리 | 모든 오류를 컴파일 시점에 체크 | 실행 중 해당 줄에서만 발생 |
| 디버깅 | 어려움 (전체 다시 컴파일해야 함) | 쉬움 (즉시 수정 가능) |
| 예시 언어 | C, C++ | Python, JavaScript |
1.5 실제 언어들은 섞어서 사용한다?
현실에서는 많은 언어가 컴파일 + 인터프리터 혼합 방식을 사용한다.
예시 1: Java
.java소스 코드를 컴파일해서.class바이트코드로 변환- 실행 시 JVM이 그걸 인터프리팅하거나 JIT 컴파일
Java는 “컴파일 언어지만 인터프리터적인 런타임을 가짐”
예시 2: Python
- 보통은 인터프리터 기반이지만, 일부 최적화된 Python(JIT 기반 PyPy 등)은 컴파일도 함
PyPy란?
- PyPy는 Python 언어를 빠르게 실행하기 위한 인터프리터.
- Python으로 만든 프로그램을 더 빠르게 실행할 수 있게 해줌.
- 보통 사용하는 Python은 CPython.
- PyPy는 Just-In-Time (JIT) 컴파일러를 사용해서 성능을 높임.
CPython vs PyPy | 항목 | CPython (기본) | PyPy (대체 구현체) | |————-|—————————–|——————————| | 속도 | 보통 | 더 빠름 (특히 반복 작업) | | 구현 언어 | C 언어 | RPython (Python의 서브셋) | | JIT 지원 | ❌ 없음 | ✅ 있음 | | 호환성 | 거의 모든 라이브러리 OK | 일부 C 확장 호환 어려움도 | | 설치 |
python| 별도 설치 (pypy) 필요 |
언제 PyPy 쓰는가?
- 반복이 많은 연산 (루프-heavy) 코드 → 속도 이득 큼
- 순수 Python으로 짠 코드 → 최적화 잘 됨
- 성능 병목 있는 코드 → PyPy로 실험해보면 좋음
그외 파이썬 인터프리터
- Jython, IronPython, MicroPython
1.6 언제 컴파일러? 언제 인터프리터?
| 조건 | 적합한 방식 | 설명 |
|---|---|---|
| 실행 속도 최우선 | 컴파일러 | 이미 기계어이므로 빠름 |
| 빠른 개발 & 테스트 | 인터프리터 | 즉시 실행, 빠른 피드백 |
| 코드가 복잡하고 안정성 중요 | 컴파일러 | 전체 오류 사전 체크 |
| 간단한 스크립트나 자동화 | 인터프리터 | 실행 환경만 있으면 끝 |
1.7 그림으로 정리하면?
컴파일러 방식 (C)
1
[코드 작성] → [컴파일러] → [실행 파일 생성] → 실행
인터프리터 방식 (Python)
1
[코드 작성] → [인터프리터] → 즉시 실행
1.8 왜 중요한가?
언어가 컴파일러 기반인지, 인터프리터 기반인지에 따라:
- 실행 속도
- 오류 처리 방식
- 배포 방식
- 디버깅 편의성
이 전부 달라지기 때문에,
개발 도구나 서비스 선택 시에도 큰 영향을 준다.
✅ 2. 스택 & 힙 메모리 구조
2.1 프로그램은 실행될 때 메모리를 나눠 쓴다
코드를 실행하면, 운영체제는 메모리(RAM) 공간을 여러 영역으로 나눈다.
주요 메모리 영역
| 영역 | 설명 |
|---|---|
| 코드(Code) | 실행될 명령어 저장 |
| 데이터(Data) | 전역 변수, 정적 변수 저장 |
| 스택(Stack) | 함수 호출 정보, 지역 변수 저장 |
| 힙(Heap) | 동적으로 생성된 객체 저장 |
2.2 스택(Stack)이란?
함수 실행 정보와 지역 변수를 저장하는 영역
후입선출(LIFO) 구조이며, 함수가 호출될 때마다 스택에 쌓이고, 종료되면 빠진다.
예시
1
2
def func():
a = 10 # 스택에 저장 func()
a는 함수 안에서만 유효- 함수 종료 시 스택에서 자동 제거됨
💡 스택은 매우 빠르지만, 메모리 크기가 작고 자동 소멸함
2.3 힙(Heap)이란?
동적 메모리 할당(필요할 때 수동 생성) 을 위한 영역
객체나 리스트 같은 데이터들이 저장된다.
예시
1
arr = [1, 2, 3] # 힙에 저장됨
arr는 힙에 저장되며, 함수 종료돼도 메모리 해제되지 않음- 프로그래머가 직접 관리하거나, 가비지 컬렉터(GC)가 처리
💡 힙은 크고 유연하지만, 접근 속도는 스택보다 느림
2.4 스택 vs 힙 비교
| 항목 | 스택(Stack) | 힙(Heap) |
|---|---|---|
| 할당 시점 | 함수 호출 시 자동 | 명시적 할당 (new 등) |
| 저장 대상 | 지역 변수, 호출 정보 | 객체, 배열 등 |
| 해제 방식 | 자동 | 수동 또는 GC |
| 속도 | 매우 빠름 | 상대적으로 느림 |
| 오류 사례 | Stack Overflow | Memory Leak |
2.5 자주 발생하는 오류
Stack Overflow
함수를 무한히 재귀 호출하는 경우
1 2 def foo(): foo() # 무한 재귀 foo()→ 호출 정보가 계속 쌓여 스택이 꽉 차며 오류 발생
Memory Leak
힙에 저장된 데이터를 더 이상 사용하지 않는데도
참조가 끊기지 않아 GC가 제거하지 못하는 상황
- 전역 변수로 계속 참조 유지
- 이벤트 리스너 해제 안 함
- 캐시에 쌓이고 제거되지 않음
2.6 왜 중요할까?
이 구조를 이해해야…
- 함수 호출 원리 (재귀가 왜 위험한지)
- 객체가 언제까지 메모리에 남는지
- 메모리 최적화가 왜 필요한지
를 알 수 있다.
특히, C/C++처럼 메모리를 직접 다루는 언어에서는
스택/힙 구조를 모르면 버그와 보안 취약점이 쉽게 생긴다!
✅ 3. 가비지 컬렉션 (Garbage Collection)
3.1 가비지란?
가비지(Garbage)란 더 이상 사용하지 않는 객체
즉, 프로그램 안에서 누구도 참조하지 않는 메모리 조각
예시
1
2
a = [1, 2, 3]
a = None # 원래 리스트는 더 이상 참조되지 않음 → 가비지
변수 a는 [1, 2, 3]이라는 리스트 객체를 참조(reference)했지만, 이제 None을 참조.
[1, 2, 3]이라는 리스트는 아무 변수도 참조하지 않게 되고, 그러면 그 리스트 객체는 쓸모없어진 데이터가 됨.
이런 데이터를 가비지(garbage) 라고 함
3.2 가비지 컬렉션이 필요한 이유
| 문제 상황 | 설명 |
|---|---|
| 메모리 누수 (Memory Leak) | 쓸모없는 데이터가 계속 남아있으면 메모리 낭비 |
| Dangling Pointer | 너무 일찍 메모리 해제 시 다른 곳에서 오류 발생 |
→ GC는 이런 위험을 줄이고, 프로그래머의 부담을 줄여줌
3.3 기본 방식 – Mark & Sweep
가장 오래된 GC 알고리즘: “표시하고 지운다”
단계
- Mark 단계: 더 이상 접근할 수 없는 객체를 탐색해서 표시
- Sweep 단계: 표시된 객체를 제거하여 메모리 회수
→ 루트 객체에서 연결된 것만 남기고 나머지는 제거
3.4 Generational GC (세대별 가비지 컬렉션)
대부분의 GC는 객체의 “나이”에 따라 관리 방식을 달리함
| 세대 | 설명 |
|---|---|
| Young Generation | 금방 생성된 객체 (많이 죽음) |
| Old Generation | 오래 살아남은 객체 (GC 적게 발생) |
→ 자주 죽는 객체는 빨리 제거하고, 오래된 객체는 GC를 덜 자주 수행해 성능 최적화
3.5 Stop-the-world란?
GC를 실행하는 동안 프로그램 전체가 멈추는 현상
- 메모리 수집이 끝나야 다음 명령 실행 가능
- 사용자 입장에선 버벅이거나 멈춘 것처럼 느껴짐
→ GC의 효율이 중요해지는 이유
3.6 참조 카운팅 방식 (Python 등)
객체가 몇 개의 변수에서 참조되고 있는지를 기준으로 판단
예시
1
2
3
4
a = [1, 2]
b = a # 참조 수 = 2
a = None # 참조 수 = 1
b = None # 참조 수 = 0 → 제거됨
단점: 순환 참조에서는 카운트가 0이 되지 않아 누수 발생
→ Python은 이를 보완하기 위해 주기적으로 순환 탐지도 수행함
- 순환 참조: 객체들이 서로 참조 (a = b, b = a)
- 순환 탐지: 순환 참조를 찾아 자동 탐지 기능
3.7 언어별 GC 요약
| 언어 | GC 특징 |
|---|---|
| Java | Mark & Sweep + Generational GC + 튜닝 가능 |
| Python | 참조 카운트 + 순환 탐지 |
| JavaScript | 최신 브라우저는 Mark & Sweep 방식 |
| Go | 자동 GC + Pause 최소화 |
| C / C++ | 없음 (개발자가 직접 해제해야 함) |
3.8 GC의 한계 – 만능은 아니다
자동이라고 해도 무조건 완벽하지 않음
개발자가 여전히 메모리 구조와 참조 관계에 주의해야 한다.
메모리 누수 예시
- 전역 변수로 객체를 참조
- 클로저 안에 불필요한 데이터가 남아있음
- 이벤트 리스너를 제거하지 않음
- 캐시된 객체를 비우지 않음
3.9 요약
- GC는 힙에 존재하는 불필요한 객체를 자동으로 제거해주는 시스템
- 가장 기본은 Mark & Sweep
- 대부분의 언어는 Generational GC를 채택해 성능을 개선
- 개발자가 GC가 있어도 메모리 구조를 잘 설계해야 함
✅ 4. JIT 컴파일 & 바이트코드
4.1 바이트코드란?
바이트코드는 사람이 읽기 어려운 중간 단계 코드로,
운영체제나 CPU에 독립적인 형태다.
왜 쓰는가?
- 바로 기계어로 번역하면 OS/CPU마다 다르게 만들어야 함
- 바이트코드는 하나만 만들고, 그걸 해석해주는 런타임(JVM, Python VM 등)이 실행함
예시
- Java →
.class(바이트코드) → JVM이 실행 - Python →
.pyc(바이트코드) → CPython이 실행
4.2 인터프리트 vs 바이트코드 기반 언어
| 방식 | 설명 |
|---|---|
| 인터프리트 | 코드 줄마다 해석 (Python, JavaScript 등) |
| 바이트코드 기반 | 컴파일 후 중간 코드(JVM, CLR 등)로 실행 (Java, Kotlin, C# 등) |
바이트코드는 컴파일된 상태지만 기계어는 아님 → “중간단계” 코드
4.3 JIT 컴파일(Just-In-Time)이란?
JIT 컴파일은 프로그램 실행 중, 바이트코드를 기계어로 실시간 변환하는 기술이야.
흐름
- 처음에는 바이트코드를 인터프리팅
- 자주 실행되는 코드를 감지
- 그 부분만 기계어로 컴파일해서 캐싱
- 이후엔 빠르게 실행 가능
즉, 실행 중 최적화하는 똑똑한 컴파일러
4.4 JIT의 장점
| 항목 | 설명 |
|---|---|
| 실행 속도 향상 | 자주 쓰는 부분만 최적화 → 성능 UP |
| 런타임 정보 활용 | 실제 데이터 패턴에 맞춰 더 효율적인 기계어 생성 |
| 크로스 플랫폼 | 바이트코드는 동일, JIT은 OS별로 번역 |
JIT이 성능을 대폭 향상시켜서, Java가 인터프리터 언어처럼 보이지만 실제론 매우 빠른 이유
4.5 AOT vs JIT vs 인터프리터
| 구분 | 설명 | 예시 |
|---|---|---|
| AOT (Ahead-of-Time) | 컴파일 타임에 기계어로 번역 | C, C++, Rust |
| JIT (Just-in-Time) | 실행 중에 기계어로 변환 | Java, .NET |
| 인터프리터 | 줄마다 해석해서 실행 | Python, JavaScript |
4.6 JIT이 없는 언어에서도 최적화는 존재한다?
Python의 표준 구현인 CPython은 JIT이 없지만,
PyPy는 JIT을 도입해 4~10배 빠르기도 함
JavaScript도 V8 엔진이 내부적으로 JIT을 활용해 웹 브라우저 속도를 혁신적으로 개선함
4.7 JIT의 단점
| 단점 | 설명 |
|---|---|
| 초기 실행 속도 느림 | 실행 중 컴파일하느라 딜레이 생김 |
| 메모리 사용 ↑ | 컴파일된 코드 캐싱 필요 |
| 예측 어려움 | 어떤 부분이 최적화될지 상황마다 다름 |
그래서 JIT은 항상 빠른 게 아니라, 시간이 지날수록 빨라지는 구조
4.8 예시로 흐름 비교
Java 실행 구조
1
2
3
4
5
[.java 소스]
↓ 컴파일
[.class 바이트코드]
↓ JVM + JIT
[기계어 실행]
C 실행 구조 (AOT)
1
2
3
4
[.c 소스]
↓ 컴파일
[실행 파일]
→ 바로 기계어 실행
✅ 5. 정적 바인딩 vs 동적 바인딩
5.1 바인딩(Binding)이란?
바인딩은 어떤 이름(함수, 변수 등) 이 실제 메모리 위치나 코드와 연결되는 시점을 말한다.
- 변수 → 메모리 공간에 연결
- 함수 호출 → 어떤 코드가 실행될지 연결
→ 이 바인딩이 “언제” 일어나는지에 따라 정적 / 동적으로 나뉜다.
5.2 정적 바인딩 (Static Binding)
바인딩이 컴파일 타임에 미리 결정되는 방식이다.
특징
- 어떤 함수/변수가 실행될지 컴파일할 때 결정됨
- 빠르다 (미리 정해졌으므로 실행 중 고민할 게 없음)
- C, C++ 등의 기본 방식
예시 (C)
1
2
3
int add(int a, int b) { return a + b; }
int main() { int result = add(3, 4); // add 함수는 컴파일 타임에 연결됨 }
5.3 동적 바인딩 (Dynamic Binding)
바인딩이 실행 중(Run-Time)에 결정되는 방식이다.
특징
- 실행 시점에 실제 어떤 함수가 호출될지 결정됨
- 느리지만 유연함 (다형성, 오버라이딩 등 지원)
- Java, Python, JavaScript 등에서 자주 사용
예시 (Java)
1
2
3
4
5
class Animal { void sound() { System.out.println("??"); } }
class Dog extends Animal { void sound() { System.out.println("멍멍"); } }
Animal a = new Dog(); a.sound(); // 실행 중에 Dog의 sound()가 호출됨
5.4 비교 표
| 항목 | 정적 바인딩 | 동적 바인딩 |
|---|---|---|
| 시점 | 컴파일 타임 | 실행 타임 |
| 속도 | 빠름 | 느림 |
| 유연성 | 낮음 | 높음 (다형성, 오버라이딩 등) |
| 예시 언어 | C, C++ (기본) | Java, Python, JavaScript 등 |
| 대표 예 | 일반 함수 호출 | 가상 함수, 오버라이딩 메서드 |
5.5 동적 바인딩이 필요한 이유
객체 지향 언어에서 “다형성(polymorphism)” 을 구현하려면
동적 바인딩이 반드시 필요하다.
예: Java에서의 오버라이딩
1
Animal a = new Cat(); a.sound(); // Cat의 sound() 실행됨
→ 컴파일 시에는 Animal 타입만 보고 컴파일하지만,
→ 실행 시점에서 진짜 객체(Cat)를 확인해서 바인딩함
5.6 언어별 기본 방식
| 언어 | 바인딩 방식 | 설명 |
|---|---|---|
| C | 정적 | 빠르고 예측 가능, 유연성은 낮음 |
| C++ | 기본 정적 + virtual 키워드로 동적 가능 | |
| Java | 기본 동적 (클래스 메서드는 대부분 동적) | |
| Python | 기본 동적 (모든 속성과 메서드 호출이 런타임 결정) |
5.7 정리하면?
- 정적 바인딩 → 빠르고 단순, 성능 위주
- 동적 바인딩 → 유연하고 확장성 ↑, 하지만 약간 느림
- 객체지향의 핵심인 다형성(Polymorphism)은 동적 바인딩으로 구현된다!
✅ 6. 주요 언어의 런타임 특성
6.1 Java – JVM 기반의 정적 타입 언어
Java는 코드를 바이트코드로 컴파일한 뒤,
JVM(Java Virtual Machine) 이 실행하는 구조
특징
| 항목 | 설명 |
|---|---|
| 실행 구조 | 소스 → 바이트코드(.class) → JVM이 실행 |
| 메모리 관리 | 자동 (GC) |
| 실행 최적화 | JIT 컴파일 지원 |
| 타입 | 정적 타입 (컴파일 시 타입 검사) |
| 병행성 | 스레드 기반 병렬 처리, 동기화 키워드 지원 |
JVM 덕분에 운영체제에 독립적이며, 대규모 서비스에 적합
6.2 Python – 인터프리터 기반 동적 타입 언어
Python은 코드를 한 줄씩 인터프리팅하며 실행되는 구조
특징
| 항목 | 설명 |
|---|---|
| 실행 구조 | 소스(.py) → 바이트코드(.pyc) → CPython이 해석 |
| 메모리 관리 | 참조 카운팅 + GC |
| 실행 속도 | 상대적으로 느림 (JIT 없음) |
| 타입 | 동적 타입 (실행 중 타입 결정) |
| 병행성 | GIL(Global Interpreter Lock) 존재 → 병렬 처리 제약 |
생산성이 매우 높고 문법이 쉬워서 AI/데이터 분야에서 널리 사용됨
6.3 C – AOT 기반 고성능 시스템 언어
C는 컴파일 시 기계어로 번역된 실행파일을 직접 실행
특징
| 항목 | 설명 |
|---|---|
| 실행 구조 | 소스(.c) → 컴파일러 → 실행파일(.exe) |
| 메모리 관리 | 수동 (malloc/free) |
| 실행 속도 | 매우 빠름 (JIT, 인터프리트 없음) |
| 타입 | 정적 타입 |
| 병행성 | 저수준 스레드 API 직접 사용 |
매우 빠르지만 버그 발생 시 위험 (메모리 해제 누락, 포인터 오류 등)
6.4 JavaScript – 이벤트 기반 인터프리터 언어
JavaScript는 웹 브라우저 또는 Node.js에서
V8 엔진 같은 인터프리터 + JIT 기반 엔진으로 실행됨
특징
| 항목 | 설명 |
|---|---|
| 실행 구조 | 소스 → 인터프리팅 + JIT 최적화 |
| 메모리 관리 | 자동 (GC) |
| 타입 | 동적 타입 |
| 병행성 | 싱글 스레드 + 이벤트 루프 구조 |
| 비동기 처리 | Promise, async/await |
브라우저뿐만 아니라 서버(Node.js)에서도 널리 사용
6.5 언어별 런타임 특성 비교 요약
| 언어 | 실행 방식 | 타입 | 메모리 관리 | 병행성 |
|---|---|---|---|---|
| Java | JVM + JIT | 정적 | GC (자동) | 스레드 |
| Python | 인터프리트 | 동적 | 참조 카운트 + GC | GIL로 병렬 제약 |
| C | AOT 컴파일 | 정적 | 수동 (malloc/free) | 스레드 직접 구현 |
| JavaScript | 인터프리트 + JIT | 동적 | GC (자동) | 이벤트 루프 기반 |
6.6 요약 포인트
- Java: 안정성과 성능 균형, 대형 서비스에 적합
- Python: 쉬운 문법, 빠른 개발, 하지만 실행 속도는 느림
- C: 최상위 성능, 위험도 큼, 시스템 프로그래밍용
- JavaScript: 웹 친화적, 비동기 처리 강점
언어마다 런타임 구조가 달라서
성능 최적화, 디버깅 방식, 메모리 이슈 대응 방식도 다르다!