diff --git "a/01\354\243\274\354\260\250/weeks_01.md" "b/01\354\243\274\354\260\250/weeks_01.md" new file mode 100644 index 0000000..972a70d --- /dev/null +++ "b/01\354\243\274\354\260\250/weeks_01.md" @@ -0,0 +1,1128 @@ +# 1주차 - 자료구조 / 시간복잡도 + +## 1. Big-O + +### Big-O란? + +입력 크기 N이 커질 때 알고리즘의 시간 또는 공간 사용량이 얼마나 증가하는지 표현하는 방법. + +중요한 점은 실제 실행 시간 자체가 아니라, 입력 크기가 증가함에 따라 실행량이 **어떤 비율로 증가하는가**를 보는 것. + +예를 들어 반복문이 n번 실행되므로: **O(N)**. + +```java +for (int i = 0; i < n; i++) { + System.out.println(i); +} +``` + + + +### 대표적인 시간복잡도 + +| 복잡도 | 의미 | 대표 사례 | +|---|---|---| +| O(1) | 입력 크기와 무관 | 배열 인덱스 접근, HashMap 평균 조회 | +| O(log N) | 탐색 범위를 계속 절반으로 줄임 | 이진 탐색 | +| O(N) | 데이터 전체 탐색 | 배열 순회 | +| O(N log N) | 효율적인 정렬 | Merge Sort, Heap Sort | +| O(N²) | 이중 반복 | 단순 비교 정렬 | +| O(2^N) | 모든 부분집합 탐색 | 백트래킹 | +| O(N!) | 모든 순열 탐색 | 순열 완전탐색 | + +성능 차이는 N이 커질수록 극단적으로 벌어짐 / 예를 들어 N = 1,000,000이면 대략 + +``` +O(1) → 1 +O(log N) → 약 20 +O(N) → 1,000,000 +O(N log N) → 약 20,000,000 +O(N²) → 1,000,000,000,000 +``` + +### 상수 제거 + +전체 연산은 `2N`이지만 Big-O에서는 **O(N)**. + +또한 `N² + N + 100` 이라면 가장 빠르게 증가하는 N²만 남겨 **O(N²)**. + +```java +for (...) {} // N +for (...) {} // N +``` + + +### 평균 / 최악 / 최선 + +예를 들어 배열에서 값을 찾는 경우 + +- 첫 번째 위치에 있으면: O(1) +- 마지막에 있거나 없으면: O(N) +- 일반적으로 알고리즘에서는 **최악의 시간복잡도**를 많이 이야기함. + +```java +for (int i = 0; i < arr.length; i++) { + if (arr[i] == target) { + return i; + } +} +``` + +## 2. Array + +Array는 데이터를 연속된 메모리 공간에 저장하는 자료구조 + +```java +int[] arr = new int[5]; +``` + +개념 + +``` +index 0 1 2 3 4 + ┌───┬───┬───┬───┬───┐ + │10 │20 │30 │40 │50 │ + └───┴───┴───┴───┴───┘ +``` + +### 인덱스 접근이 O(1)인 이유 + +배열 시작 주소를 알고 있는 경우 + +``` +주소 = 시작주소 + index × 데이터크기로 원하는 위치를 바로 계산 가능 +그래서 arr[3]은 전체 배열을 순회할 필요 없이 바로 접근 가능 → **O(1)** +``` + + + + +### 삽입 / 삭제 + +중간에 값을 삽입하는 경우 + +기존 +``` +[A][B][C][D] +``` + +B와 C 사이에 X 삽입 +``` +[A][B][X][C][D] +``` + +뒤의 값들을 모두 한 칸씩 이동해야 함 + +- 삽입 O(N) +- 삭제 O(N) + +### Array 복잡도 + +| 연산 | 복잡도 | +|---|---| +| index 조회 | O(1) | +| 값 검색 | O(N) | +| 마지막 삽입 | 경우에 따라 O(1) | +| 중간 삽입 | O(N) | +| 중간 삭제 | O(N) | + +## 3. LinkedList + +LinkedList는 Array처럼 연속된 메모리 공간을 사용하지 않음 / 각 Node가 다음 Node를 가리키는 형태 + +``` +[A | next] → [B | next] → [C | next] → null +``` + +Java의 LinkedList는 정확히는 **Doubly Linked List** + +``` +null ← A ⇄ B ⇄ C → null +``` + +각 Node가 아래처럼 이전 노드와 다음 노드를 모두 알고 있음 + +```java +class Node { + Node prev; + Object item; + Node next; +} +``` + + + +### 조회 + +Array라면 `arr[100];` 으로 바로 접근 가능 + +하지만 LinkedList의 경우 아래와 깉이 하나씩 따라가야 함 / 따라서 **O(N)**. + +``` +head + ↓ +A → B → C → D → ... +``` + + + +### 삽입 / 삭제 + +노드를 이미 알고 있다고 가정하면 + +``` +A → B → C +``` + +여기에서 B를 제거할 때 + +``` +A → C +``` + +연결만 변경하면 됨 → **O(1)** + +다만 삭제하려는 노드를 찾는 과정은 O(N)일 수 있음. + +그래서 흔히 "LinkedList의 삽입/삭제는 무조건 O(1)"이라고 외우기 X + +정확하게는: **삽입/삭제 위치를 이미 알고 있다면 O(1), 위치 탐색까지 필요하면 O(N)** + +### Array vs LinkedList + +| | Array | LinkedList | +|---|---|---| +| 메모리 | 연속 | 비연속 | +| index 조회 | O(1) | O(N) | +| 검색 | O(N) | O(N) | +| 중간 삽입/삭제 | O(N) | 노드 확보 시 O(1) | +| Cache locality | 좋음 | 나쁨 | +| 추가 포인터 | 없음 | 필요 | + + +## 4. Stack + +Stack은 **LIFO - Last In First Out** 구조. 마지막에 들어간 값이 가장 먼저 나옴 + +``` +push A +[A] + +push B +[B] +[A] + +push C +[C] +[B] +[A] + +pop → C +``` + +### 주요 연산 + +| 연산 | 설명 | +|---|---| +| push | 데이터 삽입 | +| pop | 데이터 제거 | +| peek | 최상단 조회 | + +모두 일반적으로 **O(1)** + +### 대표 사용 사례 + +**함수 호출 Stack** + +``` +main() + ↓ +foo() + ↓ +bar() +``` + +호출 순서는 `main → foo → bar`, 반환 순서는 `bar → foo → main` — 즉 LIFO + +**DFS** + +```java +Stack stack; +``` + +또는 재귀 함수가 내부적으로 Call Stack을 사용 + +**Undo** + +``` +작업1 +작업2 +작업3 + +Undo: +작업3 취소 +작업2 취소 +작업1 취소 +``` + + + +### Java에서는 Stack보다 Deque + +옛날 + +```java +Stack stack = new Stack<>(); +``` + +요즘은 deque 권장 => Stack은 오래된 Vector 기반 클래스이며 불필요한 동기화 등의 문제가 있기 때문 +```java +Deque stack = new ArrayDeque<>(); + +stack.push(1); +stack.pop(); +stack.peek(); +``` + + + +## 5. Queue + +Queue는 **FIFO — First In First Out** / 먼저 들어온 데이터가 먼저 나감 + +``` +A → B → C + +poll() +A 제거 + +B → C +``` + +### 주요 연산 + +| 연산 | 설명 | +|---|---| +| offer() | 삽입 | +| poll() | 제거 | +| peek() | 조회 | + +일반적인 구현에서 **O(1)** + +### 대표 사용 사례 + +**BFS** + +```java +Queue queue = new ArrayDeque<>(); +``` + +BFS는 가까운 노드부터 탐색하기 때문에 Queue를 사용 + +**작업 처리** + +백엔드 시스템에서도 굉장히 자주 등장 + +``` +Client + ↓ +Queue + ↓ +Worker +``` + +예: 주문 요청, 메일 발송, Push 알림, 이미지 처리, 로그 처리 + +AWS SQS, RabbitMQ, Kafka 등의 메시징 시스템도 넓게 보면 Queue와 관련된 아이디어를 가지고 있음. + +## 6. Hash Table + +Java의 `HashMap`, `HashSet`, `ConcurrentHashMap` 등이 모두 관련되어 있음 + +핵심 아이디어는 **Key를 Hash Function에 넣어 저장 위치를 계산**하는 것 + +``` +Key + ↓ +hash() + ↓ +Hash Value + ↓ +Bucket +``` + +예시 + +``` +"pooreum" + ↓ + hash() + ↓ +9348293 + ↓ +bucket index = 5 +``` + +Bucket + +``` +0 +1 +2 +3 +4 +5 → pooreum +6 +7 +``` + +따라서 데이터를 처음부터 탐색하지 않아도 됨. +- 조회 O(1) +- 삽입 O(1) +- 삭제 O(1) + +## 7. Hash Collision + +Hash 값이 다르더라도 같은 Bucket에 들어갈 수 있음 + +``` +hash(A) → bucket 3 +hash(B) → bucket 3 +``` + +이것을 **Hash Collision** 이라고 함 + +충돌을 처리하는 대표 방식으로 **Separate Chaining** + +``` +Bucket 3 + ↓ +[A] → [B] → [C] +``` + +한 Bucket에 여러 Entry를 연결하여 Java HashMap이 사용하는 방식 + +## 8. Tree + +Tree는 계층형 자료구조 + +``` + A + / \ + B C + / \ / \ + D E F G +``` + +주요 용어 + +| 용어 | 설명 | +|---|---| +| Root | A | +| Parent | B의 Parent = A | +| Child | A의 Child = B, C | +| Leaf | D, E, F, G | +| Depth | Root에서 떨어진 거리 | +| Height | 가장 깊은 Leaf까지 거리 | + +## 9. Binary Tree + +각 Node가 최대 2개의 Child를 가지는 Tree / **Binary Tree라고 해서 자동으로 정렬되어 있는 것은 아님** + +``` + A + / \ + B C +``` + + + +## 10. Binary Search Tree + +Binary Search Tree(BST)는 추가 규칙을 가지고 있음 + +``` +왼쪽 < 부모 < 오른쪽 +``` + +예시 + +``` + 8 + / \ + 4 12 + / \ / \ + 2 6 10 14 +``` + +10을 찾는 경우 + +``` +10 > 8 +→ 오른쪽 + +10 < 12 +→ 왼쪽 + +10 발견 +``` + +균형 잡힌 경우: **O(log N)** + +### BST 최악의 경우 + +데이터가 `1 2 3 4 5` 순서대로 들어가면: + +``` +1 + \ + 2 + \ + 3 + \ + 4 + \ + 5 +``` + +사실상 LinkedList가 됨. 따라서 탐색이 **O(N)** 까지 떨어짐 + +이를 해결하기 위해 **AVL Tree**, **Red-Black Tree** 같은 Self-Balancing Tree가 사용됨 + +## 11. Tree 순회 + +- **Preorder**: Root → Left → Right +- **Inorder**: Left → Root → Right — BST를 Inorder 순회하면 정렬된 결과를 얻을 수 있음 +- **Postorder**: Left → Right → Root + +## 12. Heap + +Heap은 우선순위를 빠르게 처리하기 위한 완전 이진 트리. 대표적으로 **Max Heap**, **Min Heap** 이 있음 + +### Max Heap + +부모가 자식보다 큼 + +``` + 10 + / \ + 8 7 + / \ + 3 5 +``` + +따라서 Root에는 항상 최대값이 있음 → 최대값 조회 O(1) + +### Min Heap + +부모가 자식보다 작음 + +``` + 1 + / \ + 3 2 + / \ + 7 5 +``` + +Root에는 최소값이 있음 + +### 삽입 + +새 노드를 마지막에 넣고 부모와 비교하며 올라감 (**Bubble Up / Heapify Up**) + +Tree 높이가 log N 이므로 **O(log N)** + +### 삭제 + +Root를 제거한 뒤 마지막 노드를 Root로 가져옴. 그 다음 자식과 비교하며 내려감 (**Bubble Down / Heapify Down**) + +역시 **O(log N)** + +### Heap 복잡도 + +| 연산 | 시간복잡도 | +|---|---| +| 최소/최대 조회 | O(1) | +| 삽입 | O(log N) | +| 삭제 | O(log N) | +| 특정 값 검색 | O(N) | + +### Java PriorityQueue + +Java에서는 Heap을 직접 구현하지않고 PriorityQueue를 사용. 기본적으로 **Min Heap** + +```java +PriorityQueue pq = new PriorityQueue<>(); +``` + + + +```java +pq.offer(5); +pq.offer(1); +pq.offer(3); + +pq.poll(); // 1 +``` + +Max Heap은 아래 코드처럼 사용 가능 +```java +PriorityQueue pq = + new PriorityQueue<>(Comparator.reverseOrder()); +``` + + + +--- + +# Backend 심화 + +## 13. Java Collection Framework + +Java Collection Framework는 여러 자료구조를 표준화하여 제공하는 API + +``` + Iterable + │ + Collection + ┌──────┼──────┐ + List Set Queue + │ │ │ + ArrayList HashSet Deque + LinkedList TreeSet │ + ArrayDeque + + +Map + │ + ├─ HashMap + ├─ LinkedHashMap + ├─ TreeMap + └─ ConcurrentHashMap + Map은 Collection의 하위 인터페이스가 아님 +``` + + + +## 14. List + +순서가 있고 중복을 허용 + +```java +List list = new ArrayList<>(); +``` + +대표 구현체: `ArrayList`, `LinkedList` + +### ArrayList + +내부적으로 `Object[]` 배열 사용. 따라서 index 조회가 **O(1)** + +```java +transient Object[] elementData; +``` + + + +### ArrayList 크기가 부족한 경우 + +예를 들어 배열 용량이 10인데 11번째 데이터를 넣으면 기존 배열을 계속 사용할 수 없음. 그래서 더 큰 배열을 만든 후 데이터를 복사 + +이 작업 자체는 **O(N)** / 그런데 매번 발생하는 것은 아니므로 평균적으로 마지막 삽입은 **Amortized O(1)** 이라고 표현 + +``` +[기존 배열] + ↓ +더 큰 배열 생성 + ↓ +기존 값 복사 +``` + + + +## 15. Set + +중복을 허용하지 않는 자료구조. 대표적으로 `HashSet`, `TreeSet`, `LinkedHashSet` + +### HashSet + +```java +Set set = new HashSet<>(); +``` + +평균: 삽입 O(1), 조회 O(1), 삭제 O(1) + + **HashSet 내부적으로 HashMap을 사용** + + + +```java +private transient HashMap map; +``` + +Set의 값은 HashMap의 Key로 저장 + +``` +HashSet.add("A") +≈ +HashMap.put("A", dummyValue) +``` + +## 16. Map + +Key-Value 자료구조 / Key는 중복될 수 없지만 Value는 중복 가능 + +```java +Map users = new HashMap<>(); +``` + +``` +"kim" → User +"lee" → User +"park" → User +``` + + + +## 17. HashMap 내부 구조 + +``` +HashMap + │ + └── Node[] table + │ + ├── bucket 0 + ├── bucket 1 + ├── bucket 2 → Node → Node + └── ... +``` + +Entry 개념 + +```java +class Node { + int hash; + K key; + V value; + Node next; +} +``` + +## 18. HashMap put() + +예시 + +```java +map.put("pooreum", user); +``` + + +``` +1. key.hashCode() + ↓ +2. Hash 보정 + ↓ +3. Bucket index 계산 + ↓ +4. 해당 Bucket 탐색 + ↓ +5. Key가 없으면 추가 + Key가 있으면 Value 교체 +``` + +## 19. hashCode()와 equals() + +Key를 비교할 때 기본적인 흐름 + +``` +hashCode 비교 + ↓ +equals 비교 +``` + +예시 + +```java +class User { + private Long id; +} +``` + +두 객체가 논리적으로 동일한 Key여야 한다면 `equals()`와 `hashCode()` 계약을 올바르게 구현해야 함 + +핵심 규칙: **`equals()`가 true인 두 객체는 반드시 같은 `hashCode()`를 반환해야 함** + +반대는 성립하지 않음 + +``` +hashCode가 같음 +≠ +equals가 반드시 true => Hash Collision이 존재할 수 있음 +``` + + + +## 20. HashMap Bucket 계산 + +단순하게 생각하면 `index = hash % bucketSize` 라고 생각할 수 있음 + +하지만 Java HashMap의 table 크기는 2의 거듭제곱으로 유지되기 때문에 bit 연산을 사용 + +```java +index = (n - 1) & hash; +``` + +예를 들어 `n = 16` 이면 `n - 1 = 15 = 1111₂` + +이 비트 연산으로 0 ~ 15 범위의 Bucket을 결정 가능. + +## 21. HashMap Collision + +다음처럼 충돌하는 경우 + +``` +bucket 5 + +Node A + ↓ +Node B + ↓ +Node C +``` + +초기에는 LinkedList 형태로 저장 / 그렇다면 최악에는 **O(N)** 이 될 수 있음 + +## 22. Java 8 이후 Treeification + +Java 8부터 한 Bucket에 너무 많은 Entry가 몰리면 LinkedList를 Red-Black Tree로 변환 + +``` +LinkedList +A → B → C → D → ... +↓ +Red-Black Tree +``` + +이렇게 되면 탐색 복잡도가 **O(N) → O(log N)** 으로 개선 + +대표적인 Treeify 기준은 bucket의 노드 수가 일정 수준 이상일 때이며, table 자체도 충분히 커야 함 + +## 23. HashMap Load Factor + +HashMap은 Bucket이 너무 많이 차면 성능이 떨어짐. 그래서 `capacity`, `load factor`, `threshold` 개념을 사용 + +기본 Load Factor는 **0.75** + +예를 들어 capacity가 16이면 `16 × 0.75 = 12`, 약 12개를 넘어가면서 resize가 발생할 수 있음 + +## 24. HashMap Resize + +Resize가 발생하면 더 큰 Table을 만듦 + +``` +16 → 32 → 64 → 128 +``` + +그리고 기존 Entry를 새로운 Bucket 구조에 맞게 재배치. 따라서 Resize는 상대적으로 비싼 작업 + +그래서 데이터 크기를 미리 아는 경우 => 아래처럼 적절한 초기 크기를 고려할 수도 있음 + +```java +new HashMap<>(expectedSize); +``` + + + +## 25. HashMap 시간복잡도 + +평균적인 경우 + +``` +put O(1) +get O(1) +remove O(1) +``` + +충돌이 심한 최악의 상황에서는 Tree 구조 등의 영향을 받아 달라질 수 있음 + +포인트 => **HashMap O(1)은 절대적인 것이 아니라 평균 시간복잡도** + +## 26. HashMap은 Thread-Safe한가? + +`HashMap`은 Thread-Safe하지 않음. + +예시 + +``` +Thread A ─┐ + ├→ HashMap +Thread B ─┘ +``` + +두 Thread가 동시에 변경하면 데이터 일관성 문제가 발생할 수 있음 + +그래서 멀티스레드 환경에서는 상황에 따라 `ConcurrentHashMap`을 사용 + +## 27. ConcurrentHashMap + +ConcurrentHashMap은 **여러 Thread가 동시에 Map에 접근할 수 있도록 설계된 Thread-Safe Map**. + +```java +ConcurrentHashMap map = + new ConcurrentHashMap<>(); +``` + +## 28. Hashtable과 차이 + +과거에는 `Hashtable`을 사용했음. Hashtable은 주요 메서드에 동기화가 걸려 있어서 개념적으로는 아래처럼 병목이 생길 수 있음 + +``` +Thread A + ↓ +Map 전체 Lock + ↓ +작업 + ↓ +Unlock +``` + + +ConcurrentHashMap은 더 세밀하게 동시성을 제어. 그래서 여러 Thread가 서로 다른 영역을 수정 가능 + +``` +Thread A → bucket A +Thread B → bucket B + +동시 작업 가능 +``` + +## 29. Java 7 ConcurrentHashMap + +Java 7에서는 Segment Lock 구조 사용 / Map 전체를 Lock하지 않고 Segment 단위로 Lock + +``` +ConcurrentHashMap + +Segment 1 + ├ bucket + ├ bucket + +Segment 2 + ├ bucket + ├ bucket + +Segment 3 + ... +``` + + + +## 30. Java 8 이후 ConcurrentHashMap + +Java 8부터 Segment 구조가 제거됨. 주요 아이디어는 **CAS + synchronized** + +충돌이 없는 경우에는 CAS 같은 기법을 이용하고, 특정 Bucket에서 충돌하면서 수정해야 하는 경우에는 해당 영역에 대해 동기화를 수행 + +즉, **Map 전체 Lock X, 필요한 Bucket 수준 동기화**에 가까움 + +## 31. CAS란? + +CAS는 **Compare And Swap** + +``` +현재 값이 내가 예상한 값과 같은가? + +YES +→ 새로운 값으로 변경 + +NO +→ 실패 후 재시도 +``` + +예시 + +``` +Expected = 10 +Current = 10 +→ 11로 변경 성공 +``` + +예외 + +``` +Expected = 10 +Current = 11 +→ 다른 Thread가 이미 변경함 +→ 변경 실패 +``` + +Lock을 무조건 잡는 대신 CPU의 원자적 연산을 이용해 동시성 처리 가능 + +## 32. ConcurrentHashMap이 null을 허용하지 않는 이유 + + +HashMap은 `map.put(null, "value");` 같은 것이 가능 + +하지만 ConcurrentHashMap에서는 `map.put(null, "value");` 가 허용되지 않음 + +왜냐하면 멀티스레드 환경에서는 `map.get(key) == null` 일 때 이것이 + +1. Key가 존재하지 않는 것인지 +2. Key는 있는데 Value가 null인 것인지 구별하기 어렵기 때문 + +동시에 다른 Thread가 데이터를 변경할 수도 있기 때문에 애매함을 제거하고자 null을 금지 + +## 33. HashMap vs ConcurrentHashMap + +| | HashMap | ConcurrentHashMap | +|---|---|---| +| Thread-Safe | X | O | +| null Key | 가능 | 불가능 | +| null Value | 가능 | 불가능 | +| 동시 수정 | 위험 | 지원 | +| 일반 단일 Thread | 적합 | 필요 이상일 수 있음 | +| 멀티 Thread 공유 Map | 부적합 | 적합 | + +## 34. 자료구조 전체 시간복잡도 정리 + +| 자료구조 | 접근 | 검색 | 삽입 | 삭제 | +|---|---|---|---|---| +| Array | O(1) | O(N) | O(N) | O(N) | +| LinkedList | O(N) | O(N) | O(1)* | O(1)* | +| Stack | O(N) | O(N) | O(1) | O(1) | +| Queue | O(N) | O(N) | O(1) | O(1) | +| Hash Table | - | 평균 O(1) | 평균 O(1) | 평균 O(1) | +| BST | 평균 O(log N) | 평균 O(log N) | 평균 O(log N) | 평균 O(log N) | +| Balanced BST | O(log N) | O(log N) | O(log N) | O(log N) | +| Heap | - | O(N) | O(log N) | O(log N) | + + + +## 35. Java Collection 선택 기준 + +``` +순서가 필요한가? +│ +├─ YES +│ │ +│ ├─ index 접근이 중요한가? +│ │ └─ ArrayList +│ │ +│ └─ Queue / Stack인가? +│ └─ ArrayDeque +│ +└─ NO + │ + ├─ Key-Value인가? + │ │ + │ ├─ 일반 → HashMap + │ ├─ 정렬 → TreeMap + │ └─ 멀티스레드 → ConcurrentHashMap + │ + └─ 중복 제거가 필요한가? + │ + ├─ 일반 → HashSet + └─ 정렬 → TreeSet +``` + +## 36. 백엔드 개발자 관점 + + + +**ArrayList → 내부 배열 → Resize → Amortized O(1)** + +``` +add() + ↓ +공간 있음 → O(1) + +공간 없음 + ↓ +배열 확장 + ↓ +복사 O(N) + +하지만 평균적으로 Amortized O(1) +``` + +**HashMap → Hash → Bucket → Collision → LinkedList → Red-Black Tree** + +``` +key + ↓ +hashCode() + ↓ +hash + ↓ +bucket + ↓ +Node + ↓ collision +LinkedList + ↓ collision 증가 +Red-Black Tree +``` + +**equals/hashCode → HashMap/HashSet 동작과 연결** + +``` +equals가 같다면 hashCode도 같아야 함 +``` + +**HashMap → Thread Safe X → ConcurrentHashMap** + +``` +HashMap +→ 단일 Thread + +ConcurrentHashMap +→ 공유 데이터 + Multi Thread +``` + +**Heap → PriorityQueue** + +``` +우선순위 기반 작업 +→ Heap +→ PriorityQueue +``` + +**Queue → 비동기 백엔드 처리** + +``` +API 요청 + ↓ +Message Queue + ↓ +Worker + ↓ +DB / 외부 API +``` + +## 37. 면접 예상 질문?? +1. Array와 LinkedList의 차이는? +2. Array의 index 조회가 왜 O(1)인가? +3. LinkedList의 삽입이 정말 항상 O(1)인가? +4. Stack과 Queue의 차이는? +5. DFS와 BFS는 각각 어떤 자료구조를 사용하나? +6. Hash Table이 평균 O(1)인 이유는? +7. Hash Collision이 발생하면 어떻게 처리하나? +8. hashCode()와 equals()의 관계는? +9. HashMap에서 hashCode()만 비교하지 않고 equals()도 사용하는 이유는? +10. HashMap 내부 구조 설명. +11. Java 8에서 HashMap의 충돌 처리 방식이 어떻게 개선됐나? +12. Load Factor란? +13. HashMap Resize는 언제 발생하나? +14. HashMap과 ConcurrentHashMap의 차이는? +15. ConcurrentHashMap은 어떻게 Thread Safety를 보장하나? +16. ConcurrentHashMap이 null을 허용하지 않는 이유는? +17. Binary Tree와 BST는 어떻게 다른가? +18. BST의 탐색이 항상 O(log N)인가? +19. Red-Black Tree를 사용하는 이유는? +20. Heap과 BST의 차이는? +21. PriorityQueue는 내부적으로 어떤 자료구조를 사용하나? +22. ArrayList의 add()가 왜 Amortized O(1)인가? +23. HashSet은 내부적으로 어떻게 구현되어 있나? +24. Map은 왜 Collection을 상속하지 않나? + diff --git "a/02\354\243\274\354\260\250/weeks_02.md" "b/02\354\243\274\354\260\250/weeks_02.md" new file mode 100644 index 0000000..bbd3345 --- /dev/null +++ "b/02\354\243\274\354\260\250/weeks_02.md" @@ -0,0 +1,283 @@ +# 2주차 - OS - Process / Thread / 동시성 + +## 1. Process + +Process(프로세스)는 **실행 중인 프로그램**이다. 디스크에 있는 프로그램 파일이 메모리에 올라가 CPU에서 실행되면 프로세스가 된다. + +각 프로세스는 다른 프로세스와 분리된 가상 주소 공간을 가진다. + +``` +프로세스 A 프로세스 B +Code / Data / Heap / Stack Code / Data / Heap / Stack + 독립된 메모리 공간 독립된 메모리 공간 +``` + +### 프로세스의 메모리 구조 + +| 영역 | 역할 | +|---|---| +| Code | 실행할 기계어 코드 | +| Data | 전역 변수, static 변수 | +| Heap | `new` 등으로 동적 할당한 객체 | +| Stack | 함수 호출 정보, 지역 변수, 매개변수 | + +프로세스끼리는 기본적으로 메모리를 공유하지 않는다. 그래서 한 프로세스의 오류가 다른 프로세스에 미치는 영향을 줄일 수 있지만, 데이터를 주고받으려면 IPC(파이프, 소켓, 공유 메모리 등)가 필요하다. + +### 프로세스 상태 + +``` +new → ready → running → waiting → ready → ... → terminated +``` + +- **ready**: CPU를 배정받기를 기다리는 상태 +- **running**: CPU에서 실행 중인 상태 +- **waiting (blocked)**: I/O 완료, 락 획득 같은 이벤트를 기다리는 상태 + +`waiting` 상태는 CPU를 기다리는 것이 아니라 이벤트를 기다리므로, 이벤트가 끝나기 전에는 실행될 수 없다. + +## 2. Thread + +Thread(스레드)는 프로세스 안에서 실제로 실행되는 작업 단위다. 하나의 프로세스는 최소 하나의 스레드를 가지며 여러 스레드를 가질 수 있다. + +같은 프로세스의 스레드는 Code, Data, Heap을 공유하지만 각자 Stack과 레지스터 값(Program Counter 포함)은 따로 가진다. + +``` +하나의 프로세스 + ├─ 공유: Code, Data, Heap, 파일/소켓 같은 자원 + ├─ Thread 1: Stack 1, PC 1, Registers 1 + └─ Thread 2: Stack 2, PC 2, Registers 2 +``` + +| 구분 | Process | Thread | +|---|---|---| +| 메모리 | 다른 프로세스와 분리 | 같은 프로세스 내에서 대부분 공유 | +| 생성/전환 비용 | 상대적으로 큼 | 상대적으로 작음 | +| 통신 | IPC 필요 | 공유 메모리로 직접 접근 가능 | +| 장애 영향 | 대체로 프로세스 단위로 격리 | 잘못된 공유 메모리 접근이 전체 프로세스에 영향 | + +스레드는 공유 메모리 덕분에 빠르게 협업할 수 있지만, 동시에 같은 데이터를 다루면 동기화 문제가 생긴다. + +## 3. Context Switching + +CPU 코어는 한 순간에 하나의 스레드만 실행한다. 운영체제는 실행할 스레드를 바꿀 때 현재 실행 상태를 저장하고 다음 스레드의 상태를 복원하는데, 이를 **Context Switching(문맥 교환)** 이라고 한다. + +``` +Thread A 실행 + → A의 PC, 레지스터 등 저장 + → 스케줄러가 다음 대상 선택 + → Thread B의 PC, 레지스터 등 복원 +Thread B 실행 +``` + +문맥에는 다음에 실행할 명령어 위치(PC), CPU 레지스터 값, 스택 포인터, 스레드 상태 등이 포함된다. + +문맥 교환 자체는 사용자 기능을 수행하지 않는 비용이다. 스레드 수를 과도하게 늘리면 전환·스케줄링 비용이 커지고 캐시 효율도 나빠질 수 있다. 프로세스 전환은 주소 공간까지 바뀔 수 있어 일반적으로 스레드 전환보다 무겁다. + +## 4. 동시성(Concurrency)과 병렬성(Parallelism) + +### 동시성 + +동시성은 여러 작업이 **겹치는 시간 동안 진행되는 것처럼** 다루는 능력이다. CPU 코어가 하나여도 작업을 짧게 번갈아 실행하면 동시성을 만들 수 있다. + +``` +코어 1개: A → B → A → B → A +``` + +예: 웹 서버가 DB 응답을 기다리는 요청은 잠시 멈추고 그 사이 다른 요청을 처리한다. + +### 병렬성 + +병렬성은 여러 작업을 **실제로 같은 시각에** 실행하는 것이다. 보통 여러 CPU 코어가 필요하다. + +``` +코어 1: AAAAA +코어 2: BBBBB +``` + +| 구분 | 동시성 | 병렬성 | +|---|---|---| +| 핵심 | 여러 일을 잘 전환하며 처리 | 여러 일을 동시에 실행 | +| 코어 수 | 1개로도 가능 | 보통 2개 이상 필요 | +| 주된 목적 | 응답성, I/O 대기 활용 | 처리량, 계산 속도 향상 | + +동시성과 병렬성은 함께 사용될 수 있지만 같은 의미는 아니다. + +## 5. Race Condition + +Race Condition(경쟁 상태)은 여러 스레드가 공유 데이터를 동시에 읽고 수정할 때, 실행 순서에 따라 결과가 달라지는 문제다. + +`count++`는 하나의 원자적 연산이 아니다. 값을 읽고, 1을 더하고, 다시 저장하는 여러 단계로 수행된다. + +```java +count++; // read → add → write +``` + +두 스레드가 모두 `count == 0`을 읽은 뒤 각각 1을 저장하면, 두 번 증가했어야 하는 값이 1이 된다. + +``` +Thread A: count 읽기 (0) +Thread B: count 읽기 (0) +Thread A: 1 저장 +Thread B: 1 저장 +결과: 1 +``` + +### 해결 원칙 + +- 공유 가변 상태를 줄인다. +- 꼭 공유해야 하면 임계 구역(critical section)을 한 번에 한 스레드만 실행하게 한다. +- 단순한 카운터나 상태 변경에는 원자 타입을 사용한다. +- 여러 자료를 함께 변경해야 하면 락으로 변경 전체를 보호한다. + +## 6. Mutex와 Semaphore + +### Mutex + +Mutex(Mutual Exclusion)는 임계 구역에 **한 스레드만** 들어가게 하는 락이다. + +``` +lock 획득 → 공유 데이터 변경 → lock 해제 +``` + +락을 얻지 못한 스레드는 락이 풀릴 때까지 기다린다. 락을 얻은 코드에서 예외가 나도 락이 해제되도록 해야 한다. + +### Semaphore + +Semaphore는 허용 가능한 동시 접근 수를 카운터로 관리한다. + +- `acquire`: 카운터를 하나 감소시킨다. 0이면 기다린다. +- `release`: 카운터를 하나 증가시키고 기다리는 작업을 깨울 수 있다. + +초기값이 1인 Semaphore는 Mutex처럼 쓸 수 있다. 초기값이 N이면 DB 커넥션 N개처럼 제한된 자원을 N개까지 동시에 사용하게 할 수 있다. + +| 구분 | Mutex | Semaphore | +|---|---|---| +| 허용 진입 수 | 1개 | 0개 이상 N개 | +| 용도 | 공유 데이터 보호 | 제한된 자원 개수 관리 | + +### 주의할 문제 + +- **Deadlock**: 서로 가진 락이 풀리기를 영원히 기다림 +- **Starvation**: 특정 스레드가 계속 기회를 얻지 못함 +- **Lock contention**: 많은 스레드가 하나의 락을 두고 경쟁해 성능 저하 + +락을 여러 개 잡아야 하면 항상 같은 순서로 획득하고, 임계 구역을 짧게 유지하는 것이 기본 원칙이다. + +## 7. Java Thread + +Java에서는 `Thread`를 직접 만들 수 있지만, 작업과 실행 정책을 분리하기 위해 보통 `Runnable` 또는 `ExecutorService`를 사용한다. + +```java +Runnable task = () -> System.out.println("작업 실행"); +Thread thread = new Thread(task); +thread.start(); // run()을 직접 호출하면 새 스레드가 아니다. +``` + +`start()`는 새 실행 흐름을 만들고, `run()`은 그 메서드를 현재 스레드에서 일반 호출하는 것뿐이다. + +## 8. Thread Pool + +Thread Pool은 미리 만든 일정 수의 스레드가 작업 큐에서 일을 꺼내 처리하는 방식이다. + +``` +작업 제출 → 작업 큐 → Worker Thread 1 / 2 / 3 +``` + +매 요청마다 스레드를 새로 만들지 않아 생성 비용과 무제한 스레드 증가를 피할 수 있다. + +```java +ExecutorService executor = Executors.newFixedThreadPool(4); +executor.submit(() -> doWork()); +executor.shutdown(); +``` + +풀 크기가 너무 작으면 작업이 오래 대기하고, 너무 크면 문맥 교환과 메모리 사용량이 증가한다. CPU 연산 중심인지, I/O 대기 중심인지에 따라 적절한 크기가 다르다. + +## 9. synchronized와 Atomic + +### synchronized + +`synchronized`는 객체 모니터 락을 사용해 임계 구역을 한 스레드만 실행하도록 한다. 또한 락 해제 전의 변경이 이후 락 획득 스레드에게 보이도록 하는 메모리 가시성도 제공한다. + +```java +private int count; + +public synchronized void increment() { + count++; +} +``` + +### Atomic + +`AtomicInteger` 같은 원자 타입은 단일 값의 읽기-수정-쓰기를 락 없이 원자적으로 처리할 수 있다. + +```java +private final AtomicInteger count = new AtomicInteger(); + +public void increment() { + count.incrementAndGet(); +} +``` + +단, 잔액과 거래 내역처럼 **여러 값의 일관성을 함께 지켜야 하는 작업**은 Atomic 하나로 해결되지 않는다. 이때는 하나의 락으로 전체 변경을 보호하거나 트랜잭션 같은 더 적절한 경계를 사용해야 한다. + +## 10. WAS Thread + +WAS(Web Application Server)는 요청을 처리할 때 보통 스레드 풀의 워커 스레드를 사용한다. + +``` +HTTP 요청 → WAS의 요청 스레드 → Controller → Service → DB/외부 API → 응답 +``` + +요청 처리 스레드에서 DB나 외부 API를 오래 기다리면 해당 스레드는 다른 요청을 처리하지 못한다. 모든 요청이 같은 풀을 사용한다면, 느린 의존성 하나가 풀을 고갈시켜 서버 전체 응답 지연으로 이어질 수 있다. + +- 요청 스레드에서 긴 블로킹 작업을 피한다. +- DB 커넥션 풀과 WAS 스레드 풀 크기를 함께 고려한다. +- 타임아웃을 설정해 무한 대기를 막는다. +- 공유 싱글턴 객체에 요청별 상태를 보관하지 않는다. + +## 11. JavaScript Single Thread와 Web Worker + +브라우저의 JavaScript 실행은 일반적으로 하나의 메인 스레드에서 이벤트 루프를 통해 이루어진다. 긴 계산을 메인 스레드에서 수행하면 렌더링과 사용자 입력 처리도 함께 멈춘다. + +```javascript +// 긴 반복문이 메인 스레드를 점유하면 화면이 멈출 수 있다. +for (let i = 0; i < 1_000_000_000; i++) { + // expensive work +} +``` + +비동기 API가 있다고 해서 JavaScript 코드가 여러 줄에서 동시에 실행되는 것은 아니다. 네트워크나 타이머 작업은 브라우저가 처리하고, 완료 콜백은 이벤트 루프를 통해 메인 스레드에서 실행된다. + +### Web Worker + +Web Worker는 별도 스레드에서 JavaScript를 실행해 무거운 계산이 UI를 막지 않게 한다. + +```javascript +const worker = new Worker("worker.js"); +worker.postMessage({ numbers }); +worker.onmessage = (event) => console.log(event.data); +``` + +Worker는 DOM에 직접 접근할 수 없다. 메인 스레드와는 `postMessage`로 데이터를 주고받으며, 전달 방식에 따라 데이터 복사 비용이 생길 수 있다. + +## 12. 브라우저 Multi-Process 구조 + +현대 브라우저는 안정성과 보안을 위해 여러 프로세스를 사용한다. 구현은 브라우저마다 다르지만 보통 다음 역할이 분리된다. + +| 프로세스/구성 요소 | 역할 | +|---|---| +| Browser Process | 탭, 주소창, 권한, 전체 브라우저 관리 | +| Renderer Process | HTML/CSS/JS 처리, 렌더링 | +| GPU Process | 그래픽 작업 | +| Network Process | 네트워크 요청 처리 | + +탭 또는 사이트별로 렌더러 프로세스를 분리하면 한 페이지의 충돌이 다른 페이지에 미치는 영향을 줄이고, 사이트 간 데이터를 분리하는 보안 경계로도 활용할 수 있다. 대신 프로세스마다 메모리를 사용하므로 탭이 많을수록 자원 사용량도 늘어난다. + +## 정리 + +1. 프로세스는 독립된 실행 환경이고, 스레드는 그 안의 실행 단위다. +2. 스레드는 메모리를 공유하므로 빠르지만 Race Condition을 조심해야 한다. +3. 동시성은 일을 번갈아 진행하는 방식이고, 병렬성은 실제로 동시에 실행하는 방식이다. +4. 공유 상태는 최소화하고, 필요한 경우 Mutex, `synchronized`, Atomic 같은 도구로 보호한다. +5. 서버와 브라우저 모두 제한된 스레드·프로세스 자원을 다루므로, 긴 블로킹 작업과 무제한 생성은 피해야 한다.