diff --git "a/01\354\243\274\354\260\250/README.md" "b/01\354\243\274\354\260\250/README.md" index 2a76e53..a9a9788 100644 --- "a/01\354\243\274\354\260\250/README.md" +++ "b/01\354\243\274\354\260\250/README.md" @@ -1,21 +1,483 @@ # 1주차 - 자료구조 / 시간복잡도 ## 공통 키워드 -- Big-O -- Array -- LinkedList -- Stack -- Queue -- Hash Table -- Tree -- Heap + +### Big-O + +입력값의 크기 n이 커질 때 알고리즘의 실행 시간이나 메모리 사용량이 얼마나 증가하는지 나타내는 수학적 방법이다. +즉, 알고리즘의 효율성(시간 복잡도와 공간 복잡도)을 표현할 때 사용한다. + +| 표기 | 의미 | 예시 | +| --- | --- | --- | +| O(1) | 입력 크기와 상관없이 일정한 시간 | 배열에서 특정 인덱스 값 찾기 | +| O(log n) | 입력이 커져도 매우 천천히 증가 | 이진 탐색 | +| O(n) | 입력 크기에 비례하여 증가 | 배열 전체 탐색 | +| O(n log n) | n보다 조금 더 빠르게 증가 | 효율적인 정렬 알고리즘 | +| O(n²) | 입력 크기의 제곱만큼 증가 | 이중 반복문 | + +**시간 복잡도**: 알고리즘이 입력값의 크기 n에 따라 실행 시간이 얼마나 증가하는지 나타낸다. +→ 알고리즘의 실행 속도와 효율성을 판단할 수 있다. + +**공간 복잡도**: 알고리즘이 입력값의 크기 n에 따라 사용하는 메모리 공간이 얼마나 증가하는지 나타낸다. +→ 알고리즘이 실행되는 동안 필요한 메모리 사용량을 판단할 수 있다. + +### Array + +같은 자료형의 데이터를 연속된 메모리 공간에 저장하고, **인덱스(index)** 를 통해 데이터에 접근하는 자료구조이다. + +**인덱스(index)**: 배열에서 각 데이터의 위치를 나타내는 번호이다. 일반적으로 0부터 시작한다. + +```text +[10, 20, 30, 40, 50] + 0 1 2 3 4 + ↑ ↑ ↑ ↑ ↑ + 인덱스(index) + +arr[0] → 10 +arr[2] → 30 +arr[4] → 50 +``` + +**특징** + +- 인덱스를 통한 조회: **O(1)** +- 전체 탐색: **O(n)** +- 중간 삽입/삭제: **O(n)** → 뒤의 데이터를 이동해야 함 +- 빠른 조회에 유리하지만, 중간 삽입·삭제에는 불리하다. + +### LinkedList + +데이터를 **노드(Node)** 단위로 저장하고, 각 노드가 다음 노드의 위치를 가리키며 연결된 자료구조이다. + +**노드(Node)**: 데이터를 저장하는 공간과 다음 노드를 가리키는 정보(링크)로 구성된다. + +```text +[10 | ●] → [20 | ●] → [30 | ●] → NULL + Node 1 Node 2 Node 3 +``` + +**특징** + +- 인덱스를 이용해 바로 접근하지 않고, 앞에서부터 노드를 따라가며 데이터에 접근한다. +- 특정 데이터 조회: **O(n)** +- 삽입/삭제: **O(1)** → 위치를 알고 있다면 연결만 변경하면 됨 +- 데이터가 메모리에 연속적으로 저장될 필요가 없다. +- 삽입·삭제에 유리하지만, 특정 데이터 조회에는 불리하다. + +### Stack + +데이터를 한쪽 끝에서만 넣고 빼는 자료구조이다. + +**LIFO (Last In, First Out)** +→ 마지막에 들어온 데이터가 가장 먼저 나가는 방식이다. + +```text +[30] ← Top +[20] +[10] +``` + +10 → 20 → 30 순서로 데이터를 넣으면 +30 → 20 → 10 순서로 데이터가 나온다. + +**주요 연산** + +- `push`: 데이터 추가 +- `pop`: 데이터 제거 +- `peek`: 데이터를 제거하지 않고 가장 위의 데이터 확인 + +**시간 복잡도** + +- push: **O(1)** +- pop: **O(1)** +- peek: **O(1)** + +**특징** + +- 가장 마지막에 들어온 데이터부터 처리한다. +- 실행 취소(Undo), 함수 호출 관리, 괄호 검사 등에 활용된다. + +### Queue + +데이터를 한쪽에서는 삽입하고, 반대쪽에서는 삭제하는 자료구조이다. +먼저 들어온 데이터가 먼저 나가는 **FIFO(First In, First Out)** 구조를 가진다. + +- **Front**: 데이터를 꺼내는 쪽, 가장 먼저 나갈 데이터가 있는 위치 +- **Rear**: 데이터를 넣는 쪽, 가장 최근에 들어온 데이터가 있는 위치 + +```text +Rear Front + ↓ ↓ +[30] ← [20] ← [10] ← 데이터가 나가는 방향 + ↑ +새로운 데이터 삽입 +``` + +10 → 20 → 30 순서로 들어왔다면 +10 → 20 → 30 순서로 나간다. + +**주요 연산** + +- `enqueue`: Rear에 데이터 추가 +- `dequeue`: Front에서 데이터 제거 +- `peek`: Front의 데이터를 확인 (제거하지 않음) + +**시간 복잡도** + +- enqueue: **O(1)** +- dequeue: **O(1)** +- peek: **O(1)** + +**특징** + +- 먼저 들어온 데이터부터 처리한다. +- 데이터를 넣는 위치와 빼는 위치가 서로 다르다. +- 대기열, 프린터 작업, 작업 처리, BFS(너비 우선 탐색) 등에 사용된다. + +### Hash Table + +`Key`와 `Value`를 하나의 쌍으로 저장하고, **해시 함수(Hash Function)** 를 이용해 데이터를 빠르게 저장하고 찾는 자료구조이다. + +- **Key**: 데이터를 구분하기 위한 고유한 값 +- **Value**: Key에 연결되어 있는 실제 데이터 +- **해시 함수**: Key를 입력받아 데이터를 저장할 위치(인덱스)를 계산하는 함수 + +```text +Key Hash Function Index Value +"apple" ───────────────→ 2 "사과" +"banana" ───────────────→ 5 "바나나" +``` + +`"apple"`이라는 Key를 이용하면 해시 함수가 저장 위치를 계산하고, 해당 위치에서 Value를 빠르게 찾을 수 있다. + +**충돌(Collision)** + +서로 다른 Key가 같은 위치로 계산되는 현상이다. + +```text +"apple" ──→ 2 +"grape" ──→ 2 ← 충돌! +``` + +충돌이 발생하면 **체이닝(Chaining)** 이나 **개방 주소법(Open Addressing)** 등의 방법으로 처리한다. + +**시간 복잡도** + +- 검색: 평균 **O(1)** / 최악 **O(n)** +- 삽입: 평균 **O(1)** / 최악 **O(n)** +- 삭제: 평균 **O(1)** / 최악 **O(n)** + +**특징** + +- Key를 이용해 데이터를 매우 빠르게 검색할 수 있다. +- 데이터의 순서를 중요하게 다루지 않는다. +- 해시 함수와 충돌 처리 방식에 따라 성능이 달라진다. +- 캐시, 데이터베이스 인덱싱, 중복 검사, 빠른 검색 등에 활용된다. + +### Tree + +데이터를 계층적인 구조로 표현하는 자료구조이다. +노드들이 부모와 자식 관계로 연결되어 있으며, 하나의 노드에서 여러 노드로 뻗어나가는 구조를 가진다. + +#### Tree 탐색 방법 + +트리의 모든 노드를 특정한 순서에 따라 방문하는 방법이다. + +```text + A + / \ + B C + / \ \ + D E F +``` + +**1. 전위 순회 (Preorder)** → **Root → Left → Right** + +```text +A → B → D → E → C → F +``` + +**2. 중위 순회 (Inorder)** → **Left → Root → Right** + +```text +D → B → E → A → C → F +``` + +**3. 후위 순회 (Postorder)** → **Left → Right → Root** + +```text +D → E → B → F → C → A +``` + +**4. 레벨 순회 (Level-order)** → 위에서 아래로, 같은 레벨은 왼쪽부터 오른쪽 순서 + +```text +A → B → C → D → E → F +``` + +#### DFS / BFS + +**DFS (Depth-First Search, 깊이 우선 탐색)** +한 방향으로 최대한 깊게 들어간 후, 더 이상 갈 곳이 없으면 돌아와서 다른 경로를 탐색하는 방법이다. + +- 전위 / 중위 / 후위 순회가 DFS 방식이다. +- 주로 **재귀 또는 Stack**을 이용한다. + +**BFS (Breadth-First Search, 너비 우선 탐색)** +한 레벨을 모두 탐색한 후 다음 레벨로 이동하는 방식이다. + +- 레벨 순회가 BFS 방식이다. +- 주로 **Queue**를 이용한다. + +### Heap ## Backend 심화 (선택) -- Java Collection Framework -- HashMap 내부 구조 -- ConcurrentHashMap - -## Frontend 심화 (선택) -- JS Array/Object/Map/Set -- 배열 연산의 시간복잡도 -- 불변성 + +### Java Collection Framework + +Java에서 데이터를 효율적으로 저장하고 관리할 수 있도록 다양한 자료구조를 인터페이스와 클래스로 제공하는 프레임워크이다. + +**주요 인터페이스** + +**List** → 순서가 있고 중복을 허용 + +- `ArrayList` → 배열 기반으로 인덱스를 통한 조회가 빠르지만, 중간 삽입·삭제 시 데이터 이동이 발생한다. +- `LinkedList` → 노드를 연결한 구조로, 중간 삽입·삭제에 유리하다. + +**Set** → 중복을 허용하지 않음 + +- `HashSet` → 해시를 이용해 데이터를 빠르게 저장하고 검색한다. +- `TreeSet` → 데이터를 정렬된 상태로 저장한다. + +**Queue** → 먼저 들어온 데이터를 먼저 처리하는 구조 + +- `LinkedList` → Queue로 사용할 수 있으며, 데이터를 순서대로 삽입·삭제할 수 있다. +- `PriorityQueue` → 데이터의 우선순위에 따라 높은 우선순위의 데이터부터 꺼낸다. + +**Map** → Key-Value 형태로 데이터를 저장 + +- `HashMap` → Key를 해시하여 데이터를 빠르게 저장하고 조회한다. +- `TreeMap` → Key를 기준으로 데이터를 정렬된 상태로 저장한다. + +> **핵심** +> 앞에서 기록한 자료구조를 Java에서 쉽게 사용할 수 있도록 구현해 놓은 것이 Java Collection Framework이다! + +### HashMap 내부 구조 + +HashMap은 `Key`와 `Value`를 저장할 때 해시값을 이용해 데이터를 저장할 위치를 빠르게 찾는 자료구조이다. + +#### 핵심 내부 구조 + +**1. 버킷 배열 (Bucket Array)** + +데이터를 저장하는 기본 배열이다. +HashMap은 Key의 해시값을 이용해 이 배열의 몇 번째 위치에 저장할지 결정한다. + +```text +[0] [1] [2] [3] [4] [5] ... + ↑ + 데이터 저장 +``` + +**2. Node** + +실제로 데이터를 저장하는 단위이다. + +```text +Node +├── hash → 해시값 +├── key → 키 +├── value → 값 +└── next → 다음 Node +``` + +**3. Hash Function** + +Key의 `hashCode()`를 이용해 데이터를 저장할 Bucket의 위치를 결정한다. + +```text +Key + ↓ +hashCode() + ↓ +Hash 값 + ↓ +Bucket 위치 결정 +``` + +#### Hash Collision (해시 충돌) + +서로 다른 Key가 같은 Bucket에 들어가게 되는 경우이다. + +```text +"apple" → Bucket 2 +"grape" → Bucket 2 + ↓ + 충돌 발생 +``` + +이 경우 같은 Bucket에 여러 Node를 연결한다. + +```text +Bucket[2] + +[apple | 100] → [grape | 200] → [melon | 300] +``` + +충돌이 너무 많아지면 Java에서는 연결 리스트를 레드-블랙 트리로 변경하여 검색 성능을 개선한다. + +- 연결 리스트: 최악의 경우 **O(n)** +- 레드-블랙 트리: **O(log n)** + +#### 데이터 저장 과정 (`put`) + +```text +map.put("apple", 100) + ↓ +"apple"의 hashCode() 계산 + ↓ +Hash 값 계산 + ↓ +저장할 Bucket 위치 결정 + ↓ +Bucket이 비어 있음 → 바로 저장 +Bucket에 데이터가 있음 → equals()로 Key 비교 + ↓ +같은 Key → Value 변경 +다른 Key → 새로운 Node 추가 +``` + +예를 들어, + +```java +map.put("apple", 100); +map.put("apple", 200); +``` + +같은 Key이기 때문에 새로운 데이터를 추가하는 것이 아니라 **Value가 100 → 200으로 변경**된다. + +#### 데이터 검색 과정 (`get`) + +```text +map.get("apple") + ↓ +"apple"의 hashCode() 계산 + ↓ +Bucket 위치 찾기 + ↓ +해당 Bucket의 Node 확인 + ↓ +equals()로 Key 비교 + ↓ +Value 반환 +``` + +즉, **hashCode()로 위치를 찾고 → equals()로 정확한 Key인지 확인한다.** + +#### Resizing (크기 조절) + +HashMap은 데이터가 너무 많이 쌓이면 Bucket 배열의 크기를 늘린다. + +기본적으로 **Load Factor(적재율) 0.75**를 사용한다. + +```text +Capacity = 16 +Load Factor = 0.75 + +16 × 0.75 = 12 +``` + +데이터가 기준치를 넘으면 배열 크기를 2배로 늘리고, 기존 데이터를 새로운 배열에 다시 배치한다. + +```text +16칸 + ↓ +데이터 증가 + ↓ +기준 초과 + ↓ +32칸으로 확장 + ↓ +기존 데이터 재배치 +``` + +#### 핵심 흐름 + +> **Key → hashCode() → Bucket 위치 → Node 저장** + +그리고 같은 위치에 여러 데이터가 들어오면 + +> **Collision → LinkedList → 충돌이 많으면 Red-Black Tree** + +마지막으로 데이터가 너무 많아지면 + +> **Resize → Bucket 배열 크기 증가 → 데이터 재배치** + +**HashMap = Bucket 배열 + Node + Hash + Collision 처리 + Resize** + +### ConcurrentHashMap + +여러 **Thread(스레드)** 가 동시에 데이터를 읽고 수정할 수 있도록 만들어진 Map이다. + +일반적인 `HashMap`은 여러 스레드가 동시에 데이터를 수정할 경우 데이터가 꼬이거나 예상하지 못한 결과가 발생할 수 있다. +`ConcurrentHashMap`은 이런 동시 접근 문제를 줄이면서도 성능을 유지하도록 설계되어 있다. + +#### HashMap과 비교 + +- **HashMap** → 여러 스레드가 동시에 수정하는 상황에서는 안전하지 않음 +- **ConcurrentHashMap** → 여러 스레드가 동시에 접근해도 안전하게 사용할 수 있도록 설계됨 + +```text +Thread 1 ──┐ + ├──→ ConcurrentHashMap +Thread 2 ──┤ ↓ +Thread 3 ──┘ 안전하게 처리 +``` + +#### 어떻게 동시에 사용할 수 있을까? + +전체 Map을 한 번에 잠그는 것이 아니라, 필요한 부분만 동기화하여 여러 스레드가 동시에 작업할 수 있도록 한다. + +예를 들어 한 스레드가 특정 Bucket을 수정하고 있어도, 다른 스레드는 영향을 받지 않는 다른 부분에 접근할 수 있다. + +→ `HashMap` 전체를 하나의 Lock으로 잠그는 방식보다 동시성이 높다. + +#### 주요 특징 + +- 여러 Thread에서 동시에 읽기/쓰기 가능 +- Thread-Safe한 Map이 필요한 경우 사용 +- 전체 Map을 하나의 Lock으로 잠그지 않아 동시 처리 성능을 높일 수 있음 +- `null` Key와 Value를 허용하지 않음 + +#### 주요 메서드 + +```java +ConcurrentHashMap map = new ConcurrentHashMap<>(); + +map.put("apple", 100); +map.get("apple"); +map.remove("apple"); +``` + +여러 Thread가 동시에 접근해도 안전하게 사용할 수 있다. + +#### HashMap → ConcurrentHashMap 연결 + +```text +HashMap + ↓ +Key → Hash → Bucket + ↓ +빠른 데이터 검색 + +ConcurrentHashMap + ↓ +HashMap과 비슷한 구조 + + +여러 Thread의 동시 접근을 안전하게 처리 +``` + +> **HashMap = 빠른 Key-Value 저장** +> **ConcurrentHashMap = 여러 Thread가 동시에 사용해도 안전한 HashMap** + +즉, 멀티스레드 환경에서 Map을 사용해야 한다면 ConcurrentHashMap을 고려한다. \ No newline at end of file diff --git "a/02\354\243\274\354\260\250/README.md" "b/02\354\243\274\354\260\250/README.md" index 12cf94e..b51b366 100644 --- "a/02\354\243\274\354\260\250/README.md" +++ "b/02\354\243\274\354\260\250/README.md" @@ -1,21 +1,444 @@ # 2주차 - OS - Process / Thread / 동시성 ## 공통 키워드 -- Process -- Thread -- Context Switching -- 동시성/병렬성 -- Race Condition -- Mutex/Semaphore + +### Process + +실행 중인 프로그램을 의미한다. 프로그램이 디스크에 저장된 코드 덩어리라면, 프로세스는 그 프로그램이 메모리에 올라가 CPU를 할당받아 실제로 동작하고 있는 상태이다. + +운영체제는 각 프로세스에게 **독립된 메모리 공간**을 할당한다. + +``` +Process +┌──────────────┐ +│ Stack │ ← 지역 변수, 함수 호출 정보 +│ ↓ │ +│ │ +│ ↑ │ +│ Heap │ ← 동적으로 할당되는 데이터 +├──────────────┤ +│ Data │ ← 전역 변수, 정적 변수 +├──────────────┤ +│ Code │ ← 실행할 코드 +└──────────────┘ +``` + +**PCB (Process Control Block)** + +운영체제가 프로세스를 관리하기 위해 프로세스의 상태, PID, 프로그램 카운터, 레지스터, 메모리 관리 정보 등을 저장하는 자료구조이다. + +``` +PCB +├── 프로세스 ID (PID) +├── 프로세스 상태 +├── Program Counter → 다음에 실행할 명령어 위치 +├── 레지스터 값 +└── 메모리 정보 +└── 파일 정보 +``` + +**프로세스 상태** + +``` +생성(new) → 준비(ready) ⇄ 실행(running) → 종료(terminated) + ↑ ↓ + └── 대기(waiting) ←┘ +``` + +**특징** + +프로세스끼리는 독립된 메모리 공간(Address Space)을 사용하며, 기본적으로 메모리를 공유하지 않는다. + +- 하나의 프로세스에 문제가 생겨도 **다른 프로세스에 직접적인 영향을 주지 않는다.** + - → **프로세스 격리(Process Isolation)** 덕분에 안정성과 보안성이 높음. +- 프로세스 간 데이터를 주고받으려면 IPC(Inter-Process Communication)**가 필요하다. + - 프로세스마다 메모리 공간이 분리되어 있기 때문에 직접 데이터를 주고받을 수 없음. + - 대표적인 IPC 방식: + - **Pipe** → 부모-자식 프로세스 등 관련된 프로세스 간 데이터 전달 + - **Named Pipe** → 서로 관련 없는 프로세스 간에도 통신 가능 + - **Socket** → 같은 컴퓨터뿐 아니라 네트워크를 통한 프로세스 간 통신 가능 + - **Shared Memory** → 메모리 영역을 공유하여 빠르게 데이터 교환 + - **Message Queue** → 메시지를 큐에 넣어 프로세스 간 데이터 전달 + - **Signal** → 프로세스에 특정 이벤트를 알림 +- **프로세스 생성 비용과 Context Switching 비용이 상대적으로 크다.** + - 독립된 메모리 공간과 PCB 등을 관리해야 하기 때문. +- 프로세스마다 **독립적인 주소 공간**을 가지므로 **안정성과 보안성이 높다.** +- 여러 프로세스가 독립적으로 실행될 수 있어 **한 프로세스의 장애가 전체 시스템으로 전파되는 것을 막을 수 있다.** +- 프로세스 격리(Process Isolation)를 통해 하나의 프로세스가 다른 프로세스의 메모리를 임의로 변경하기 어렵다. + - → 안정성(Stability)과 보안성(Security)이 높음. + +--- + +### Thread + +프로세스 안에서 실제로 실행되는 흐름의 단위이다. 하나의 프로세스는 여러 개의 스레드를 가질 수 있다. + +스레드들은 프로세스의 **Code, Data, Heap 영역을 공유**하고, **Stack은 각자 따로** 가진다. + +``` +Process +┌─────────────────────────────────┐ +│ Code Data Heap (공유) │ +├──────────┬──────────┬───────────┤ +│ Stack 1 │ Stack 2 │ Stack 3 │ +│ Thread 1 │ Thread 2 │ Thread 3 │ +└──────────┴──────────┴───────────┘ +``` + +Stack을 따로 가지는 이유는 스레드마다 실행하는 함수와 지역 변수가 다르기 때문이다. + +**Process vs Thread** + +| 구분 | Process | Thread | +|---|---|---| +| 메모리 | 독립적으로 할당 | Code/Data/Heap 공유 | +| 생성 비용 | 크다 | 작다 | +| 전환 비용 | 크다 | 작다 | +| 통신 | IPC 필요 | 공유 메모리로 바로 가능 | +| 안정성 | 하나가 죽어도 영향 없음 | 하나가 죽으면 프로세스 전체에 영향 | + +**특징** + +- 데이터를 공유하기 때문에 통신이 빠르다. +- 공유하기 때문에 동기화 문제(Race Condition)가 발생할 수 있다. + +--- + +### Context Switching + +CPU가 현재 실행 중인 **Process 또는 Thread의 실행 상태(Context)를 저장하고, 다른 Process 또는 Thread의 실행 상태를 복원하여 CPU 실행 대상을 전환하는 것**이다. + +CPU는 한 번에 하나의 실행 흐름만 처리할 수 있기 때문에, 여러 Process/Thread를 번갈아 실행하기 위해 Context Switching이 발생한다. + +```text +Process A 실행 + ↓ +Context 저장 +(PCB/TCB에 CPU 상태 저장) + ↓ +Scheduler가 다음 실행 대상 선택 + ↓ +Process B의 Context 복원 + ↓ +Process B 실행 +``` + +**왜 필요한가** + +CPU는 한 번에 하나의 작업만 처리할 수 있지만, 여러 작업을 짧게 번갈아 실행하면 사용자 입장에서는 동시에 실행되는 것처럼 보인다. + +**오버헤드** + +전환하는 동안에는 실제 작업을 하지 않기 때문에 순수한 비용만 발생한다. + +- 상태를 저장하고 복원하는 시간 +- CPU 캐시와 TLB가 무효화되어 다시 채워야 하는 비용 + +**프로세스 전환 vs 스레드 전환** + +``` +프로세스 전환 → 메모리 공간까지 전부 교체 → 비용 큼 +스레드 전환 → 메모리 공간은 그대로, Stack과 레지스터만 교체 → 비용 작음 +``` + +스레드 간 전환이 프로세스 간 전환보다 훨씬 가볍다. + +--- + +### 동시성 / 병렬성 + +**동시성 (Concurrency)** + +여러 작업을 아주 짧게 번갈아 처리해서 동시에 실행되는 것처럼 보이게 하는 것이다. 싱글 코어에서도 가능하다. + +``` +CPU 1개 + +시간 → +[A][B][A][B][A][B] +``` + +**병렬성 (Parallelism)** + +여러 개의 CPU 코어가 실제로 같은 시점에 작업을 처리하는 것이다. 멀티 코어가 있어야 가능하다. + +``` +CPU 1 [A][A][A][A] +CPU 2 [B][B][B][B] + +시간 → +``` + +| 구분 | 동시성 | 병렬성 | +|---|---|---| +| 실행 | 번갈아 실행 | 실제 동시 실행 | +| 코어 | 싱글 코어에서도 가능 | 멀티 코어 필요 | +| 목적 | 여러 작업을 다루는 구조 | 처리 속도 향상 | + +--- + +### Race Condition + +두 개 이상의 스레드가 **공유 자원에 동시에 접근**할 때, 실행 순서에 따라 결과가 달라지는 현상이다. + +`count++` 는 한 줄처럼 보이지만 실제로는 세 단계로 나뉜다. + +``` +1. count 값을 읽는다 +2. 1을 더한다 +3. 결과를 다시 저장한다 +``` + +이 중간에 다른 스레드가 끼어들면 문제가 생긴다. + +``` +초기값 count = 0 + +Thread A Thread B +read count (0) + read count (0) ++1 → 1 + +1 → 1 +write 1 + write 1 + +기대값: 2 +실제값: 1 ← 갱신 손실 +``` + +**임계 영역 (Critical Section)** + +공유 자원에 접근하는 코드 영역을 말한다. 이 영역에는 한 번에 하나의 스레드만 들어가도록 보장해야 하며, 이를 **상호 배제(Mutual Exclusion)** 라고 한다. + +--- + +### Mutex / Semaphore + +임계 영역을 보호하기 위해 사용하는 동기화 도구이다. + +**Mutex (Mutual Exclusion)** + +락을 획득한 **하나의 스레드만** 임계 영역에 들어갈 수 있다. 락에 대한 **소유권** 개념이 있어서, 락을 건 스레드만 풀 수 있다. + +``` +Thread A ── lock 획득 ──→ [ 임계 영역 ] ──→ unlock +Thread B ── 대기 ─────────────────────────→ 진입 +``` + +**Semaphore** + +정해진 개수만큼 동시 접근을 허용한다. 내부적으로 카운트를 가지고 있으며, 소유권 개념이 없어 다른 스레드가 신호를 줄 수도 있다. + +``` +Semaphore(2) ← 동시에 2개까지 허용 + +Thread A → 진입 (남은 자리 1) +Thread B → 진입 (남은 자리 0) +Thread C → 대기 +``` + +| 구분 | Mutex | Semaphore | +|---|---|---| +| 동시 접근 | 1개 | N개 | +| 소유권 | 있음 | 없음 | +| 용도 | 임계 영역 보호 | 자원 개수 제한, 흐름 제어 | + +**Deadlock (교착 상태)** + +두 스레드가 서로가 가진 락을 기다리며 영원히 멈추는 상황이다. + +``` +Thread A: Lock 1 보유 → Lock 2 대기 +Thread B: Lock 2 보유 → Lock 1 대기 + ↓ + 영원히 대기 +``` + +발생 조건은 상호 배제, 점유와 대기, 비선점, 순환 대기 네 가지이며, 이 중 하나만 깨도 교착 상태를 막을 수 있다. 실무에서는 보통 **락을 획득하는 순서를 통일**해서 순환 대기를 방지한다. + +--- ## Backend 심화 (선택) -- Java Thread -- Thread Pool -- synchronized -- Atomic -- WAS Thread - -## Frontend 심화 (선택) -- JS Single Thread -- Web Worker -- 브라우저 Multi-Process 구조 + +### Java Thread + +**생성 방법** + +```java +// 1. Thread 상속 +class MyThread extends Thread { + public void run() { ... } +} + +// 2. Runnable 구현 (권장) +class MyTask implements Runnable { + public void run() { ... } +} +new Thread(new MyTask()).start(); +``` + +Java는 단일 상속만 가능하기 때문에 `Runnable`을 구현하는 방식이 더 유연하다. + +**start() vs run()** + +``` +start() → 새로운 스레드를 만들어서 run()을 실행 +run() → 그냥 현재 스레드에서 메서드를 호출할 뿐 (새 스레드 X) +``` + +**스레드 생명주기** + +``` +NEW → RUNNABLE ⇄ BLOCKED / WAITING / TIMED_WAITING + ↓ + TERMINATED +``` + +- `NEW`: 생성되었지만 아직 start() 되지 않음 +- `RUNNABLE`: 실행 중이거나 실행 대기 중 +- `BLOCKED`: 모니터 락을 기다리는 중 +- `WAITING`: 다른 스레드의 신호를 기다리는 중 +- `TIMED_WAITING`: 시간이 정해진 대기 +- `TERMINATED`: 종료 + +--- + +### Thread Pool + +스레드를 미리 만들어 두고 재사용하는 방식이다. + +**왜 필요한가** + +- 스레드 생성과 소멸 비용 감소를 위해! +- 요청마다 스레드를 만들면 스레드 수가 무한정 늘어나 메모리 부족이나 과도한 Context Switching이 발생한다. + +``` +요청 → [작업 큐] → [Thread Pool] + ├── Thread 1 + ├── Thread 2 + └── Thread 3 + (작업이 끝나면 반납 후 재사용) +``` + +**주요 설정값** + +- `corePoolSize`: 기본으로 유지하는 스레드 수 +- `maximumPoolSize`: 최대 스레드 수 +- `workQueue`: 대기 중인 작업을 담는 큐 +- `keepAliveTime`: 초과 생성된 스레드가 놀고 있을 때 유지되는 시간 + +**동작 순서** + +``` +작업 요청 + ↓ +core 스레드에 여유 있음 → 바로 실행 + ↓ +없음 → 큐에 대기 + ↓ +큐도 가득 참 → max까지 스레드 추가 생성 + ↓ +그것도 초과 → 거부 정책(RejectedExecutionHandler) 실행 +``` + +--- + +### synchronized + +임계 영역을 보호하는 Java의 기본 동기화 키워드이다. Java의 모든 객체는 **모니터 락**을 하나씩 가지고 있고, `synchronized`는 그 락을 사용한다. + +```java +// 메서드 전체 +public synchronized void add() { count++; } + +// 블록 단위 (범위를 좁힐 수 있어 더 효율적) +public void add() { + synchronized (this) { count++; } +} +``` + +**특징** + +- 락을 얻지 못한 스레드는 `BLOCKED` 상태로 대기한다. +- 대기하는 동안 스레드가 멈춰 있어 성능 저하가 발생할 수 있다. +- 락 범위가 넓을수록 동시 처리 성능이 떨어지므로 블록 단위로 최소화하는 것이 좋다. + +--- + +### Atomic + +락을 사용하지 않고도 원자적인 연산을 보장하는 클래스이다. `AtomicInteger`, `AtomicLong` 등이 있다. + +```java +AtomicInteger count = new AtomicInteger(0); +count.incrementAndGet(); // 원자적으로 +1 +``` + +**CAS (Compare And Swap)** + +``` +1. 현재 값을 읽는다 (기대값) +2. 메모리의 실제 값과 기대값을 비교한다 +3. 같으면 새 값으로 변경 + 다르면 실패 → 다시 시도 (재시도 반복) +``` + +**synchronized와 비교** + +| 구분 | synchronized | Atomic | +|---|---|---| +| 방식 | 락 기반 (Blocking) | CAS 기반 (Non-Blocking) | +| 대기 | 스레드가 멈춤 | 실패 시 재시도 | +| 성능 | 경합이 심하면 유리할 수도 | 단순 연산에서 유리 | +| 범위 | 여러 줄의 로직 보호 가능 | 단일 변수 연산에 적합 | + +경합이 아주 심하면 CAS 재시도가 반복되어 오히려 손해일 수 있다. + +--- + +### WAS Thread + +Tomcat 같은 WAS는 요청을 처리할 때 **스레드 풀에서 스레드를 1개를 꺼내서 할당**한다. (Thread per Request 모델) + +``` +HTTP 요청 → [Acceptor] → [작업 큐] → [Tomcat Thread Pool] + ├── Thread 1 → 요청 처리 + ├── Thread 2 → 요청 처리 + └── ... + 응답 후 스레드 반납 +``` + +**주요 설정 (Tomcat)** + +- `maxThreads`: 동시에 요청을 처리할 수 있는 최대 스레드 수 (Spring Boot 기본 200) +- `acceptCount`: 스레드가 전부 사용 중일 때 대기 큐에 쌓을 수 있는 요청 수 +- `maxConnections`: 동시에 처리 가능한 커넥션 수 + +**스레드 고갈 (Thread Starvation)** + +느린 DB 쿼리나 외부 API 호출 때문에 스레드가 오래 점유되면, 스레드 풀이 전부 소진되어 새로운 요청을 받지 못하는 상태가 된다. + +``` +느린 외부 API 호출 + ↓ +스레드가 응답을 기다리며 계속 점유 + ↓ +maxThreads 전부 소진 + ↓ +새 요청은 큐에서 대기 → 응답 지연 / 타임아웃 +``` + +**DB 커넥션 풀과의 관계** + +WAS 스레드 수보다 DB 커넥션 수가 적으면, 스레드가 커넥션을 얻기 위해 또 대기하게 된다. 그래서 스레드 풀 크기와 커넥션 풀 크기는 함께 고려해야 한다. + +``` +Tomcat Thread 200개 + ↓ +DB Connection Pool 10개 + ↓ +190개 스레드는 커넥션 대기 → 병목 +``` + +무조건 `maxThreads`를 늘리는 것이 답은 아니며, 스레드가 늘어날수록 Context Switching 비용과 메모리 사용량도 함께 증가한다. +