From 8e54bf96ca3926bd1e6945b086a04c22a377fdf3 Mon Sep 17 00:00:00 2001 From: 1stevering <1stevering@naver.com> Date: Sat, 4 Apr 2026 19:26:26 +0900 Subject: [PATCH 1/5] Add hardcoded fallback data so content shows even when backend is down Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/generate-data.js | 115 ++ frontend/src/api.js | 1 + frontend/src/hardcodedData.js | 1513 +++++++++++++++++++++++++++ frontend/src/pages/CategoryPage.jsx | 10 +- frontend/src/pages/Home.jsx | 16 +- frontend/src/pages/QuestionPage.jsx | 6 +- frontend/src/pages/StatsPage.jsx | 19 +- 7 files changed, 1671 insertions(+), 9 deletions(-) create mode 100644 frontend/generate-data.js create mode 100644 frontend/src/hardcodedData.js diff --git a/frontend/generate-data.js b/frontend/generate-data.js new file mode 100644 index 0000000..852f87c --- /dev/null +++ b/frontend/generate-data.js @@ -0,0 +1,115 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const contentDir = path.resolve(__dirname, '..', 'content'); + +const categories = [ + { id: 1, name: '데이터베이스', slug: 'database', description: 'Database concepts and SQL', displayOrder: 1, iconEmoji: '🗄️' }, + { id: 2, name: '자료구조', slug: 'data-structure', description: 'Data structures', displayOrder: 2, iconEmoji: '🏗️' }, + { id: 3, name: '운영체제', slug: 'operating-system', description: 'Operating system concepts', displayOrder: 3, iconEmoji: '🖥️' }, + { id: 4, name: '네트워크', slug: 'network', description: 'Network and protocols', displayOrder: 4, iconEmoji: '🌐' }, + { id: 5, name: '알고리즘', slug: 'algorithm', description: 'Algorithms and problem solving', displayOrder: 5, iconEmoji: '⚡' }, + { id: 6, name: '소프트웨어공학', slug: 'software-engineering', description: 'Software engineering principles', displayOrder: 6, iconEmoji: '📐' }, + { id: 7, name: '스프링', slug: 'spring', description: 'Spring and Spring Boot', displayOrder: 7, iconEmoji: '🌱' }, + { id: 8, name: '자바', slug: 'java', description: 'Java programming language', displayOrder: 8, iconEmoji: '☕' }, + { id: 9, name: '쿠버네티스', slug: 'kubernetes', description: 'Kubernetes container orchestration', displayOrder: 9, iconEmoji: '⎈' }, + { id: 10, name: '도커', slug: 'docker', description: 'Docker containerization', displayOrder: 10, iconEmoji: '🐳' }, + { id: 11, name: 'CI/CD', slug: 'cicd', description: 'Continuous Integration and Deployment', displayOrder: 11, iconEmoji: '🔄' }, +]; + +function parseFrontmatter(content) { + const fm = {}; + if (!content.startsWith('---')) return fm; + const end = content.indexOf('---', 3); + if (end === -1) return fm; + const lines = content.substring(3, end).trim().split('\n'); + for (const line of lines) { + const idx = line.indexOf(':'); + if (idx > 0) { + const key = line.substring(0, idx).trim(); + let val = line.substring(idx + 1).trim(); + if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1); + fm[key] = val; + } + } + return fm; +} + +function extractBody(content) { + if (!content.startsWith('---')) return content; + const end = content.indexOf('---', 3); + return end === -1 ? content : content.substring(end + 3).trim(); +} + +function extractSection(body, sectionName) { + const marker = '## ' + sectionName; + let start = body.indexOf(marker); + if (start === -1) return ''; + start = body.indexOf('\n', start); + if (start === -1) return ''; + start++; + const end = body.indexOf('\n## ', start); + return end === -1 ? body.substring(start).trim() : body.substring(start, end).trim(); +} + +const catSlugToId = {}; +categories.forEach(c => { catSlugToId[c.slug] = c.id; }); + +const catSlugToName = {}; +categories.forEach(c => { catSlugToName[c.slug] = c.name; }); + +const questions = []; +let questionId = 1; + +const dirs = fs.readdirSync(contentDir).filter(d => fs.statSync(path.join(contentDir, d)).isDirectory()); + +for (const dir of dirs.sort()) { + const dirPath = path.join(contentDir, dir); + const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md')).sort(); + + for (const file of files) { + const filePath = path.join(dirPath, file); + const content = fs.readFileSync(filePath, 'utf-8'); + const fm = parseFrontmatter(content); + const body = extractBody(content); + + if (!fm.title) continue; + + const categorySlug = fm.category || 'database'; + const difficulty = (fm.difficulty || 'BASIC').toUpperCase(); + const tags = fm.tags || ''; + + let questionContent = extractSection(body, '질문'); + let answerContent = extractSection(body, '답변'); + if (!questionContent) questionContent = body; + if (!answerContent) answerContent = body; + + questions.push({ + id: questionId, + title: fm.title, + content: questionContent, + answer: answerContent, + difficulty, + tags, + categorySlug, + categoryName: catSlugToName[categorySlug] || categorySlug, + categoryId: catSlugToId[categorySlug] || 1, + studyCount: 0, + bookmarked: false, + }); + questionId++; + } +} + +const output = `// Auto-generated hardcoded data from content/ markdown files +// Generated: ${new Date().toISOString()} + +export const hardcodedCategories = ${JSON.stringify(categories, null, 2)}; + +export const hardcodedQuestions = ${JSON.stringify(questions, null, 2)}; +`; + +fs.writeFileSync(path.join(__dirname, 'src', 'hardcodedData.js'), output, 'utf-8'); +console.log(`Generated ${categories.length} categories and ${questions.length} questions.`); diff --git a/frontend/src/api.js b/frontend/src/api.js index 7a7e7fe..5cec482 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -2,6 +2,7 @@ import axios from 'axios'; const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'https://cs-study-backend.onrender.com/api/v1', + timeout: 5000, }); export default api; diff --git a/frontend/src/hardcodedData.js b/frontend/src/hardcodedData.js new file mode 100644 index 0000000..befd439 --- /dev/null +++ b/frontend/src/hardcodedData.js @@ -0,0 +1,1513 @@ +// Auto-generated hardcoded data from content/ markdown files +// Generated: 2026-04-04T10:20:02.477Z + +export const hardcodedCategories = [ + { + "id": 1, + "name": "데이터베이스", + "slug": "database", + "description": "Database concepts and SQL", + "displayOrder": 1, + "iconEmoji": "🗄️" + }, + { + "id": 2, + "name": "자료구조", + "slug": "data-structure", + "description": "Data structures", + "displayOrder": 2, + "iconEmoji": "🏗️" + }, + { + "id": 3, + "name": "운영체제", + "slug": "operating-system", + "description": "Operating system concepts", + "displayOrder": 3, + "iconEmoji": "🖥️" + }, + { + "id": 4, + "name": "네트워크", + "slug": "network", + "description": "Network and protocols", + "displayOrder": 4, + "iconEmoji": "🌐" + }, + { + "id": 5, + "name": "알고리즘", + "slug": "algorithm", + "description": "Algorithms and problem solving", + "displayOrder": 5, + "iconEmoji": "⚡" + }, + { + "id": 6, + "name": "소프트웨어공학", + "slug": "software-engineering", + "description": "Software engineering principles", + "displayOrder": 6, + "iconEmoji": "📐" + }, + { + "id": 7, + "name": "스프링", + "slug": "spring", + "description": "Spring and Spring Boot", + "displayOrder": 7, + "iconEmoji": "🌱" + }, + { + "id": 8, + "name": "자바", + "slug": "java", + "description": "Java programming language", + "displayOrder": 8, + "iconEmoji": "☕" + }, + { + "id": 9, + "name": "쿠버네티스", + "slug": "kubernetes", + "description": "Kubernetes container orchestration", + "displayOrder": 9, + "iconEmoji": "⎈" + }, + { + "id": 10, + "name": "도커", + "slug": "docker", + "description": "Docker containerization", + "displayOrder": 10, + "iconEmoji": "🐳" + }, + { + "id": 11, + "name": "CI/CD", + "slug": "cicd", + "description": "Continuous Integration and Deployment", + "displayOrder": 11, + "iconEmoji": "🔄" + } +]; + +export const hardcodedQuestions = [ + { + "id": 1, + "title": "거품 정렬(Bubble Sort)", + "content": "버블 정렬(Bubble Sort)의 동작 원리와 시간 복잡도에 대해 설명해주세요.", + "answer": "버블 정렬은 인접한 두 요소를 비교하여 순서가 맞지 않으면 교환하는 과정을 반복하는 정렬 알고리즘입니다. 가장 큰 요소가 거품처럼 배열의 끝으로 이동하는 모습에서 이름이 유래했습니다.\r\n\r\n### 동작 과정\r\n\r\n```\r\n초기: [5, 3, 8, 4, 2]\r\n\r\n1회전: 가장 큰 값 8이 끝으로 이동\r\n[3, 5, 8, 4, 2] → [3, 5, 4, 8, 2] → [3, 5, 4, 2, 8]\r\n\r\n2회전: 5가 제자리로\r\n[3, 5, 4, 2, 8] → [3, 4, 5, 2, 8] → [3, 4, 2, 5, 8]\r\n\r\n3회전: 4가 제자리로\r\n[3, 4, 2, 5, 8] → [3, 2, 4, 5, 8]\r\n\r\n4회전: 3이 제자리로\r\n[2, 3, 4, 5, 8] → 정렬 완료!\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\nvoid bubbleSort(int[] arr) {\r\n int n = arr.length;\r\n for (int i = 0; i < n - 1; i++) {\r\n boolean swapped = false; // 최적화: 교환 여부 체크\r\n for (int j = 0; j < n - 1 - i; j++) {\r\n if (arr[j] > arr[j + 1]) {\r\n // 인접 요소 교환\r\n int temp = arr[j];\r\n arr[j] = arr[j + 1];\r\n arr[j + 1] = temp;\r\n swapped = true;\r\n }\r\n }\r\n if (!swapped) break; // 교환이 없으면 이미 정렬된 상태\r\n }\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 경우 | 시간 복잡도 | 설명 |\r\n|------|-----------|------|\r\n| 최선 | O(n) | 이미 정렬된 경우 (최적화 적용 시) |\r\n| 평균 | O(n^2) | |\r\n| 최악 | O(n^2) | 역순 정렬된 경우 |\r\n\r\n- 공간 복잡도: O(1) (제자리 정렬)\r\n- 안정 정렬: 동일한 값의 상대적 순서가 유지됨\r\n\r\n### 장단점\r\n**장점:** 구현이 매우 간단, 안정 정렬, 추가 메모리 불필요\r\n**단점:** O(n^2)으로 비효율적, 실무에서 거의 사용되지 않음\r\n\r\n### 면접 팁\r\n버블 정렬은 교육적 목적으로 중요하지만 실무에서는 사용하지 않습니다. 최적화(early termination)를 적용하면 이미 정렬된 배열에 대해 O(n)이 된다는 점을 언급하세요.", + "difficulty": "BASIC", + "tags": "거품 정렬, 버블 정렬, 정렬, 비교 정렬, 안정 정렬", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 2, + "title": "선택 정렬(Selection Sort)", + "content": "선택 정렬(Selection Sort)의 동작 원리와 특징에 대해 설명해주세요.", + "answer": "선택 정렬은 배열에서 가장 작은(또는 큰) 요소를 선택하여 정렬되지 않은 부분의 첫 번째 위치에 놓는 과정을 반복하는 알고리즘입니다.\r\n\r\n### 동작 과정\r\n\r\n```\r\n초기: [64, 25, 12, 22, 11]\r\n\r\n1회전: 최소값 11 선택 → 첫 번째와 교환\r\n[11, 25, 12, 22, 64]\r\n\r\n2회전: 나머지에서 최소값 12 선택\r\n[11, 12, 25, 22, 64]\r\n\r\n3회전: 나머지에서 최소값 22 선택\r\n[11, 12, 22, 25, 64]\r\n\r\n4회전: 나머지에서 최소값 25 선택\r\n[11, 12, 22, 25, 64] → 정렬 완료\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\nvoid selectionSort(int[] arr) {\r\n int n = arr.length;\r\n for (int i = 0; i < n - 1; i++) {\r\n int minIdx = i;\r\n // 최소값의 인덱스 찾기\r\n for (int j = i + 1; j < n; j++) {\r\n if (arr[j] < arr[minIdx]) {\r\n minIdx = j;\r\n }\r\n }\r\n // 교환\r\n int temp = arr[i];\r\n arr[i] = arr[minIdx];\r\n arr[minIdx] = temp;\r\n }\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 경우 | 시간 복잡도 |\r\n|------|-----------|\r\n| 최선 | O(n^2) |\r\n| 평균 | O(n^2) |\r\n| 최악 | O(n^2) |\r\n\r\n- 공간 복잡도: O(1)\r\n- **불안정 정렬**: 동일한 값의 순서가 바뀔 수 있음\r\n- 비교 횟수: n(n-1)/2 (항상 동일)\r\n- 교환 횟수: O(n) (버블 정렬보다 적음)\r\n\r\n### 장단점\r\n**장점:** 구현이 단순, 교환 횟수가 적어 교환 비용이 클 때 유리\r\n**단점:** 항상 O(n^2), 이미 정렬되어 있어도 동일한 비교 수행\r\n\r\n### 면접 팁\r\n선택 정렬의 핵심 특징은 교환 횟수가 O(n)으로 최소라는 점입니다. 메모리 쓰기가 비용이 큰 환경에서는 버블 정렬보다 유리할 수 있습니다. 하지만 어떤 경우에도 O(n^2)이라는 한계가 있습니다.", + "difficulty": "BASIC", + "tags": "선택 정렬, 정렬, 비교 정렬, 제자리 정렬", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 3, + "title": "삽입 정렬(Insertion Sort)", + "content": "삽입 정렬(Insertion Sort)의 동작 원리와 특징에 대해 설명해주세요.", + "answer": "삽입 정렬은 배열의 각 요소를 이미 정렬된 부분의 적절한 위치에 삽입하는 방식으로 동작합니다. 카드 게임에서 손에 들고 있는 카드를 정렬하는 방식과 유사합니다.\r\n\r\n### 동작 과정\r\n\r\n```\r\n초기: [5, 3, 4, 1, 2]\r\n\r\n1회전: 3을 삽입 → [3, 5, 4, 1, 2]\r\n2회전: 4를 삽입 → [3, 4, 5, 1, 2]\r\n3회전: 1을 삽입 → [1, 3, 4, 5, 2]\r\n4회전: 2를 삽입 → [1, 2, 3, 4, 5] 완료!\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\nvoid insertionSort(int[] arr) {\r\n for (int i = 1; i < arr.length; i++) {\r\n int key = arr[i];\r\n int j = i - 1;\r\n\r\n // key보다 큰 요소들을 오른쪽으로 이동\r\n while (j >= 0 && arr[j] > key) {\r\n arr[j + 1] = arr[j];\r\n j--;\r\n }\r\n arr[j + 1] = key;\r\n }\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 경우 | 시간 복잡도 | 설명 |\r\n|------|-----------|------|\r\n| 최선 | O(n) | 이미 정렬된 경우 |\r\n| 평균 | O(n^2) | |\r\n| 최악 | O(n^2) | 역순 정렬된 경우 |\r\n\r\n- 공간 복잡도: O(1)\r\n- 안정 정렬\r\n\r\n### 장단점\r\n**장점:**\r\n- 거의 정렬된 데이터에서 O(n)에 가까운 성능\r\n- 구현이 간단하고 안정 정렬\r\n- 데이터가 추가될 때마다 정렬 가능 (온라인 알고리즘)\r\n- 작은 데이터셋에서 효율적\r\n\r\n**단점:**\r\n- 평균/최악 O(n^2)\r\n\r\n### 실무 활용\r\n- Tim Sort(Java, Python의 기본 정렬)는 내부적으로 작은 부분 배열에 삽입 정렬을 사용합니다\r\n- 데이터가 거의 정렬되어 있을 때 퀵 정렬보다 빠를 수 있습니다\r\n\r\n### 면접 팁\r\n삽입 정렬은 O(n^2) 정렬 알고리즘 중 실무에서 가장 많이 활용됩니다. Tim Sort가 삽입 정렬을 사용하는 이유를 설명할 수 있으면 좋습니다. 또한 이진 삽입 정렬(Binary Insertion Sort)도 언급할 수 있습니다.", + "difficulty": "BASIC", + "tags": "삽입 정렬, 정렬, 안정 정렬, 거의 정렬된 배열", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 4, + "title": "퀵 정렬(Quick Sort)", + "content": "퀵 정렬(Quick Sort)의 동작 원리와 시간 복잡도에 대해 설명해주세요.", + "answer": "퀵 정렬은 피벗(Pivot)을 기준으로 배열을 두 부분으로 분할한 후, 각 부분을 재귀적으로 정렬하는 분할 정복 알고리즘입니다. 평균적으로 가장 빠른 정렬 알고리즘 중 하나입니다.\r\n\r\n### 동작 과정\r\n\r\n```\r\n[5, 3, 8, 4, 2, 7, 1, 6] 피벗 = 5\r\n\r\n1. 파티션: 피벗보다 작은 값 | 피벗 | 피벗보다 큰 값\r\n [3, 4, 2, 1] [5] [8, 7, 6]\r\n\r\n2. 왼쪽 재귀: [3, 4, 2, 1] 피벗 = 3\r\n [2, 1] [3] [4]\r\n\r\n3. 오른쪽 재귀: [8, 7, 6] 피벗 = 8\r\n [7, 6] [8]\r\n\r\n4. 계속 재귀... → [1, 2, 3, 4, 5, 6, 7, 8]\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\nvoid quickSort(int[] arr, int low, int high) {\r\n if (low < high) {\r\n int pivotIndex = partition(arr, low, high);\r\n quickSort(arr, low, pivotIndex - 1); // 왼쪽 부분\r\n quickSort(arr, pivotIndex + 1, high); // 오른쪽 부분\r\n }\r\n}\r\n\r\nint partition(int[] arr, int low, int high) {\r\n int pivot = arr[high]; // 마지막 요소를 피벗으로\r\n int i = low - 1;\r\n\r\n for (int j = low; j < high; j++) {\r\n if (arr[j] <= pivot) {\r\n i++;\r\n swap(arr, i, j);\r\n }\r\n }\r\n swap(arr, i + 1, high);\r\n return i + 1;\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 경우 | 시간 복잡도 | 설명 |\r\n|------|-----------|------|\r\n| 최선 | O(n log n) | 균등 분할 |\r\n| 평균 | O(n log n) | |\r\n| 최악 | O(n^2) | 이미 정렬된 배열에서 첫/마지막 피벗 |\r\n\r\n- 공간 복잡도: O(log n) (재귀 스택)\r\n- 불안정 정렬\r\n\r\n### 최악의 경우 방지\r\n\r\n**1. 랜덤 피벗 선택**\r\n```java\r\nint randomIndex = low + random.nextInt(high - low + 1);\r\nswap(arr, randomIndex, high);\r\n```\r\n\r\n**2. Median of Three**\r\n첫 번째, 중간, 마지막 요소의 중앙값을 피벗으로 선택합니다.\r\n\r\n**3. 작은 배열에서 삽입 정렬로 전환**\r\n요소가 10~20개 이하일 때 삽입 정렬이 더 효율적입니다.\r\n\r\n### 퀵 정렬의 장단점\r\n**장점:** 평균 O(n log n), 캐시 효율이 좋음 (제자리 정렬), 실무에서 가장 빠른 정렬 중 하나\r\n**단점:** 최악 O(n^2), 불안정 정렬\r\n\r\n### 면접 팁\r\n퀵 정렬이 병합 정렬보다 실무에서 빠른 이유(캐시 지역성, 상수 계수가 작음)를 설명할 수 있으면 좋습니다. 최악의 경우를 방지하는 방법(랜덤 피벗, Median of Three)도 중요합니다.", + "difficulty": "INTERMEDIATE", + "tags": "퀵 정렬, 분할 정복, 피벗, 파티션, 불안정 정렬", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 5, + "title": "병합 정렬(Merge Sort)", + "content": "병합 정렬(Merge Sort)의 동작 원리와 특징에 대해 설명해주세요.", + "answer": "병합 정렬은 배열을 반으로 나누고, 각각을 재귀적으로 정렬한 뒤, 두 정렬된 배열을 병합하는 분할 정복 알고리즘입니다.\r\n\r\n### 동작 과정\r\n\r\n```\r\n[38, 27, 43, 3, 9, 82, 10]\r\n\r\n분할:\r\n[38, 27, 43, 3] | [9, 82, 10]\r\n[38, 27] | [43, 3] | [9, 82] | [10]\r\n[38] [27] [43] [3] [9] [82] [10]\r\n\r\n병합:\r\n[27, 38] [3, 43] [9, 82] [10]\r\n[3, 27, 38, 43] [9, 10, 82]\r\n[3, 9, 10, 27, 38, 43, 82]\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\nvoid mergeSort(int[] arr, int left, int right) {\r\n if (left < right) {\r\n int mid = (left + right) / 2;\r\n mergeSort(arr, left, mid); // 왼쪽 정렬\r\n mergeSort(arr, mid + 1, right); // 오른쪽 정렬\r\n merge(arr, left, mid, right); // 병합\r\n }\r\n}\r\n\r\nvoid merge(int[] arr, int left, int mid, int right) {\r\n int[] temp = new int[right - left + 1];\r\n int i = left, j = mid + 1, k = 0;\r\n\r\n while (i <= mid && j <= right) {\r\n if (arr[i] <= arr[j]) {\r\n temp[k++] = arr[i++];\r\n } else {\r\n temp[k++] = arr[j++];\r\n }\r\n }\r\n\r\n while (i <= mid) temp[k++] = arr[i++];\r\n while (j <= right) temp[k++] = arr[j++];\r\n\r\n System.arraycopy(temp, 0, arr, left, temp.length);\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 경우 | 시간 복잡도 |\r\n|------|-----------|\r\n| 최선 | O(n log n) |\r\n| 평균 | O(n log n) |\r\n| 최악 | O(n log n) |\r\n\r\n- 공간 복잡도: O(n) (임시 배열 필요)\r\n- 안정 정렬\r\n- 항상 O(n log n) 보장\r\n\r\n### 장단점\r\n**장점:**\r\n- 항상 O(n log n) 보장 (최악의 경우가 없음)\r\n- 안정 정렬\r\n- 연결 리스트 정렬에 효율적 (추가 메모리 불필요)\r\n- 외부 정렬(External Sort)에 적합\r\n\r\n**단점:**\r\n- O(n) 추가 메모리 필요\r\n- 퀵 정렬보다 캐시 효율이 낮음\r\n\r\n### 활용\r\n- Java의 `Collections.sort()`, `Arrays.sort()`(객체 배열)는 Tim Sort(병합 정렬 + 삽입 정렬)를 사용\r\n- 외부 정렬: 메모리에 다 올릴 수 없는 대용량 데이터 정렬\r\n\r\n### 면접 팁\r\n병합 정렬은 \"항상 O(n log n)을 보장하는 안정 정렬\"이라는 점이 핵심입니다. 퀵 정렬과의 비교에서 각각의 장단점을 논리적으로 설명하세요.", + "difficulty": "INTERMEDIATE", + "tags": "병합 정렬, 분할 정복, 안정 정렬, O(n log n), 외부 정렬", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 6, + "title": "힙 정렬(Heap Sort)", + "content": "힙 정렬(Heap Sort)의 동작 원리와 특징에 대해 설명해주세요.", + "answer": "힙 정렬은 최대 힙(Max Heap) 자료구조를 이용한 정렬 알고리즘입니다. 배열을 최대 힙으로 구성한 뒤, 루트(최대값)를 반복적으로 추출하여 정렬합니다.\r\n\r\n### 동작 과정\r\n\r\n```\r\n1. 배열을 최대 힙으로 구성 (Build Max Heap)\r\n2. 루트(최대값)를 배열 끝과 교환\r\n3. 힙 크기를 줄이고 다시 heapify\r\n4. 2-3을 반복\r\n\r\n배열: [4, 10, 3, 5, 1]\r\n\r\nStep 1: Build Max Heap → [10, 5, 3, 4, 1]\r\nStep 2: 10과 1 교환 → [1, 5, 3, 4, | 10] → heapify → [5, 4, 3, 1, | 10]\r\nStep 3: 5와 1 교환 → [1, 4, 3, | 5, 10] → heapify → [4, 1, 3, | 5, 10]\r\nStep 4: 4와 3 교환 → [3, 1, | 4, 5, 10] → heapify → [3, 1, | 4, 5, 10]\r\nStep 5: 3과 1 교환 → [1, | 3, 4, 5, 10]\r\n결과: [1, 3, 4, 5, 10]\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\nvoid heapSort(int[] arr) {\r\n int n = arr.length;\r\n\r\n // 1. Build Max Heap: O(n)\r\n for (int i = n / 2 - 1; i >= 0; i--) {\r\n heapify(arr, n, i);\r\n }\r\n\r\n // 2. 루트를 끝으로 보내고 heapify 반복\r\n for (int i = n - 1; i > 0; i--) {\r\n swap(arr, 0, i); // 루트(최대값)를 끝으로\r\n heapify(arr, i, 0); // 줄어든 힙에서 heapify\r\n }\r\n}\r\n\r\nvoid heapify(int[] arr, int n, int i) {\r\n int largest = i;\r\n int left = 2 * i + 1;\r\n int right = 2 * i + 2;\r\n\r\n if (left < n && arr[left] > arr[largest]) largest = left;\r\n if (right < n && arr[right] > arr[largest]) largest = right;\r\n\r\n if (largest != i) {\r\n swap(arr, i, largest);\r\n heapify(arr, n, largest); // 재귀적 heapify\r\n }\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 경우 | 시간 복잡도 |\r\n|------|-----------|\r\n| 최선 | O(n log n) |\r\n| 평균 | O(n log n) |\r\n| 최악 | O(n log n) |\r\n\r\n- 공간 복잡도: O(1) (제자리 정렬)\r\n- 불안정 정렬\r\n\r\n### 장단점\r\n**장점:** 항상 O(n log n) 보장, 추가 메모리 불필요 (제자리)\r\n**단점:** 불안정 정렬, 캐시 효율이 낮음 (배열 접근 패턴이 불규칙)\r\n\r\n### 정렬 알고리즘 비교\r\n\r\n| 알고리즘 | 평균 | 최악 | 공간 | 안정 |\r\n|---------|------|------|------|------|\r\n| 퀵 정렬 | O(n log n) | O(n^2) | O(log n) | X |\r\n| 병합 정렬 | O(n log n) | O(n log n) | O(n) | O |\r\n| 힙 정렬 | O(n log n) | O(n log n) | O(1) | X |\r\n\r\n### 면접 팁\r\n힙 정렬은 \"최악 O(n log n) + 추가 메모리 O(1)\"이라는 고유한 장점이 있습니다. 그러나 실무에서는 캐시 효율이 좋은 퀵 정렬이나 안정 정렬인 병합 정렬이 더 많이 사용됩니다.", + "difficulty": "INTERMEDIATE", + "tags": "힙 정렬, 힙, 최대 힙, O(n log n), 불안정 정렬", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 7, + "title": "기수 정렬(Radix Sort)", + "content": "기수 정렬(Radix Sort)의 동작 원리와 특징에 대해 설명해주세요.", + "answer": "기수 정렬은 데이터의 각 자릿수를 기준으로 정렬하는 비비교(Non-comparison) 정렬 알고리즘입니다. 원소 간 직접 비교하지 않고 각 자릿수별로 카운팅 정렬을 적용합니다.\r\n\r\n### 동작 과정 (LSD - Least Significant Digit)\r\n\r\n```\r\n입력: [170, 45, 75, 90, 802, 24, 2, 66]\r\n\r\n1의 자리로 정렬: [170, 90, 802, 2, 24, 45, 75, 66]\r\n10의 자리로 정렬: [802, 2, 24, 45, 66, 170, 75, 90]\r\n100의 자리로 정렬: [2, 24, 45, 66, 75, 90, 170, 802]\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\nvoid radixSort(int[] arr) {\r\n int max = Arrays.stream(arr).max().getAsInt();\r\n\r\n // 각 자릿수에 대해 카운팅 정렬 수행\r\n for (int exp = 1; max / exp > 0; exp *= 10) {\r\n countingSortByDigit(arr, exp);\r\n }\r\n}\r\n\r\nvoid countingSortByDigit(int[] arr, int exp) {\r\n int n = arr.length;\r\n int[] output = new int[n];\r\n int[] count = new int[10]; // 0~9\r\n\r\n // 해당 자릿수의 등장 횟수 카운트\r\n for (int num : arr) {\r\n int digit = (num / exp) % 10;\r\n count[digit]++;\r\n }\r\n\r\n // 누적 합\r\n for (int i = 1; i < 10; i++) {\r\n count[i] += count[i - 1];\r\n }\r\n\r\n // 뒤에서부터 배치 (안정성 유지)\r\n for (int i = n - 1; i >= 0; i--) {\r\n int digit = (arr[i] / exp) % 10;\r\n output[--count[digit]] = arr[i];\r\n }\r\n\r\n System.arraycopy(output, 0, arr, 0, n);\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n- **O(d * (n + k))**: d = 최대 자릿수, n = 원소 수, k = 기수(진법, 보통 10)\r\n- 자릿수가 상수이면 사실상 O(n)\r\n\r\n### LSD vs MSD\r\n- **LSD (Least Significant Digit)**: 가장 낮은 자릿수부터 정렬. 구현이 간단.\r\n- **MSD (Most Significant Digit)**: 가장 높은 자릿수부터 정렬. 중간에 멈출 수 있어 효율적일 수 있음.\r\n\r\n### 장단점\r\n**장점:** 비교 정렬의 O(n log n) 하한을 돌파, 안정 정렬, 데이터 범위가 제한적일 때 매우 빠름\r\n**단점:** 추가 메모리 필요, 자릿수가 많으면 비효율적, 음수나 실수 처리가 복잡\r\n\r\n### 면접 팁\r\n비교 기반 정렬의 이론적 하한은 O(n log n)이지만, 기수 정렬은 비비교 정렬이므로 이 한계를 넘을 수 있다는 점을 설명하세요. 단, 범용성이 부족하여 특정 조건에서만 효율적입니다.", + "difficulty": "INTERMEDIATE", + "tags": "기수 정렬, 비비교 정렬, O(nk), 안정 정렬, 자릿수", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 8, + "title": "계수 정렬(Counting Sort)", + "content": "계수 정렬(Counting Sort)의 동작 원리와 특징에 대해 설명해주세요.", + "answer": "계수 정렬(Counting Sort)은 각 원소의 등장 횟수를 카운팅하여 정렬하는 비비교 정렬 알고리즘입니다. 데이터의 범위가 제한적일 때 매우 효율적입니다.\r\n\r\n### 동작 과정\r\n\r\n```\r\n입력: [4, 2, 2, 8, 3, 3, 1] (범위: 1~8)\r\n\r\n1. 카운팅: count[1]=1, count[2]=2, count[3]=2, count[4]=1, count[8]=1\r\n2. 누적 합: count[1]=1, count[2]=3, count[3]=5, count[4]=6, count[8]=7\r\n3. 뒤에서부터 배치:\r\n arr[6]=1 → output[0]=1, count[1]=0\r\n arr[5]=3 → output[4]=3, count[3]=4\r\n ...\r\n4. 결과: [1, 2, 2, 3, 3, 4, 8]\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\nvoid countingSort(int[] arr) {\r\n int max = Arrays.stream(arr).max().getAsInt();\r\n int min = Arrays.stream(arr).min().getAsInt();\r\n int range = max - min + 1;\r\n\r\n int[] count = new int[range];\r\n int[] output = new int[arr.length];\r\n\r\n // 1. 각 원소 카운팅\r\n for (int num : arr) {\r\n count[num - min]++;\r\n }\r\n\r\n // 2. 누적 합 계산\r\n for (int i = 1; i < range; i++) {\r\n count[i] += count[i - 1];\r\n }\r\n\r\n // 3. 뒤에서부터 배치 (안정성 보장)\r\n for (int i = arr.length - 1; i >= 0; i--) {\r\n output[--count[arr[i] - min]] = arr[i];\r\n }\r\n\r\n System.arraycopy(output, 0, arr, 0, arr.length);\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n- **O(n + k)**: n = 원소 수, k = 데이터 범위\r\n- k가 n에 비해 매우 크면 비효율적\r\n\r\n### 장단점\r\n**장점:** O(n + k)로 매우 빠름, 안정 정렬, 구현이 비교적 간단\r\n**단점:** 데이터 범위(k)가 크면 메모리 낭비, 음수/실수 처리 복잡, 범위가 제한적이어야 함\r\n\r\n### 활용\r\n- 성적(0~100), 나이(0~150) 등 범위가 제한된 정수 데이터 정렬\r\n- 기수 정렬의 내부 정렬 알고리즘으로 사용\r\n\r\n### 면접 팁\r\n계수 정렬은 데이터 범위가 원소 수에 비해 크지 않을 때 효과적입니다. 누적 합을 이용하여 안정 정렬을 보장하는 원리를 이해하고 설명할 수 있어야 합니다.", + "difficulty": "INTERMEDIATE", + "tags": "계수 정렬, 비비교 정렬, O(n+k), 안정 정렬, 카운팅", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 9, + "title": "이분 탐색(Binary Search)", + "content": "이분 탐색(Binary Search)의 동작 원리와 구현 방법에 대해 설명해주세요.", + "answer": "이분 탐색은 정렬된 배열에서 탐색 범위를 절반씩 줄여가며 원하는 값을 찾는 알고리즘입니다. 매 단계마다 탐색 범위가 반으로 줄어들어 O(log n)의 시간 복잡도를 가집니다.\r\n\r\n### 동작 과정\r\n\r\n```\r\n정렬된 배열: [1, 3, 5, 7, 9, 11, 13, 15]\r\n찾는 값: 9\r\n\r\nStep 1: left=0, right=7, mid=3 → arr[3]=7 < 9 → left=4\r\nStep 2: left=4, right=7, mid=5 → arr[5]=11 > 9 → right=4\r\nStep 3: left=4, right=4, mid=4 → arr[4]=9 = 9 → 찾음!\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\n// 반복문 방식\r\nint binarySearch(int[] arr, int target) {\r\n int left = 0, right = arr.length - 1;\r\n\r\n while (left <= right) {\r\n int mid = left + (right - left) / 2; // 오버플로우 방지\r\n\r\n if (arr[mid] == target) return mid;\r\n else if (arr[mid] < target) left = mid + 1;\r\n else right = mid - 1;\r\n }\r\n return -1; // 찾지 못함\r\n}\r\n\r\n// 재귀 방식\r\nint binarySearch(int[] arr, int left, int right, int target) {\r\n if (left > right) return -1;\r\n\r\n int mid = left + (right - left) / 2;\r\n if (arr[mid] == target) return mid;\r\n if (arr[mid] < target) return binarySearch(arr, mid + 1, right, target);\r\n return binarySearch(arr, left, mid - 1, target);\r\n}\r\n```\r\n\r\n### Lower Bound / Upper Bound\r\n\r\n```java\r\n// Lower Bound: target 이상인 첫 번째 위치\r\nint lowerBound(int[] arr, int target) {\r\n int left = 0, right = arr.length;\r\n while (left < right) {\r\n int mid = left + (right - left) / 2;\r\n if (arr[mid] < target) left = mid + 1;\r\n else right = mid;\r\n }\r\n return left;\r\n}\r\n\r\n// Upper Bound: target 초과인 첫 번째 위치\r\nint upperBound(int[] arr, int target) {\r\n int left = 0, right = arr.length;\r\n while (left < right) {\r\n int mid = left + (right - left) / 2;\r\n if (arr[mid] <= target) left = mid + 1;\r\n else right = mid;\r\n }\r\n return left;\r\n}\r\n```\r\n\r\n### 매개변수 탐색 (Parametric Search)\r\n\"최적화 문제\"를 \"결정 문제\"로 바꾸어 이분 탐색을 적용하는 기법입니다.\r\n\r\n```java\r\n// 예: 나무 자르기 - 최대 높이 H를 이분 탐색\r\nint parametricSearch(int[] trees, int target) {\r\n int left = 0, right = maxHeight;\r\n int answer = 0;\r\n\r\n while (left <= right) {\r\n int mid = left + (right - left) / 2;\r\n if (canGetEnough(trees, mid, target)) {\r\n answer = mid;\r\n left = mid + 1;\r\n } else {\r\n right = mid - 1;\r\n }\r\n }\r\n return answer;\r\n}\r\n```\r\n\r\n### 주의사항\r\n- **배열이 반드시 정렬되어 있어야 합니다**\r\n- `mid = (left + right) / 2`는 정수 오버플로우가 발생할 수 있으므로 `left + (right - left) / 2` 사용\r\n- `left <= right` vs `left < right` 조건에 따라 동작이 달라짐\r\n\r\n### 면접 팁\r\n기본 이분 탐색뿐만 아니라 Lower Bound, Upper Bound, 매개변수 탐색까지 구현할 수 있어야 합니다. 코딩 테스트에서 매우 자주 출제되는 패턴입니다.", + "difficulty": "BASIC", + "tags": "이분 탐색, 이진 탐색, O(log n), 정렬된 배열, 탐색", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 10, + "title": "해시 테이블 구현", + "content": "해시 테이블을 직접 구현한다면 어떻게 설계하시겠습니까?", + "answer": "### 해시 테이블 구현\r\n\r\n```java\r\npublic class MyHashMap {\r\n private static final int DEFAULT_CAPACITY = 16;\r\n private static final float LOAD_FACTOR = 0.75f;\r\n\r\n private LinkedList>[] table;\r\n private int size;\r\n\r\n @SuppressWarnings(\"unchecked\")\r\n public MyHashMap() {\r\n table = new LinkedList[DEFAULT_CAPACITY];\r\n size = 0;\r\n }\r\n\r\n // 해시 함수\r\n private int hash(K key) {\r\n int h = key.hashCode();\r\n // 상위 비트를 하위 비트에 분산 (Java HashMap과 동일)\r\n h ^= (h >>> 16);\r\n return h & (table.length - 1);\r\n }\r\n\r\n // 삽입: O(1) 평균\r\n public void put(K key, V value) {\r\n if (size >= table.length * LOAD_FACTOR) {\r\n resize();\r\n }\r\n\r\n int index = hash(key);\r\n if (table[index] == null) {\r\n table[index] = new LinkedList<>();\r\n }\r\n\r\n // 기존 키가 있으면 갱신\r\n for (Entry entry : table[index]) {\r\n if (entry.key.equals(key)) {\r\n entry.value = value;\r\n return;\r\n }\r\n }\r\n\r\n table[index].add(new Entry<>(key, value));\r\n size++;\r\n }\r\n\r\n // 조회: O(1) 평균\r\n public V get(K key) {\r\n int index = hash(key);\r\n if (table[index] == null) return null;\r\n\r\n for (Entry entry : table[index]) {\r\n if (entry.key.equals(key)) {\r\n return entry.value;\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n // 삭제: O(1) 평균\r\n public V remove(K key) {\r\n int index = hash(key);\r\n if (table[index] == null) return null;\r\n\r\n Iterator> it = table[index].iterator();\r\n while (it.hasNext()) {\r\n Entry entry = it.next();\r\n if (entry.key.equals(key)) {\r\n V value = entry.value;\r\n it.remove();\r\n size--;\r\n return value;\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n // 리사이징: O(n)\r\n @SuppressWarnings(\"unchecked\")\r\n private void resize() {\r\n LinkedList>[] oldTable = table;\r\n table = new LinkedList[oldTable.length * 2];\r\n size = 0;\r\n\r\n for (LinkedList> bucket : oldTable) {\r\n if (bucket != null) {\r\n for (Entry entry : bucket) {\r\n put(entry.key, entry.value);\r\n }\r\n }\r\n }\r\n }\r\n\r\n static class Entry {\r\n K key;\r\n V value;\r\n\r\n Entry(K key, V value) {\r\n this.key = key;\r\n this.value = value;\r\n }\r\n }\r\n}\r\n```\r\n\r\n### 좋은 해시 함수의 조건\r\n1. **균일 분포**: 해시 값이 고르게 분포되어야 합니다\r\n2. **빠른 계산**: O(1)에 계산 가능해야 합니다\r\n3. **결정적**: 같은 입력에 항상 같은 출력\r\n\r\n### 설계 시 고려사항\r\n\r\n**테이블 크기**: 2의 거듭제곱으로 설정하면 비트 연산으로 모듈러 계산 가능\r\n```java\r\nindex = hash & (capacity - 1); // capacity가 2의 거듭제곱일 때\r\n```\r\n\r\n**로드 팩터**: 0.75가 일반적. 높으면 메모리 절약 but 충돌 증가, 낮으면 반대\r\n\r\n**리사이징**: 로드 팩터 초과 시 2배로 확장하고 모든 엔트리를 재해싱\r\n\r\n### 면접 팁\r\nJava HashMap의 내부 구현을 알고 있으면 좋습니다: 초기 용량 16, 로드 팩터 0.75, 체이닝 길이가 8을 넘으면 Red-Black Tree로 변환, 6 이하로 줄면 다시 연결 리스트로 변환됩니다.", + "difficulty": "INTERMEDIATE", + "tags": "해시 테이블, 해시 함수, 충돌 해결, 체이닝, 개방 주소법", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 11, + "title": "DFS & BFS", + "content": "DFS와 BFS의 동작 원리와 차이점에 대해 설명해주세요.", + "answer": "### DFS (Depth-First Search, 깊이 우선 탐색)\r\n한 경로를 끝까지 탐색한 후 되돌아와서 다른 경로를 탐색합니다. 스택 또는 재귀로 구현합니다.\r\n\r\n```java\r\n// 재귀 DFS\r\nvoid dfs(int node, boolean[] visited, List> graph) {\r\n visited[node] = true;\r\n System.out.print(node + \" \");\r\n\r\n for (int next : graph.get(node)) {\r\n if (!visited[next]) {\r\n dfs(next, visited, graph);\r\n }\r\n }\r\n}\r\n\r\n// 스택 DFS\r\nvoid dfsIterative(int start, List> graph) {\r\n boolean[] visited = new boolean[graph.size()];\r\n Stack stack = new Stack<>();\r\n stack.push(start);\r\n\r\n while (!stack.isEmpty()) {\r\n int node = stack.pop();\r\n if (visited[node]) continue;\r\n visited[node] = true;\r\n System.out.print(node + \" \");\r\n\r\n for (int next : graph.get(node)) {\r\n if (!visited[next]) stack.push(next);\r\n }\r\n }\r\n}\r\n```\r\n\r\n### BFS (Breadth-First Search, 너비 우선 탐색)\r\n시작 노드에서 가까운 노드부터 탐색합니다. 큐로 구현합니다.\r\n\r\n```java\r\nvoid bfs(int start, List> graph) {\r\n boolean[] visited = new boolean[graph.size()];\r\n Queue queue = new LinkedList<>();\r\n\r\n visited[start] = true;\r\n queue.offer(start);\r\n\r\n while (!queue.isEmpty()) {\r\n int node = queue.poll();\r\n System.out.print(node + \" \");\r\n\r\n for (int next : graph.get(node)) {\r\n if (!visited[next]) {\r\n visited[next] = true;\r\n queue.offer(next);\r\n }\r\n }\r\n }\r\n}\r\n```\r\n\r\n### 탐색 순서 비교\r\n\r\n```\r\n그래프:\r\n 1\r\n / \\\r\n 2 3\r\n / \\ \\\r\n4 5 6\r\n\r\nDFS: 1 → 2 → 4 → 5 → 3 → 6 (깊이 우선)\r\nBFS: 1 → 2 → 3 → 4 → 5 → 6 (너비 우선, 레벨별)\r\n```\r\n\r\n### DFS vs BFS 비교\r\n\r\n| 특성 | DFS | BFS |\r\n|------|-----|-----|\r\n| 자료구조 | 스택/재귀 | 큐 |\r\n| 메모리 | 적음 (경로 길이) | 많음 (같은 레벨 노드) |\r\n| 최단 경로 | 보장 안 됨 | 보장 (가중치 없을 때) |\r\n| 완전성 | 무한 그래프에서 위험 | 항상 찾음 |\r\n\r\n### 활용 사례\r\n\r\n**DFS가 적합한 경우:**\r\n- 경로 탐색, 사이클 감지\r\n- 위상 정렬\r\n- 미로 찾기 (모든 경로 탐색)\r\n- 백트래킹 문제\r\n- 연결 요소 탐색\r\n\r\n**BFS가 적합한 경우:**\r\n- 최단 경로 (가중치가 동일할 때)\r\n- 레벨별 탐색\r\n- 최소 이동 횟수\r\n- SNS 친구 추천 (n촌 관계)\r\n\r\n### 면접 팁\r\n\"최단 경로를 구해야 하면 BFS, 모든 경로를 탐색해야 하면 DFS\"라는 기본 원칙을 기억하세요. 코딩 테스트에서는 2차원 배열 그래프 탐색(상하좌우 이동)이 자주 출제됩니다.", + "difficulty": "INTERMEDIATE", + "tags": "DFS, BFS, 깊이 우선 탐색, 너비 우선 탐색, 그래프 탐색", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 12, + "title": "최장 증가 수열(LIS)", + "content": "최장 증가 수열(LIS) 알고리즘에 대해 설명해주세요.", + "answer": "최장 증가 수열(LIS, Longest Increasing Subsequence)은 주어진 수열에서 원소들의 순서를 유지하면서 원소의 값이 순증가하는 가장 긴 부분 수열을 찾는 문제입니다.\r\n\r\n### 예시\r\n```\r\n수열: [10, 20, 10, 30, 20, 50]\r\nLIS: [10, 20, 30, 50] → 길이 = 4\r\n```\r\n\r\n### 방법 1: DP - O(n^2)\r\n\r\n```java\r\nint lisDP(int[] arr) {\r\n int n = arr.length;\r\n int[] dp = new int[n];\r\n Arrays.fill(dp, 1); // 자기 자신만 포함\r\n\r\n for (int i = 1; i < n; i++) {\r\n for (int j = 0; j < i; j++) {\r\n if (arr[j] < arr[i]) {\r\n dp[i] = Math.max(dp[i], dp[j] + 1);\r\n }\r\n }\r\n }\r\n return Arrays.stream(dp).max().getAsInt();\r\n}\r\n```\r\n\r\n`dp[i]` = arr[i]를 마지막으로 하는 LIS의 길이\r\n\r\n### 방법 2: 이분 탐색 - O(n log n)\r\n\r\n```java\r\nint lisBinarySearch(int[] arr) {\r\n List tails = new ArrayList<>();\r\n\r\n for (int num : arr) {\r\n int pos = lowerBound(tails, num);\r\n if (pos == tails.size()) {\r\n tails.add(num); // 끝에 추가 (LIS 길이 증가)\r\n } else {\r\n tails.set(pos, num); // 기존 값 교체 (더 작은 값으로)\r\n }\r\n }\r\n return tails.size();\r\n}\r\n\r\nint lowerBound(List list, int target) {\r\n int lo = 0, hi = list.size();\r\n while (lo < hi) {\r\n int mid = (lo + hi) / 2;\r\n if (list.get(mid) < target) lo = mid + 1;\r\n else hi = mid;\r\n }\r\n return lo;\r\n}\r\n```\r\n\r\n### 이분 탐색 풀이 동작 과정\r\n\r\n```\r\n수열: [10, 20, 10, 30, 20, 50]\r\n\r\nnum=10: tails=[] → [10]\r\nnum=20: tails=[10] → [10, 20]\r\nnum=10: tails=[10, 20] → [10, 20] (10은 이미 있으므로 교체 없음)\r\nnum=30: tails=[10, 20] → [10, 20, 30]\r\nnum=20: tails=[10, 20, 30] → [10, 20, 30] (20 교체)\r\nnum=50: tails=[10, 20, 30] → [10, 20, 30, 50]\r\n\r\nLIS 길이 = 4\r\n```\r\n\r\n**주의**: tails 배열은 실제 LIS가 아니라 LIS의 길이만 정확합니다.\r\n\r\n### 실제 LIS 역추적\r\n\r\n```java\r\nint[] lisWithPath(int[] arr) {\r\n int n = arr.length;\r\n List tails = new ArrayList<>();\r\n int[] parent = new int[n]; // 역추적용\r\n int[] indices = new int[n]; // tails에서의 위치\r\n\r\n for (int i = 0; i < n; i++) {\r\n int pos = lowerBound(tails, arr[i]);\r\n if (pos == tails.size()) tails.add(arr[i]);\r\n else tails.set(pos, arr[i]);\r\n\r\n indices[i] = pos;\r\n parent[i] = pos > 0 ? findLastIndex(indices, pos - 1, i) : -1;\r\n }\r\n\r\n // 역추적하여 실제 LIS 구성\r\n // ...\r\n}\r\n```\r\n\r\n### 면접 팁\r\nO(n^2) DP 풀이는 기본이고, O(n log n) 이분 탐색 풀이까지 구현할 수 있어야 합니다. tails 배열이 실제 LIS가 아님을 주의하세요. 실제 LIS를 구해야 할 때는 역추적이 필요합니다.", + "difficulty": "ADVANCED", + "tags": "LIS, 최장 증가 수열, 동적 계획법, 이분 탐색, O(n log n)", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 13, + "title": "최소 공통 조상(LCA)", + "content": "최소 공통 조상(LCA) 알고리즘에 대해 설명해주세요.", + "answer": "최소 공통 조상(LCA, Lowest Common Ancestor)은 트리에서 두 노드의 공통 조상 중 가장 깊은(가장 가까운) 노드를 찾는 알고리즘입니다.\r\n\r\n### 예시\r\n```\r\n 1\r\n / \\\r\n 2 3\r\n / \\ \\\r\n 4 5 6\r\n /\r\n 7\r\n\r\nLCA(7, 5) = 2\r\nLCA(4, 6) = 1\r\nLCA(7, 2) = 2\r\n```\r\n\r\n### 방법 1: 나이브 - O(N)\r\n\r\n두 노드의 깊이를 맞춘 후 동시에 올라갑니다.\r\n\r\n```java\r\nint lcaNaive(int u, int v, int[] parent, int[] depth) {\r\n // 1. 깊이 맞추기\r\n while (depth[u] > depth[v]) u = parent[u];\r\n while (depth[v] > depth[u]) v = parent[v];\r\n\r\n // 2. 같은 노드가 될 때까지 동시에 올라가기\r\n while (u != v) {\r\n u = parent[u];\r\n v = parent[v];\r\n }\r\n return u;\r\n}\r\n```\r\n\r\n### 방법 2: 희소 배열 (Sparse Table) - O(log N)\r\n\r\n전처리로 각 노드의 2^k번째 조상을 미리 계산합니다.\r\n\r\n```java\r\nclass LCA {\r\n int[][] ancestor; // ancestor[k][v] = v의 2^k번째 조상\r\n int[] depth;\r\n int LOG;\r\n\r\n // 전처리: O(N log N)\r\n void preprocess(int root, List> tree) {\r\n int n = tree.size();\r\n LOG = (int) (Math.log(n) / Math.log(2)) + 1;\r\n ancestor = new int[LOG][n];\r\n depth = new int[n];\r\n\r\n // BFS로 깊이와 부모 계산\r\n bfs(root, tree);\r\n\r\n // 희소 배열 구성\r\n for (int k = 1; k < LOG; k++) {\r\n for (int v = 0; v < n; v++) {\r\n if (ancestor[k-1][v] != -1) {\r\n ancestor[k][v] = ancestor[k-1][ancestor[k-1][v]];\r\n } else {\r\n ancestor[k][v] = -1;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // LCA 쿼리: O(log N)\r\n int query(int u, int v) {\r\n // 1. 깊이 맞추기\r\n if (depth[u] < depth[v]) { int tmp = u; u = v; v = tmp; }\r\n int diff = depth[u] - depth[v];\r\n\r\n for (int k = 0; k < LOG; k++) {\r\n if (((diff >> k) & 1) == 1) {\r\n u = ancestor[k][u];\r\n }\r\n }\r\n\r\n // 2. 같으면 LCA\r\n if (u == v) return u;\r\n\r\n // 3. 동시에 올라가기 (이분 탐색 방식)\r\n for (int k = LOG - 1; k >= 0; k--) {\r\n if (ancestor[k][u] != ancestor[k][v]) {\r\n u = ancestor[k][u];\r\n v = ancestor[k][v];\r\n }\r\n }\r\n\r\n return ancestor[0][u]; // 부모가 LCA\r\n }\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 방법 | 전처리 | 쿼리 |\r\n|------|--------|------|\r\n| 나이브 | O(N) | O(N) |\r\n| 희소 배열 | O(N log N) | O(log N) |\r\n| 오일러 투어 + RMQ | O(N log N) | O(1) |\r\n\r\n### 활용 사례\r\n- 트리에서 두 노드 사이의 거리 계산\r\n- 트리에서 경로의 가중치 합 계산\r\n- 파일 시스템에서 공통 디렉토리 찾기\r\n\r\n### 면접 팁\r\nLCA 알고리즘의 핵심은 \"2^k 점프\"입니다. 이진수 표현을 이용하여 임의의 거리를 O(log N)에 이동할 수 있다는 원리를 이해하세요. 쿼리가 많은 경우 O(N log N) 전처리 + O(log N) 쿼리가 효율적입니다.", + "difficulty": "ADVANCED", + "tags": "LCA, 최소 공통 조상, 희소 배열, 트리, 이분 탐색", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 14, + "title": "동적 계획법(Dynamic Programming)", + "content": "동적 계획법(DP)이란 무엇이며, 어떤 문제에 적용할 수 있나요?", + "answer": "동적 계획법(Dynamic Programming)은 복잡한 문제를 작은 부분 문제로 나누어 해결하고, 그 결과를 저장하여 중복 계산을 방지하는 알고리즘 설계 기법입니다.\r\n\r\n### DP 적용 조건\r\n1. **최적 부분 구조 (Optimal Substructure)**: 큰 문제의 최적해가 작은 부분 문제의 최적해로 구성됨\r\n2. **중복 부분 문제 (Overlapping Subproblems)**: 같은 부분 문제가 반복적으로 등장\r\n\r\n### Top-down vs Bottom-up\r\n\r\n**Top-down (메모이제이션)**\r\n재귀 + 캐싱 방식. 필요한 부분 문제만 계산합니다.\r\n\r\n```java\r\n// 피보나치: Top-down\r\nint[] memo = new int[n + 1];\r\nArrays.fill(memo, -1);\r\n\r\nint fib(int n) {\r\n if (n <= 1) return n;\r\n if (memo[n] != -1) return memo[n];\r\n return memo[n] = fib(n - 1) + fib(n - 2);\r\n}\r\n```\r\n\r\n**Bottom-up (타뷸레이션)**\r\n작은 문제부터 순차적으로 해결. 반복문 사용.\r\n\r\n```java\r\n// 피보나치: Bottom-up\r\nint fib(int n) {\r\n int[] dp = new int[n + 1];\r\n dp[0] = 0;\r\n dp[1] = 1;\r\n for (int i = 2; i <= n; i++) {\r\n dp[i] = dp[i - 1] + dp[i - 2];\r\n }\r\n return dp[n];\r\n}\r\n```\r\n\r\n### 대표적인 DP 문제\r\n\r\n**1. 배낭 문제 (0/1 Knapsack)**\r\n```java\r\n// dp[i][w] = i번째 물건까지 고려했을 때 용량 w에서의 최대 가치\r\nfor (int i = 1; i <= n; i++) {\r\n for (int w = 0; w <= W; w++) {\r\n if (weight[i] <= w) {\r\n dp[i][w] = Math.max(dp[i-1][w],\r\n dp[i-1][w-weight[i]] + value[i]);\r\n } else {\r\n dp[i][w] = dp[i-1][w];\r\n }\r\n }\r\n}\r\n```\r\n\r\n**2. 최장 공통 부분 수열 (LCS)**\r\n```java\r\n// dp[i][j] = s1[0..i-1]과 s2[0..j-1]의 LCS 길이\r\nfor (int i = 1; i <= m; i++) {\r\n for (int j = 1; j <= n; j++) {\r\n if (s1.charAt(i-1) == s2.charAt(j-1)) {\r\n dp[i][j] = dp[i-1][j-1] + 1;\r\n } else {\r\n dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);\r\n }\r\n }\r\n}\r\n```\r\n\r\n**3. 동전 교환 (Coin Change)**\r\n```java\r\n// dp[i] = 금액 i를 만드는 최소 동전 수\r\nArrays.fill(dp, Integer.MAX_VALUE);\r\ndp[0] = 0;\r\nfor (int i = 1; i <= amount; i++) {\r\n for (int coin : coins) {\r\n if (coin <= i && dp[i - coin] != Integer.MAX_VALUE) {\r\n dp[i] = Math.min(dp[i], dp[i - coin] + 1);\r\n }\r\n }\r\n}\r\n```\r\n\r\n### DP 문제 풀이 단계\r\n1. DP 테이블 정의 (dp[i]의 의미)\r\n2. 점화식 도출\r\n3. 초기값 설정\r\n4. 탐색 방향 결정\r\n5. 최적화 (공간 최적화 등)\r\n\r\n### 면접 팁\r\nDP의 핵심은 \"점화식 도출\"입니다. dp[i]가 의미하는 바를 명확히 정의하고, 이전 상태와의 관계를 찾아야 합니다. 분할 정복과의 차이점(중복 부분 문제의 유무)도 설명할 수 있어야 합니다.", + "difficulty": "INTERMEDIATE", + "tags": "DP, 동적 계획법, 메모이제이션, 탑다운, 바텀업", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 15, + "title": "다익스트라(Dijkstra) 알고리즘", + "content": "다익스트라 알고리즘의 동작 원리와 시간 복잡도에 대해 설명해주세요.", + "answer": "다익스트라(Dijkstra) 알고리즘은 하나의 출발 노드에서 다른 모든 노드까지의 최단 경로를 구하는 알고리즘입니다. 음수 가중치가 없는 그래프에서만 동작합니다.\r\n\r\n### 동작 원리\r\n\r\n1. 출발 노드의 거리를 0, 나머지는 무한대로 초기화\r\n2. 방문하지 않은 노드 중 거리가 가장 짧은 노드를 선택\r\n3. 선택한 노드의 인접 노드들의 거리를 갱신 (Relaxation)\r\n4. 모든 노드를 방문할 때까지 2-3 반복\r\n\r\n### 구현 (우선순위 큐)\r\n\r\n```java\r\nint[] dijkstra(int start, List[] graph, int n) {\r\n int[] dist = new int[n];\r\n Arrays.fill(dist, Integer.MAX_VALUE);\r\n dist[start] = 0;\r\n\r\n // 우선순위 큐: [거리, 노드]\r\n PriorityQueue pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);\r\n pq.offer(new int[]{0, start});\r\n\r\n while (!pq.isEmpty()) {\r\n int[] curr = pq.poll();\r\n int currDist = curr[0];\r\n int currNode = curr[1];\r\n\r\n // 이미 더 짧은 경로로 처리된 경우 스킵\r\n if (currDist > dist[currNode]) continue;\r\n\r\n // 인접 노드 탐색\r\n for (int[] edge : graph[currNode]) {\r\n int next = edge[0];\r\n int weight = edge[1];\r\n int newDist = dist[currNode] + weight;\r\n\r\n if (newDist < dist[next]) {\r\n dist[next] = newDist;\r\n pq.offer(new int[]{newDist, next});\r\n }\r\n }\r\n }\r\n return dist;\r\n}\r\n```\r\n\r\n### 동작 예시\r\n\r\n```\r\n그래프:\r\nA --1-- B --2-- D\r\n| | |\r\n4 3 1\r\n| | |\r\nC --5-- E --2-- F\r\n\r\n출발: A\r\n\r\n초기: A=0, B=INF, C=INF, D=INF, E=INF, F=INF\r\n\r\nStep 1: A 선택 → B=1, C=4\r\nStep 2: B 선택 → D=3, E=4\r\nStep 3: D 선택 → F=4\r\nStep 4: C 선택 → (갱신 없음)\r\nStep 5: E 선택 → (갱신 없음)\r\nStep 6: F 선택\r\n\r\n결과: A=0, B=1, C=4, D=3, E=4, F=4\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 구현 방식 | 시간 복잡도 |\r\n|-----------|-----------|\r\n| 배열 | O(V^2) |\r\n| 우선순위 큐 (이진 힙) | O((V+E) log V) |\r\n| 피보나치 힙 | O(V log V + E) |\r\n\r\n### 음수 가중치에서 사용 불가한 이유\r\n다익스트라는 그리디 방식으로 최단 거리가 확정된 노드를 다시 방문하지 않습니다. 음수 가중치가 있으면 이미 확정된 거리가 나중에 더 짧아질 수 있어 올바른 결과를 보장하지 않습니다. 음수 가중치가 있으면 벨만-포드 알고리즘을 사용합니다.\r\n\r\n### 경로 역추적\r\n\r\n```java\r\nint[] prev = new int[n]; // 이전 노드 기록\r\nArrays.fill(prev, -1);\r\n\r\n// relaxation 시\r\nif (newDist < dist[next]) {\r\n dist[next] = newDist;\r\n prev[next] = currNode; // 경로 기록\r\n pq.offer(new int[]{newDist, next});\r\n}\r\n\r\n// 역추적\r\nList path = new ArrayList<>();\r\nfor (int v = target; v != -1; v = prev[v]) {\r\n path.add(v);\r\n}\r\nCollections.reverse(path);\r\n```\r\n\r\n### 면접 팁\r\n다익스트라 알고리즘이 음수 가중치에서 동작하지 않는 이유와, 그 대안(벨만-포드, SPFA)을 알고 있으면 좋습니다. 우선순위 큐 구현이 표준이며, 이미 처리된 노드를 스킵하는 최적화가 중요합니다.", + "difficulty": "INTERMEDIATE", + "tags": "다익스트라, 최단 경로, 그리디, 우선순위 큐, 가중치 그래프", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 16, + "title": "비트마스크(Bitmask)", + "content": "비트마스크(Bitmask)란 무엇이며, 알고리즘에서 어떻게 활용되나요?", + "answer": "비트마스크는 정수의 이진 표현을 이용하여 집합을 표현하고 조작하는 기법입니다. 각 비트가 원소의 포함 여부를 나타내어, 부분집합을 효율적으로 표현하고 연산할 수 있습니다.\r\n\r\n### 기본 비트 연산\r\n\r\n```java\r\n// AND (&): 교집합\r\n0b1010 & 0b1100 = 0b1000\r\n\r\n// OR (|): 합집합\r\n0b1010 | 0b1100 = 0b1110\r\n\r\n// XOR (^): 대칭 차집합\r\n0b1010 ^ 0b1100 = 0b0110\r\n\r\n// NOT (~): 여집합\r\n~0b1010 = 0b...0101\r\n\r\n// LEFT SHIFT (<<): 2^n 곱셈\r\n1 << 3 = 8 (2^3)\r\n\r\n// RIGHT SHIFT (>>): 2^n 나눗셈\r\n8 >> 2 = 2 (8 / 2^2)\r\n```\r\n\r\n### 집합 연산\r\n\r\n```java\r\nint set = 0; // 공집합\r\n\r\n// i번째 원소 추가\r\nset |= (1 << i);\r\n\r\n// i번째 원소 제거\r\nset &= ~(1 << i);\r\n\r\n// i번째 원소 포함 여부 확인\r\nboolean has = (set & (1 << i)) != 0;\r\n\r\n// i번째 원소 토글\r\nset ^= (1 << i);\r\n\r\n// 집합의 크기 (1의 개수)\r\nint size = Integer.bitCount(set);\r\n\r\n// 모든 부분집합 순회\r\nfor (int subset = set; subset > 0; subset = (subset - 1) & set) {\r\n // subset 처리\r\n}\r\n```\r\n\r\n### 활용 1: 모든 부분집합 열거\r\n\r\n```java\r\n// n개 원소의 모든 부분집합: 2^n개\r\nint n = 4;\r\nfor (int mask = 0; mask < (1 << n); mask++) {\r\n System.out.print(\"{ \");\r\n for (int i = 0; i < n; i++) {\r\n if ((mask & (1 << i)) != 0) {\r\n System.out.print(i + \" \");\r\n }\r\n }\r\n System.out.println(\"}\");\r\n}\r\n// {}, {0}, {1}, {0,1}, {2}, {0,2}, ...\r\n```\r\n\r\n### 활용 2: 상태 압축 DP (외판원 문제 - TSP)\r\n\r\n```java\r\n// dp[visited][current] = visited 집합의 도시를 방문하고\r\n// current에 있을 때의 최소 비용\r\nint[][] dp = new int[1 << n][n];\r\n\r\nint tsp(int visited, int current, int[][] dist) {\r\n if (visited == (1 << n) - 1) { // 모든 도시 방문\r\n return dist[current][0]; // 출발지로 복귀\r\n }\r\n if (dp[visited][current] != -1) return dp[visited][current];\r\n\r\n int result = Integer.MAX_VALUE;\r\n for (int next = 0; next < n; next++) {\r\n if ((visited & (1 << next)) == 0) { // 미방문\r\n int cost = dist[current][next] + tsp(visited | (1 << next), next, dist);\r\n result = Math.min(result, cost);\r\n }\r\n }\r\n return dp[visited][current] = result;\r\n}\r\n```\r\n\r\n### 활용 3: 권한 관리\r\n\r\n```java\r\n// 권한을 비트마스크로 관리\r\nfinal int READ = 1; // 001\r\nfinal int WRITE = 2; // 010\r\nfinal int EXECUTE = 4; // 100\r\n\r\nint permission = READ | WRITE; // 011\r\n\r\n// 권한 확인\r\nif ((permission & EXECUTE) != 0) {\r\n // 실행 권한 있음\r\n}\r\n\r\n// 권한 추가\r\npermission |= EXECUTE; // 111\r\n```\r\n\r\n### 장점\r\n- 메모리 효율적 (하나의 정수로 집합 표현)\r\n- 비트 연산은 매우 빠름 (CPU 한 클럭)\r\n- 부분집합 연산이 간결\r\n\r\n### 면접 팁\r\n비트마스크는 코딩 테스트에서 상태 압축 DP, 부분집합 문제에 자주 등장합니다. 기본 비트 연산과 집합 표현 방법을 확실히 이해하세요.", + "difficulty": "INTERMEDIATE", + "tags": "비트마스크, 비트 연산, 부분집합, 상태 압축, DP", + "categorySlug": "algorithm", + "categoryName": "알고리즘", + "categoryId": 5, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 17, + "title": "Array", + "content": "배열(Array)의 특징과 시간 복잡도에 대해 설명해주세요.", + "answer": "배열(Array)은 동일한 타입의 데이터를 연속적인 메모리 공간에 저장하는 가장 기본적인 자료구조입니다. 인덱스를 사용하여 요소에 빠르게 접근할 수 있습니다.\r\n\r\n### 특징\r\n\r\n**1. 연속적인 메모리 할당**\r\n배열의 요소들은 메모리에서 연속적으로 배치됩니다. 시작 주소와 인덱스로 요소의 주소를 계산할 수 있습니다.\r\n```\r\n요소 주소 = 시작 주소 + (인덱스 × 요소 크기)\r\n```\r\n\r\n**2. 고정 크기**\r\n배열은 생성 시 크기가 결정되며, 이후 변경할 수 없습니다. 크기 변경이 필요하면 새로운 배열을 생성하고 데이터를 복사해야 합니다.\r\n\r\n```java\r\nint[] arr = new int[5]; // 크기 5로 고정\r\narr[0] = 10;\r\narr[1] = 20;\r\n\r\n// 접근: O(1)\r\nSystem.out.println(arr[2]); // 인덱스로 바로 접근\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 연산 | 시간 복잡도 | 설명 |\r\n|------|-----------|------|\r\n| 접근 (Access) | O(1) | 인덱스로 직접 접근 |\r\n| 검색 (Search) | O(n) | 순차 탐색 필요 |\r\n| 삽입 (Insert) | O(n) | 요소 이동 필요 |\r\n| 삭제 (Delete) | O(n) | 요소 이동 필요 |\r\n| 끝에 추가 | O(1) | 공간이 있는 경우 |\r\n\r\n### 장점\r\n- **빠른 접근 속도**: 인덱스를 통한 O(1) 랜덤 접근\r\n- **캐시 지역성(Cache Locality)**: 연속 메모리로 CPU 캐시 효율이 좋음\r\n- **메모리 효율**: 포인터 등 추가 메모리가 불필요\r\n\r\n### 단점\r\n- **고정 크기**: 크기 변경이 불가능하여 메모리 낭비 또는 부족 발생 가능\r\n- **삽입/삭제 비효율**: 중간에 삽입/삭제 시 요소 이동 필요 (O(n))\r\n- **메모리 단편화**: 큰 연속 메모리 공간이 필요\r\n\r\n### 배열이 적합한 경우\r\n- 데이터의 크기가 미리 정해져 있을 때\r\n- 인덱스를 통한 빠른 접근이 필요할 때\r\n- 삽입/삭제보다 조회가 빈번할 때\r\n\r\n### 면접 팁\r\n배열의 핵심은 \"연속 메모리\"와 \"O(1) 랜덤 접근\"입니다. LinkedList와의 비교를 통해 각각의 장단점을 명확히 설명할 수 있어야 합니다. 캐시 지역성 개념도 언급하면 좋습니다.", + "difficulty": "BASIC", + "tags": "배열, 인덱스, 랜덤 접근, 연속 메모리", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 18, + "title": "LinkedList", + "content": "LinkedList의 구조와 동작 원리에 대해 설명해주세요.", + "answer": "LinkedList(연결 리스트)는 각 요소(노드)가 데이터와 다음 노드를 가리키는 포인터로 구성된 자료구조입니다. 배열과 달리 메모리의 비연속적인 공간에 데이터를 저장합니다.\r\n\r\n### 노드 구조\r\n\r\n```java\r\nclass Node {\r\n int data; // 데이터\r\n Node next; // 다음 노드를 가리키는 포인터\r\n\r\n Node(int data) {\r\n this.data = data;\r\n this.next = null;\r\n }\r\n}\r\n```\r\n\r\n### LinkedList 종류\r\n\r\n**1. 단일 연결 리스트 (Singly Linked List)**\r\n각 노드가 다음 노드만 가리킵니다. 한 방향으로만 탐색 가능합니다.\r\n```\r\n[Data|Next] → [Data|Next] → [Data|null]\r\n```\r\n\r\n**2. 이중 연결 리스트 (Doubly Linked List)**\r\n각 노드가 이전 노드와 다음 노드를 모두 가리킵니다.\r\n```\r\nnull ← [Prev|Data|Next] ⇄ [Prev|Data|Next] ⇄ [Prev|Data|Next] → null\r\n```\r\n\r\n**3. 원형 연결 리스트 (Circular Linked List)**\r\n마지막 노드가 첫 번째 노드를 가리켜 순환 구조를 이룹니다.\r\n\r\n### 주요 연산 구현\r\n\r\n```java\r\nclass LinkedList {\r\n Node head;\r\n\r\n // 맨 앞에 삽입: O(1)\r\n void insertFirst(int data) {\r\n Node newNode = new Node(data);\r\n newNode.next = head;\r\n head = newNode;\r\n }\r\n\r\n // 특정 위치 삽입: O(n)\r\n void insertAt(int index, int data) {\r\n Node newNode = new Node(data);\r\n Node current = head;\r\n for (int i = 0; i < index - 1; i++) {\r\n current = current.next;\r\n }\r\n newNode.next = current.next;\r\n current.next = newNode;\r\n }\r\n\r\n // 삭제: O(n)\r\n void delete(int data) {\r\n if (head.data == data) {\r\n head = head.next;\r\n return;\r\n }\r\n Node current = head;\r\n while (current.next != null && current.next.data != data) {\r\n current = current.next;\r\n }\r\n if (current.next != null) {\r\n current.next = current.next.next;\r\n }\r\n }\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 연산 | 시간 복잡도 |\r\n|------|-----------|\r\n| 접근 (Access) | O(n) |\r\n| 검색 (Search) | O(n) |\r\n| 맨 앞 삽입/삭제 | O(1) |\r\n| 중간 삽입/삭제 | O(n) (탐색) + O(1) (삽입/삭제) |\r\n\r\n### 장점\r\n- **동적 크기**: 크기가 고정되지 않아 유연합니다\r\n- **삽입/삭제 효율**: 위치를 알고 있다면 O(1)에 수행 가능\r\n- **메모리 효율**: 필요한 만큼만 메모리를 사용\r\n\r\n### 단점\r\n- **랜덤 접근 불가**: 인덱스 접근이 불가능하여 순차 탐색 필요\r\n- **추가 메모리**: 포인터를 저장하기 위한 추가 메모리 필요\r\n- **캐시 비효율**: 메모리가 비연속적이라 캐시 지역성이 낮음\r\n\r\n### 면접 팁\r\nLinkedList의 삽입/삭제가 O(1)이라고 할 때, 이는 \"위치를 이미 알고 있는 경우\"에 해당합니다. 위치를 찾는 데 O(n)이 걸리므로, 실질적으로는 O(n)입니다. Java의 LinkedList는 이중 연결 리스트로 구현되어 있습니다.", + "difficulty": "BASIC", + "tags": "연결리스트, 노드, 포인터, 단일 연결 리스트, 이중 연결 리스트", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 19, + "title": "Array & ArrayList & LinkedList 비교", + "content": "Array, ArrayList, LinkedList의 차이점을 비교하고, 각각 언제 사용하는 것이 적합한지 설명해주세요.", + "answer": "세 가지 자료구조는 데이터를 선형으로 저장한다는 공통점이 있지만, 내부 구현과 성능 특성에서 큰 차이가 있습니다.\r\n\r\n### Array (배열)\r\n```java\r\nint[] arr = new int[5]; // 고정 크기, 기본 타입 저장 가능\r\narr[0] = 10;\r\n```\r\n- 고정 크기, 크기 변경 불가\r\n- 기본 타입(int, double 등) 저장 가능\r\n- 연속 메모리 공간에 저장\r\n\r\n### ArrayList\r\n```java\r\nArrayList list = new ArrayList<>(); // 동적 크기\r\nlist.add(10); // O(1) - 끝에 추가\r\nlist.get(0); // O(1) - 인덱스 접근\r\nlist.add(0, 20); // O(n) - 중간 삽입\r\nlist.remove(0); // O(n) - 중간 삭제\r\n```\r\n- 내부적으로 배열을 사용하는 동적 배열\r\n- 용량이 부족하면 기존 크기의 1.5배로 새 배열을 생성하고 복사\r\n- 객체(참조 타입)만 저장 가능\r\n\r\n### LinkedList\r\n```java\r\nLinkedList list = new LinkedList<>();\r\nlist.addFirst(10); // O(1) - 맨 앞 추가\r\nlist.addLast(20); // O(1) - 맨 뒤 추가\r\nlist.get(5); // O(n) - 인덱스 접근 (순차 탐색)\r\n```\r\n- 노드 기반, 비연속 메모리\r\n- Java의 LinkedList는 이중 연결 리스트\r\n- Deque 인터페이스도 구현\r\n\r\n### 성능 비교\r\n\r\n| 연산 | Array | ArrayList | LinkedList |\r\n|------|-------|-----------|------------|\r\n| 인덱스 접근 | O(1) | O(1) | O(n) |\r\n| 검색 | O(n) | O(n) | O(n) |\r\n| 맨 끝 삽입 | - | O(1)* | O(1) |\r\n| 중간 삽입 | - | O(n) | O(1)** |\r\n| 맨 끝 삭제 | - | O(1) | O(1) |\r\n| 중간 삭제 | - | O(n) | O(1)** |\r\n| 메모리 | 가장 적음 | 중간 | 가장 많음 |\r\n\r\n*평균 O(1), 리사이즈 시 O(n)\r\n**위치를 알고 있는 경우. 탐색 포함 시 O(n)\r\n\r\n### 선택 기준\r\n\r\n**Array를 사용하는 경우:**\r\n- 크기가 고정되어 있을 때\r\n- 기본 타입 데이터를 효율적으로 저장할 때\r\n- 다차원 배열이 필요할 때\r\n\r\n**ArrayList를 사용하는 경우:**\r\n- 크기가 동적으로 변할 때\r\n- 인덱스를 통한 빈번한 조회가 필요할 때\r\n- 삽입/삭제가 주로 끝에서 이루어질 때\r\n- 대부분의 일반적인 상황에서 기본 선택\r\n\r\n**LinkedList를 사용하는 경우:**\r\n- 삽입/삭제가 빈번하고 위치가 이미 알려진 경우\r\n- Stack, Queue, Deque 구현이 필요할 때\r\n- 데이터 크기를 예측할 수 없고 빈번한 구조 변경이 있을 때\r\n\r\n### 면접 팁\r\n실무에서는 대부분의 경우 ArrayList가 더 나은 성능을 보입니다. LinkedList는 노드당 포인터 저장에 의한 메모리 오버헤드와 캐시 비효율로 인해, 이론적으로 유리한 상황에서도 ArrayList에 밀리는 경우가 많습니다.", + "difficulty": "BASIC", + "tags": "배열, ArrayList, LinkedList, 비교, 자료구조 선택", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 20, + "title": "스택(Stack) & 큐(Queue)", + "content": "스택과 큐의 차이점과 각각의 활용 사례에 대해 설명해주세요.", + "answer": "### 스택 (Stack) - LIFO\r\n\r\n스택은 \"후입선출(Last In First Out)\" 구조입니다. 가장 마지막에 넣은 데이터가 가장 먼저 나옵니다.\r\n\r\n```java\r\nStack stack = new Stack<>();\r\nstack.push(1); // [1]\r\nstack.push(2); // [1, 2]\r\nstack.push(3); // [1, 2, 3]\r\nstack.pop(); // 3 반환, [1, 2]\r\nstack.peek(); // 2 반환 (제거하지 않음)\r\n```\r\n\r\n**활용 사례:**\r\n- **함수 호출 스택**: 함수가 호출되면 스택에 push, 반환되면 pop\r\n- **뒤로 가기 기능**: 브라우저 뒤로 가기, 에디터 Undo\r\n- **괄호 검증**: 올바른 괄호 쌍 확인\r\n- **DFS (깊이 우선 탐색)**: 재귀 또는 명시적 스택 사용\r\n- **후위 표기법 계산**: 계산기 구현\r\n\r\n```java\r\n// 괄호 검증 예시\r\npublic boolean isValid(String s) {\r\n Stack stack = new Stack<>();\r\n for (char c : s.toCharArray()) {\r\n if (c == '(' || c == '{' || c == '[') {\r\n stack.push(c);\r\n } else {\r\n if (stack.isEmpty()) return false;\r\n char top = stack.pop();\r\n if (c == ')' && top != '(') return false;\r\n if (c == '}' && top != '{') return false;\r\n if (c == ']' && top != '[') return false;\r\n }\r\n }\r\n return stack.isEmpty();\r\n}\r\n```\r\n\r\n### 큐 (Queue) - FIFO\r\n\r\n큐는 \"선입선출(First In First Out)\" 구조입니다. 가장 먼저 넣은 데이터가 가장 먼저 나옵니다.\r\n\r\n```java\r\nQueue queue = new LinkedList<>();\r\nqueue.offer(1); // [1]\r\nqueue.offer(2); // [1, 2]\r\nqueue.offer(3); // [1, 2, 3]\r\nqueue.poll(); // 1 반환, [2, 3]\r\nqueue.peek(); // 2 반환 (제거하지 않음)\r\n```\r\n\r\n**활용 사례:**\r\n- **BFS (너비 우선 탐색)**: 그래프/트리 탐색\r\n- **프로세스 스케줄링**: CPU 작업 대기열\r\n- **프린터 대기열**: 먼저 요청된 인쇄 작업 먼저 처리\r\n- **메시지 큐**: 비동기 메시지 처리 (Kafka, RabbitMQ)\r\n- **버퍼**: 데이터 전송 시 버퍼링\r\n\r\n### 덱 (Deque - Double-Ended Queue)\r\n\r\n양쪽 끝에서 삽입과 삭제가 모두 가능한 자료구조입니다.\r\n\r\n```java\r\nDeque deque = new ArrayDeque<>();\r\ndeque.offerFirst(1); // 앞에 추가\r\ndeque.offerLast(2); // 뒤에 추가\r\ndeque.pollFirst(); // 앞에서 제거\r\ndeque.pollLast(); // 뒤에서 제거\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 연산 | Stack | Queue |\r\n|------|-------|-------|\r\n| 삽입 | O(1) | O(1) |\r\n| 삭제 | O(1) | O(1) |\r\n| 조회(top/front) | O(1) | O(1) |\r\n| 검색 | O(n) | O(n) |\r\n\r\n### 면접 팁\r\nJava에서 Stack 클래스보다 Deque(ArrayDeque)를 사용하는 것이 권장됩니다. Stack은 Vector를 상속받아 불필요한 동기화 오버헤드가 있기 때문입니다. 또한 두 개의 스택으로 큐를 구현하는 문제가 자주 출제됩니다.", + "difficulty": "BASIC", + "tags": "스택, 큐, LIFO, FIFO, 덱", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 21, + "title": "힙(Heap)", + "content": "힙(Heap) 자료구조의 특징과 동작 원리에 대해 설명해주세요.", + "answer": "힙(Heap)은 완전 이진 트리 기반의 자료구조로, 부모 노드와 자식 노드 사이에 대소 관계가 성립합니다. 우선순위 큐를 구현하는 데 주로 사용됩니다.\r\n\r\n### 힙의 종류\r\n\r\n**최대 힙 (Max Heap)**: 부모 노드가 자식 노드보다 항상 크거나 같습니다.\r\n**최소 힙 (Min Heap)**: 부모 노드가 자식 노드보다 항상 작거나 같습니다.\r\n\r\n```\r\n최대 힙 예시: 최소 힙 예시:\r\n 50 10\r\n / \\ / \\\r\n 30 40 20 30\r\n / \\ / \\\r\n 10 20 50 40\r\n```\r\n\r\n### 배열을 이용한 구현\r\n\r\n완전 이진 트리이므로 배열로 효율적으로 구현할 수 있습니다.\r\n- 부모 인덱스: `(i - 1) / 2`\r\n- 왼쪽 자식: `2 * i + 1`\r\n- 오른쪽 자식: `2 * i + 2`\r\n\r\n```java\r\nclass MinHeap {\r\n private int[] heap;\r\n private int size;\r\n\r\n // 삽입: O(log n)\r\n public void insert(int value) {\r\n heap[size] = value;\r\n siftUp(size);\r\n size++;\r\n }\r\n\r\n private void siftUp(int index) {\r\n while (index > 0) {\r\n int parent = (index - 1) / 2;\r\n if (heap[parent] > heap[index]) {\r\n swap(parent, index);\r\n index = parent;\r\n } else break;\r\n }\r\n }\r\n\r\n // 삭제(최소값): O(log n)\r\n public int extractMin() {\r\n int min = heap[0];\r\n heap[0] = heap[--size];\r\n siftDown(0);\r\n return min;\r\n }\r\n\r\n private void siftDown(int index) {\r\n while (2 * index + 1 < size) {\r\n int child = 2 * index + 1;\r\n if (child + 1 < size && heap[child + 1] < heap[child]) {\r\n child++;\r\n }\r\n if (heap[index] > heap[child]) {\r\n swap(index, child);\r\n index = child;\r\n } else break;\r\n }\r\n }\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 연산 | 시간 복잡도 |\r\n|------|-----------|\r\n| 삽입 | O(log n) |\r\n| 삭제 (최대/최소) | O(log n) |\r\n| 최대/최소 조회 | O(1) |\r\n| 힙 구성 (heapify) | O(n) |\r\n\r\n### Java에서 우선순위 큐\r\n\r\n```java\r\n// 최소 힙 (기본)\r\nPriorityQueue minHeap = new PriorityQueue<>();\r\nminHeap.offer(30);\r\nminHeap.offer(10);\r\nminHeap.offer(20);\r\nminHeap.poll(); // 10 (가장 작은 값)\r\n\r\n// 최대 힙\r\nPriorityQueue maxHeap = new PriorityQueue<>(Collections.reverseOrder());\r\n```\r\n\r\n### 활용 사례\r\n- **우선순위 큐**: 작업 스케줄링, 다익스트라 알고리즘\r\n- **힙 정렬**: O(n log n) 정렬 알고리즘\r\n- **Top K 문제**: K개의 최대/최소 원소 추출\r\n- **중앙값 구하기**: 최대 힙과 최소 힙을 함께 사용\r\n\r\n### 면접 팁\r\n힙은 정렬된 구조가 아니라는 점을 주의하세요. 부모-자식 간의 대소관계만 보장하며, 형제 노드 간에는 순서가 없습니다. heapify의 시간 복잡도가 O(n log n)이 아닌 O(n)인 이유를 수학적으로 설명할 수 있으면 차별화됩니다.", + "difficulty": "INTERMEDIATE", + "tags": "힙, 최대힙, 최소힙, 우선순위 큐, 완전 이진 트리", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 22, + "title": "트리(Tree)", + "content": "트리(Tree) 자료구조의 개념과 종류, 순회 방법에 대해 설명해주세요.", + "answer": "트리(Tree)는 노드와 간선으로 이루어진 비선형 계층적 자료구조입니다. 하나의 루트 노드에서 시작하여 자식 노드로 확장되며, 사이클이 없는 연결 그래프입니다.\r\n\r\n### 트리 용어\r\n- **루트(Root)**: 최상위 노드\r\n- **리프(Leaf)**: 자식이 없는 말단 노드\r\n- **부모(Parent)**: 상위 노드\r\n- **자식(Child)**: 하위 노드\r\n- **깊이(Depth)**: 루트에서 특정 노드까지의 거리\r\n- **높이(Height)**: 트리의 최대 깊이\r\n- **차수(Degree)**: 노드의 자식 수\r\n\r\n### 트리의 종류\r\n\r\n**이진 트리 (Binary Tree)**: 각 노드가 최대 2개의 자식을 가짐\r\n\r\n**완전 이진 트리 (Complete Binary Tree)**: 마지막 레벨을 제외한 모든 레벨이 채워져 있고, 마지막 레벨은 왼쪽부터 채워짐\r\n\r\n**포화 이진 트리 (Full Binary Tree)**: 모든 레벨이 완전히 채워진 이진 트리\r\n\r\n**이진 탐색 트리 (BST)**: 왼쪽 자식 < 부모 < 오른쪽 자식 규칙을 만족\r\n\r\n### 트리 구현\r\n\r\n```java\r\nclass TreeNode {\r\n int data;\r\n TreeNode left;\r\n TreeNode right;\r\n\r\n TreeNode(int data) {\r\n this.data = data;\r\n }\r\n}\r\n```\r\n\r\n### 트리 순회\r\n\r\n```java\r\nclass TreeTraversal {\r\n // 전위 순회 (Pre-order): 루트 → 왼쪽 → 오른쪽\r\n void preorder(TreeNode node) {\r\n if (node == null) return;\r\n System.out.print(node.data + \" \");\r\n preorder(node.left);\r\n preorder(node.right);\r\n }\r\n\r\n // 중위 순회 (In-order): 왼쪽 → 루트 → 오른쪽\r\n void inorder(TreeNode node) {\r\n if (node == null) return;\r\n inorder(node.left);\r\n System.out.print(node.data + \" \");\r\n inorder(node.right);\r\n }\r\n\r\n // 후위 순회 (Post-order): 왼쪽 → 오른쪽 → 루트\r\n void postorder(TreeNode node) {\r\n if (node == null) return;\r\n postorder(node.left);\r\n postorder(node.right);\r\n System.out.print(node.data + \" \");\r\n }\r\n\r\n // 레벨 순회 (Level-order): BFS 방식\r\n void levelorder(TreeNode root) {\r\n Queue queue = new LinkedList<>();\r\n queue.offer(root);\r\n while (!queue.isEmpty()) {\r\n TreeNode node = queue.poll();\r\n System.out.print(node.data + \" \");\r\n if (node.left != null) queue.offer(node.left);\r\n if (node.right != null) queue.offer(node.right);\r\n }\r\n }\r\n}\r\n```\r\n\r\n### 순회 결과 예시\r\n```\r\n 1\r\n / \\\r\n 2 3\r\n / \\\r\n 4 5\r\n\r\n전위: 1 2 4 5 3\r\n중위: 4 2 5 1 3\r\n후위: 4 5 2 3 1\r\n레벨: 1 2 3 4 5\r\n```\r\n\r\n### 면접 팁\r\n트리 순회는 재귀적 방법과 반복적(스택 사용) 방법을 모두 구현할 수 있어야 합니다. BST에서 중위 순회를 하면 오름차순으로 정렬된 결과를 얻는다는 점도 자주 출제됩니다.", + "difficulty": "INTERMEDIATE", + "tags": "트리, 이진 트리, 완전 이진 트리, 트리 순회, 노드", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 23, + "title": "이진탐색트리(BST)", + "content": "이진 탐색 트리(BST)의 특징과 연산의 시간 복잡도에 대해 설명해주세요.", + "answer": "이진 탐색 트리(Binary Search Tree, BST)는 이진 트리의 일종으로, 각 노드에 대해 \"왼쪽 서브트리의 모든 노드 값 < 현재 노드 값 < 오른쪽 서브트리의 모든 노드 값\" 규칙을 만족하는 자료구조입니다.\r\n\r\n### 주요 연산 구현\r\n\r\n```java\r\nclass BST {\r\n TreeNode root;\r\n\r\n // 탐색: 평균 O(log n)\r\n TreeNode search(TreeNode node, int key) {\r\n if (node == null || node.data == key) return node;\r\n if (key < node.data) return search(node.left, key);\r\n return search(node.right, key);\r\n }\r\n\r\n // 삽입: 평균 O(log n)\r\n TreeNode insert(TreeNode node, int key) {\r\n if (node == null) return new TreeNode(key);\r\n if (key < node.data) node.left = insert(node.left, key);\r\n else if (key > node.data) node.right = insert(node.right, key);\r\n return node;\r\n }\r\n\r\n // 삭제: 평균 O(log n)\r\n TreeNode delete(TreeNode node, int key) {\r\n if (node == null) return null;\r\n\r\n if (key < node.data) {\r\n node.left = delete(node.left, key);\r\n } else if (key > node.data) {\r\n node.right = delete(node.right, key);\r\n } else {\r\n // Case 1: 리프 노드 또는 자식이 하나\r\n if (node.left == null) return node.right;\r\n if (node.right == null) return node.left;\r\n\r\n // Case 2: 자식이 둘 - 중위 후속자로 대체\r\n TreeNode successor = findMin(node.right);\r\n node.data = successor.data;\r\n node.right = delete(node.right, successor.data);\r\n }\r\n return node;\r\n }\r\n\r\n TreeNode findMin(TreeNode node) {\r\n while (node.left != null) node = node.left;\r\n return node;\r\n }\r\n}\r\n```\r\n\r\n### 삭제 시 세 가지 경우\r\n1. **리프 노드 삭제**: 단순히 제거\r\n2. **자식이 하나인 노드 삭제**: 자식을 부모에 연결\r\n3. **자식이 둘인 노드 삭제**: 중위 후속자(오른쪽 서브트리의 최소값) 또는 중위 선행자(왼쪽 서브트리의 최대값)로 대체\r\n\r\n### 시간 복잡도\r\n\r\n| 연산 | 평균 | 최악 (편향) |\r\n|------|------|-----------|\r\n| 탐색 | O(log n) | O(n) |\r\n| 삽입 | O(log n) | O(n) |\r\n| 삭제 | O(log n) | O(n) |\r\n\r\n### 편향 트리 문제\r\n\r\n정렬된 데이터를 순서대로 삽입하면 한쪽으로 치우친 편향 트리가 됩니다.\r\n```\r\n1 → 2 → 3 → 4 → 5 (사실상 연결 리스트)\r\n```\r\n이 경우 모든 연산이 O(n)으로 퇴화합니다.\r\n\r\n### 해결: 균형 이진 탐색 트리\r\n- **AVL 트리**: 모든 노드에서 좌우 서브트리 높이 차이가 1 이하\r\n- **Red-Black 트리**: 색상 규칙을 통해 대략적인 균형 유지 (Java의 TreeMap, TreeSet)\r\n- **Splay 트리**: 최근 접근한 노드를 루트로 올림\r\n\r\n### 면접 팁\r\nBST의 중위 순회가 정렬된 결과를 반환한다는 점은 자주 출제됩니다. 또한 BST의 최악 케이스와 이를 해결하는 균형 트리(특히 Red-Black Tree)에 대해서도 설명할 수 있어야 합니다.", + "difficulty": "INTERMEDIATE", + "tags": "이진탐색트리, BST, 탐색, 삽입, 삭제", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 24, + "title": "해시(Hash)", + "content": "해시 테이블의 동작 원리와 충돌 해결 방법에 대해 설명해주세요.", + "answer": "해시 테이블(Hash Table)은 키(Key)를 해시 함수(Hash Function)에 넣어 나온 해시 값을 인덱스로 사용하여 데이터를 저장하고 검색하는 자료구조입니다. 평균 O(1)의 시간 복잡도로 데이터에 접근할 수 있습니다.\r\n\r\n### 동작 원리\r\n\r\n```\r\n키(Key) → 해시 함수 → 해시 값(인덱스) → 버킷(Bucket)에 저장\r\n\"apple\" → hash() → 3 → table[3] = \"apple\"\r\n```\r\n\r\n```java\r\n// 간단한 해시 함수 예시\r\nint hash(String key) {\r\n int hashValue = 0;\r\n for (char c : key.toCharArray()) {\r\n hashValue = (hashValue * 31 + c) % tableSize;\r\n }\r\n return hashValue;\r\n}\r\n```\r\n\r\n### 해시 충돌 (Hash Collision)\r\n서로 다른 키가 동일한 해시 값을 가지는 현상입니다. 해시 함수의 출력 범위가 유한하므로 충돌은 불가피합니다.\r\n\r\n### 충돌 해결 방법\r\n\r\n**1. 체이닝 (Separate Chaining)**\r\n같은 해시 값을 가진 데이터를 연결 리스트로 연결합니다.\r\n\r\n```java\r\nclass HashTable {\r\n LinkedList[] table;\r\n\r\n void put(String key, String value) {\r\n int index = hash(key);\r\n for (Entry entry : table[index]) {\r\n if (entry.key.equals(key)) {\r\n entry.value = value; // 키가 이미 존재하면 갱신\r\n return;\r\n }\r\n }\r\n table[index].add(new Entry(key, value));\r\n }\r\n}\r\n```\r\n\r\n장점: 삭제가 간단, 테이블 확장이 유연\r\n단점: 메모리 추가 사용, 캐시 효율 낮음\r\n\r\n**2. 개방 주소법 (Open Addressing)**\r\n충돌 시 다른 빈 버킷을 탐색하여 저장합니다.\r\n\r\n- **선형 탐사(Linear Probing)**: 다음 인덱스를 순차적으로 탐색\r\n ```\r\n index = (hash(key) + i) % tableSize\r\n ```\r\n- **이차 탐사(Quadratic Probing)**: 제곱 간격으로 탐색\r\n ```\r\n index = (hash(key) + i²) % tableSize\r\n ```\r\n- **이중 해싱(Double Hashing)**: 두 번째 해시 함수로 탐사 간격 결정\r\n ```\r\n index = (hash1(key) + i * hash2(key)) % tableSize\r\n ```\r\n\r\n### 로드 팩터 (Load Factor)\r\n```\r\nLoad Factor = 저장된 항목 수 / 테이블 크기\r\n```\r\n로드 팩터가 높아지면 충돌이 빈번해져 성능이 저하됩니다. Java의 HashMap은 로드 팩터가 0.75를 초과하면 테이블 크기를 2배로 확장(rehashing)합니다.\r\n\r\n### 시간 복잡도\r\n\r\n| 연산 | 평균 | 최악 |\r\n|------|------|------|\r\n| 삽입 | O(1) | O(n) |\r\n| 삭제 | O(1) | O(n) |\r\n| 탐색 | O(1) | O(n) |\r\n\r\n### Java의 HashMap\r\nJava 8부터 HashMap의 체이닝에서 연결 리스트의 길이가 8을 초과하면 Red-Black Tree로 변환하여 최악의 경우에도 O(log n)을 보장합니다.\r\n\r\n### 면접 팁\r\n좋은 해시 함수의 조건(균일 분포, 빠른 계산)과 Java HashMap의 내부 구현(초기 용량, 로드 팩터, 트리화)을 함께 설명할 수 있으면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "해시, 해시 함수, 해시 충돌, 체이닝, 개방 주소법", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 25, + "title": "트라이(Trie)", + "content": "트라이(Trie) 자료구조의 개념과 활용 사례에 대해 설명해주세요.", + "answer": "트라이(Trie)는 문자열을 저장하고 효율적으로 탐색하기 위한 트리 형태의 자료구조입니다. \"Retrieval\"에서 유래한 이름으로, 접두사 트리(Prefix Tree)라고도 합니다.\r\n\r\n### 구조\r\n\r\n각 노드는 문자 하나를 나타내며, 루트에서 특정 노드까지의 경로가 하나의 문자열(접두사)을 나타냅니다.\r\n\r\n```\r\n예: \"cat\", \"car\", \"card\", \"dog\" 저장\r\n\r\n root\r\n / \\\r\n c d\r\n | |\r\n a o\r\n / \\ |\r\n t r g*\r\n * |\r\n d*\r\n\r\n(* = 단어의 끝)\r\n```\r\n\r\n### 구현\r\n\r\n```java\r\nclass TrieNode {\r\n TrieNode[] children = new TrieNode[26]; // 알파벳 소문자\r\n boolean isEndOfWord;\r\n}\r\n\r\nclass Trie {\r\n TrieNode root = new TrieNode();\r\n\r\n // 삽입: O(m)\r\n void insert(String word) {\r\n TrieNode node = root;\r\n for (char c : word.toCharArray()) {\r\n int index = c - 'a';\r\n if (node.children[index] == null) {\r\n node.children[index] = new TrieNode();\r\n }\r\n node = node.children[index];\r\n }\r\n node.isEndOfWord = true;\r\n }\r\n\r\n // 검색: O(m)\r\n boolean search(String word) {\r\n TrieNode node = findNode(word);\r\n return node != null && node.isEndOfWord;\r\n }\r\n\r\n // 접두사 검색: O(m)\r\n boolean startsWith(String prefix) {\r\n return findNode(prefix) != null;\r\n }\r\n\r\n private TrieNode findNode(String str) {\r\n TrieNode node = root;\r\n for (char c : str.toCharArray()) {\r\n int index = c - 'a';\r\n if (node.children[index] == null) return null;\r\n node = node.children[index];\r\n }\r\n return node;\r\n }\r\n\r\n // 자동완성: 접두사로 시작하는 모든 단어 찾기\r\n List autocomplete(String prefix) {\r\n List results = new ArrayList<>();\r\n TrieNode node = findNode(prefix);\r\n if (node != null) {\r\n dfs(node, new StringBuilder(prefix), results);\r\n }\r\n return results;\r\n }\r\n\r\n private void dfs(TrieNode node, StringBuilder sb, List results) {\r\n if (node.isEndOfWord) results.add(sb.toString());\r\n for (int i = 0; i < 26; i++) {\r\n if (node.children[i] != null) {\r\n sb.append((char) ('a' + i));\r\n dfs(node.children[i], sb, results);\r\n sb.deleteCharAt(sb.length() - 1);\r\n }\r\n }\r\n }\r\n}\r\n```\r\n\r\n### 시간 복잡도\r\n\r\n| 연산 | 시간 복잡도 |\r\n|------|-----------|\r\n| 삽입 | O(m) |\r\n| 검색 | O(m) |\r\n| 접두사 검색 | O(m) |\r\n| 삭제 | O(m) |\r\n\r\nm = 문자열의 길이\r\n\r\n### 장점\r\n- 문자열 검색이 O(m)으로 매우 빠름 (해시 테이블의 해시 충돌 문제 없음)\r\n- 접두사 기반 검색에 최적화\r\n- 사전순 정렬이 자연스럽게 유지됨\r\n\r\n### 단점\r\n- 메모리 사용량이 큼 (각 노드마다 자식 포인터 배열 필요)\r\n- 문자 종류가 많으면(유니코드) 메모리 낭비가 심함\r\n\r\n### 활용 사례\r\n- **자동완성**: 검색 엔진의 검색어 추천\r\n- **맞춤법 검사**: 사전에 단어가 존재하는지 확인\r\n- **IP 라우팅**: Longest Prefix Matching\r\n- **문자열 매칭**: 여러 패턴을 동시에 검색\r\n\r\n### 면접 팁\r\n트라이는 공간 효율을 개선한 변형 구조(압축 트라이, 삼진 탐색 트라이)도 있다는 것을 알고 있으면 좋습니다. 해시 테이블과 비교하여 각각의 장단점을 설명할 수 있어야 합니다.", + "difficulty": "ADVANCED", + "tags": "트라이, 문자열 검색, 자동완성, 접두사 트리", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 26, + "title": "B-Tree & B+Tree", + "content": "B-Tree와 B+Tree의 구조와 차이점에 대해 설명하고, 데이터베이스에서 B+Tree를 사용하는 이유를 설명해주세요.", + "answer": "B-Tree와 B+Tree는 디스크 기반의 대용량 데이터를 효율적으로 관리하기 위해 설계된 균형 다진 탐색 트리(Balanced Multi-way Search Tree)입니다.\r\n\r\n### B-Tree\r\n\r\n차수(order) M인 B-Tree의 특성:\r\n- 루트 노드는 최소 2개의 자식을 가짐\r\n- 루트를 제외한 내부 노드는 최소 ⌈M/2⌉개의 자식을 가짐\r\n- 모든 리프 노드는 같은 레벨에 위치 (균형)\r\n- 각 노드는 최대 M-1개의 키를 가짐\r\n- 키는 정렬된 상태로 유지\r\n\r\n```\r\nB-Tree (차수 3):\r\n [30]\r\n / \\\r\n [10, 20] [40, 50]\r\n / | \\ / | \\\r\n ... ... ... ... ... ...\r\n```\r\n\r\n**장점:**\r\n- 모든 노드에 데이터가 저장되어 있어 검색 시 빨리 찾을 수 있음\r\n- 디스크 I/O를 최소화하도록 설계됨 (높이가 낮음)\r\n\r\n### B+Tree\r\n\r\nB-Tree의 변형으로, 데이터베이스 인덱스에 가장 널리 사용됩니다.\r\n\r\n**B-Tree와의 차이점:**\r\n1. **데이터 저장 위치**: 실제 데이터는 리프 노드에만 저장되고, 내부 노드는 키만 저장 (인덱스 역할)\r\n2. **리프 노드 연결**: 리프 노드들이 연결 리스트로 연결되어 있음\r\n3. **키 중복**: 내부 노드의 키가 리프 노드에도 존재\r\n\r\n```\r\nB+Tree (차수 3):\r\n [30] ← 내부 노드 (키만 저장)\r\n / \\\r\n [10, 20] [30, 40] ← 내부 노드 (키만 저장)\r\n / | \\ / | \\\r\n [데이터들] ← 리프 노드 (실제 데이터)\r\n ←→ ←→ ←→ ←→ ←→ ← 리프 노드 간 연결 리스트\r\n```\r\n\r\n### 비교\r\n\r\n| 특성 | B-Tree | B+Tree |\r\n|------|--------|--------|\r\n| 데이터 저장 | 모든 노드 | 리프 노드만 |\r\n| 리프 연결 | 없음 | 연결 리스트 |\r\n| 내부 노드 크기 | 큼 | 작음 (키만) |\r\n| 범위 탐색 | 비효율적 | 효율적 |\r\n| 중복 키 | 없음 | 있음 |\r\n\r\n### B+Tree가 DB 인덱스에 사용되는 이유\r\n\r\n**1. 범위 탐색 효율성**\r\n리프 노드가 연결 리스트로 연결되어 있어 순차 탐색이 매우 효율적입니다.\r\n```sql\r\n-- B+Tree가 유리한 범위 쿼리\r\nSELECT * FROM employees WHERE age BETWEEN 25 AND 35;\r\n```\r\n\r\n**2. 내부 노드의 작은 크기**\r\n내부 노드에 데이터 없이 키만 저장하므로 하나의 노드에 더 많은 키를 담을 수 있습니다. 이는 트리의 높이를 낮추어 디스크 I/O 횟수를 줄입니다.\r\n\r\n**3. 균일한 검색 성능**\r\n모든 데이터가 리프 노드에 있으므로, 어떤 데이터를 검색하든 동일한 시간이 소요됩니다.\r\n\r\n### 시간 복잡도\r\n- 검색: O(log n)\r\n- 삽입: O(log n)\r\n- 삭제: O(log n)\r\n\r\n### 면접 팁\r\nB+Tree가 데이터베이스 인덱스에 사용되는 이유를 디스크 I/O 관점에서 설명할 수 있어야 합니다. 해시 인덱스와 비교하여 B+Tree가 범위 탐색에서 유리한 이유도 함께 설명하세요.", + "difficulty": "ADVANCED", + "tags": "B-Tree, B+Tree, 데이터베이스 인덱스, 디스크 기반, 다진 탐색 트리", + "categorySlug": "data-structure", + "categoryName": "자료구조", + "categoryId": 2, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 27, + "title": "키(Key) 정리", + "content": "데이터베이스에서 사용되는 키(Key)의 종류와 각각의 역할에 대해 설명해주세요.", + "answer": "데이터베이스에서 키(Key)는 테이블의 각 행(튜플)을 유일하게 식별하거나, 테이블 간의 관계를 설정하기 위해 사용되는 속성 또는 속성의 집합입니다.\r\n\r\n### 키의 종류\r\n\r\n**1. 슈퍼키 (Super Key)**\r\n테이블에서 튜플을 유일하게 식별할 수 있는 속성의 집합입니다. 유일성은 만족하지만 최소성은 만족하지 않을 수 있습니다. 예를 들어, 학생 테이블에서 (학번, 이름) 조합은 슈퍼키가 될 수 있습니다.\r\n\r\n**2. 후보키 (Candidate Key)**\r\n슈퍼키 중에서 최소성을 만족하는 키입니다. 즉, 튜플을 유일하게 식별하는 데 필요한 최소한의 속성으로 구성됩니다. 하나의 테이블에 여러 후보키가 존재할 수 있습니다.\r\n\r\n**3. 기본키 (Primary Key)**\r\n후보키 중에서 테이블의 대표 키로 선택된 키입니다. 기본키의 특징은 다음과 같습니다:\r\n- NULL 값을 허용하지 않습니다\r\n- 중복 값을 허용하지 않습니다\r\n- 테이블당 하나만 존재합니다\r\n\r\n```sql\r\nCREATE TABLE student (\r\n student_id INT PRIMARY KEY, -- 기본키\r\n name VARCHAR(50) NOT NULL,\r\n email VARCHAR(100) UNIQUE -- 대체키가 될 수 있음\r\n);\r\n```\r\n\r\n**4. 대체키 (Alternate Key)**\r\n후보키 중에서 기본키로 선택되지 않은 나머지 키입니다. 위 예시에서 email이 후보키였다면, 기본키로 student_id를 선택한 후 email은 대체키가 됩니다.\r\n\r\n**5. 외래키 (Foreign Key)**\r\n다른 테이블의 기본키를 참조하는 속성으로, 테이블 간의 관계를 설정합니다. 참조 무결성을 유지하는 데 핵심적인 역할을 합니다.\r\n\r\n```sql\r\nCREATE TABLE enrollment (\r\n enrollment_id INT PRIMARY KEY,\r\n student_id INT,\r\n course_id INT,\r\n FOREIGN KEY (student_id) REFERENCES student(student_id)\r\n);\r\n```\r\n\r\n**6. 복합키 (Composite Key)**\r\n두 개 이상의 속성을 조합하여 만든 키입니다. 단일 속성만으로는 유일성을 보장할 수 없을 때 사용합니다.\r\n\r\n### 키 선택 시 고려사항\r\n- **유일성**: 모든 튜플을 구별할 수 있어야 합니다\r\n- **최소성**: 꼭 필요한 속성만으로 구성해야 합니다\r\n- **불변성**: 자주 변경되지 않는 속성을 선택해야 합니다\r\n- **NOT NULL**: NULL 값이 없어야 합니다\r\n\r\n### 면접 팁\r\n키의 계층 구조를 명확히 이해하세요: 슈퍼키 ⊃ 후보키 ⊃ 기본키. 실무에서는 자연키(Natural Key)보다 인조키(Surrogate Key, Auto Increment)를 기본키로 사용하는 경우가 많은데, 그 이유도 함께 설명할 수 있으면 좋습니다.", + "difficulty": "BASIC", + "tags": "기본키, 외래키, 후보키, 슈퍼키, 대체키", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 28, + "title": "SQL JOIN", + "content": "SQL에서 JOIN의 종류와 각각의 차이점에 대해 설명해주세요.", + "answer": "JOIN은 두 개 이상의 테이블을 연결하여 데이터를 조회하는 SQL 연산입니다. 관계형 데이터베이스에서 정규화로 분리된 테이블들을 다시 결합할 때 사용합니다.\r\n\r\n### JOIN의 종류\r\n\r\n**1. INNER JOIN**\r\n두 테이블에서 조건이 일치하는 행만 반환합니다. 가장 많이 사용되는 JOIN입니다.\r\n\r\n```sql\r\nSELECT e.name, d.dept_name\r\nFROM employee e\r\nINNER JOIN department d ON e.dept_id = d.dept_id;\r\n```\r\n\r\n**2. LEFT OUTER JOIN**\r\n왼쪽 테이블의 모든 행과, 오른쪽 테이블에서 조건이 일치하는 행을 반환합니다. 일치하지 않는 경우 NULL로 채워집니다.\r\n\r\n```sql\r\nSELECT e.name, d.dept_name\r\nFROM employee e\r\nLEFT JOIN department d ON e.dept_id = d.dept_id;\r\n-- 부서가 없는 직원도 조회됨\r\n```\r\n\r\n**3. RIGHT OUTER JOIN**\r\n오른쪽 테이블의 모든 행과, 왼쪽 테이블에서 조건이 일치하는 행을 반환합니다.\r\n\r\n```sql\r\nSELECT e.name, d.dept_name\r\nFROM employee e\r\nRIGHT JOIN department d ON e.dept_id = d.dept_id;\r\n-- 직원이 없는 부서도 조회됨\r\n```\r\n\r\n**4. FULL OUTER JOIN**\r\n두 테이블의 모든 행을 반환합니다. 일치하지 않는 행은 NULL로 채워집니다.\r\n\r\n```sql\r\nSELECT e.name, d.dept_name\r\nFROM employee e\r\nFULL OUTER JOIN department d ON e.dept_id = d.dept_id;\r\n```\r\n\r\n**5. CROSS JOIN**\r\n두 테이블의 모든 행을 조합하는 카르테시안 곱(Cartesian Product)을 반환합니다.\r\n\r\n```sql\r\nSELECT e.name, d.dept_name\r\nFROM employee e\r\nCROSS JOIN department d;\r\n-- 직원 수 × 부서 수만큼의 행이 반환됨\r\n```\r\n\r\n**6. SELF JOIN**\r\n같은 테이블을 자기 자신과 JOIN하는 것입니다. 계층 구조를 표현할 때 유용합니다.\r\n\r\n```sql\r\nSELECT e.name AS 직원, m.name AS 관리자\r\nFROM employee e\r\nLEFT JOIN employee m ON e.manager_id = m.emp_id;\r\n```\r\n\r\n### JOIN 성능 최적화\r\n- JOIN할 컬럼에 인덱스를 생성하면 성능이 크게 향상됩니다\r\n- 필요한 컬럼만 SELECT하고, WHERE 조건을 적절히 사용합니다\r\n- 작은 테이블을 기준으로 JOIN하는 것이 일반적으로 유리합니다\r\n\r\n### 면접 팁\r\n각 JOIN의 벤 다이어그램을 머릿속에 그려보세요. INNER JOIN은 교집합, LEFT JOIN은 왼쪽 원 전체, FULL OUTER JOIN은 합집합입니다. 실무에서는 INNER JOIN과 LEFT JOIN을 가장 많이 사용합니다.", + "difficulty": "BASIC", + "tags": "INNER JOIN, OUTER JOIN, CROSS JOIN, SELF JOIN", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 29, + "title": "SQL Injection", + "content": "SQL Injection이란 무엇이며, 어떻게 방어할 수 있나요?", + "answer": "SQL Injection은 악의적인 사용자가 입력 필드를 통해 SQL 쿼리를 조작하여 데이터베이스를 비정상적으로 조작하는 공격 기법입니다. OWASP Top 10에 항상 포함되는 대표적인 웹 보안 취약점입니다.\r\n\r\n### 공격 원리\r\n\r\n사용자의 입력값이 SQL 쿼리에 직접 삽입될 때 발생합니다.\r\n\r\n```java\r\n// 취약한 코드\r\nString query = \"SELECT * FROM users WHERE id = '\" + userInput + \"'\";\r\n```\r\n\r\n공격자가 `' OR '1'='1`을 입력하면:\r\n```sql\r\nSELECT * FROM users WHERE id = '' OR '1'='1'\r\n-- 모든 사용자 정보가 노출됨\r\n```\r\n\r\n### 공격 유형\r\n\r\n**1. 인증 우회**\r\n로그인 폼에서 `admin' --`를 입력하여 비밀번호 검증을 무력화합니다.\r\n\r\n**2. 데이터 추출 (Union-based)**\r\n```sql\r\n' UNION SELECT username, password FROM users --\r\n```\r\nUNION을 사용하여 다른 테이블의 데이터를 추출합니다.\r\n\r\n**3. Blind SQL Injection**\r\n참/거짓 반응을 통해 데이터를 한 글자씩 추측합니다.\r\n```sql\r\n' AND SUBSTRING(password,1,1)='a' --\r\n```\r\n\r\n**4. Time-based Blind SQL Injection**\r\n서버 응답 시간 차이를 이용하여 데이터를 추출합니다.\r\n\r\n### 방어 방법\r\n\r\n**1. PreparedStatement 사용 (가장 효과적)**\r\n```java\r\nString query = \"SELECT * FROM users WHERE id = ?\";\r\nPreparedStatement pstmt = conn.prepareStatement(query);\r\npstmt.setString(1, userInput);\r\nResultSet rs = pstmt.executeQuery();\r\n```\r\n\r\n**2. ORM 사용**\r\n```java\r\n// JPA 사용 예시\r\n@Query(\"SELECT u FROM User u WHERE u.id = :userId\")\r\nUser findByUserId(@Param(\"userId\") String userId);\r\n```\r\n\r\n**3. 입력값 검증 및 이스케이프 처리**\r\n- 화이트리스트 방식으로 허용된 문자만 입력 가능하도록 제한\r\n- 특수문자(', \", --, ;) 이스케이프 처리\r\n\r\n**4. 최소 권한 원칙**\r\n데이터베이스 접속 계정에 최소한의 권한만 부여합니다.\r\n\r\n**5. 웹 방화벽(WAF) 사용**\r\nSQL Injection 패턴을 탐지하고 차단하는 웹 애플리케이션 방화벽을 도입합니다.\r\n\r\n### 면접 팁\r\n단순히 \"PreparedStatement를 쓰면 됩니다\"보다는, 왜 PreparedStatement가 SQL Injection을 방어하는지(쿼리 구조와 데이터를 분리하여 컴파일하기 때문) 원리까지 설명하면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "SQL Injection, 보안, PreparedStatement, 파라미터 바인딩", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 30, + "title": "SQL vs NoSQL", + "content": "SQL과 NoSQL의 차이점을 설명하고, 각각 어떤 상황에서 사용하는 것이 적합한가요?", + "answer": "SQL(Structured Query Language) 데이터베이스는 관계형 모델을 기반으로 정형화된 스키마를 사용하며, NoSQL(Not Only SQL)은 비관계형 모델로 유연한 스키마를 제공합니다.\r\n\r\n### SQL (관계형 데이터베이스)\r\n\r\n**특징:**\r\n- 정해진 스키마에 따라 데이터를 저장합니다\r\n- 테이블 간 관계(Relationship)를 통해 데이터를 관리합니다\r\n- ACID 트랜잭션을 보장합니다\r\n- SQL이라는 표준화된 질의 언어를 사용합니다\r\n\r\n**대표 제품:** MySQL, PostgreSQL, Oracle, MSSQL\r\n\r\n```sql\r\nCREATE TABLE orders (\r\n order_id INT PRIMARY KEY,\r\n user_id INT REFERENCES users(user_id),\r\n product_name VARCHAR(100),\r\n amount DECIMAL(10,2)\r\n);\r\n```\r\n\r\n### NoSQL (비관계형 데이터베이스)\r\n\r\n**유형:**\r\n- **문서형(Document)**: MongoDB, CouchDB - JSON/BSON 형태로 저장\r\n- **키-값(Key-Value)**: Redis, DynamoDB - 단순한 키-값 쌍으로 저장\r\n- **컬럼형(Column-Family)**: Cassandra, HBase - 컬럼 단위로 저장\r\n- **그래프형(Graph)**: Neo4j - 노드와 엣지로 관계를 표현\r\n\r\n```javascript\r\n// MongoDB 문서 예시\r\n{\r\n \"_id\": ObjectId(\"...\"),\r\n \"name\": \"홍길동\",\r\n \"orders\": [\r\n { \"product\": \"노트북\", \"amount\": 1500000 },\r\n { \"product\": \"마우스\", \"amount\": 35000 }\r\n ]\r\n}\r\n```\r\n\r\n### 비교\r\n\r\n| 항목 | SQL | NoSQL |\r\n|------|-----|-------|\r\n| 스키마 | 고정 스키마 | 유연한 스키마 |\r\n| 확장성 | 수직적 확장 (Scale-up) | 수평적 확장 (Scale-out) |\r\n| 트랜잭션 | ACID 보장 | BASE (Eventually Consistent) |\r\n| 관계 | JOIN을 통한 관계 표현 | 비정규화, 내장 문서 |\r\n| 일관성 | 강한 일관성 | 최종적 일관성 |\r\n\r\n### 선택 기준\r\n\r\n**SQL이 적합한 경우:**\r\n- 데이터 구조가 명확하고 변경이 적을 때\r\n- 복잡한 쿼리와 JOIN이 필요할 때\r\n- 트랜잭션의 ACID가 중요할 때 (금융, 결제 시스템)\r\n\r\n**NoSQL이 적합한 경우:**\r\n- 데이터 구조가 자주 변경될 때\r\n- 대용량 데이터를 빠르게 처리해야 할 때\r\n- 수평적 확장이 필요할 때 (SNS, IoT, 로그 데이터)\r\n\r\n### 면접 팁\r\nCAP 이론(Consistency, Availability, Partition Tolerance)과 연관하여 설명하면 깊이 있는 답변이 됩니다. 실무에서는 하나만 선택하기보다 Polyglot Persistence 전략으로 두 가지를 함께 사용하는 경우가 많습니다.", + "difficulty": "INTERMEDIATE", + "tags": "SQL, NoSQL, RDBMS, MongoDB, 관계형 데이터베이스", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 31, + "title": "정규화(Normalization)", + "content": "데이터베이스 정규화란 무엇이며, 각 정규형에 대해 설명해주세요.", + "answer": "정규화(Normalization)란 관계형 데이터베이스에서 데이터의 중복을 최소화하고 무결성을 보장하기 위해 테이블을 분해하는 과정입니다. 이상 현상(Anomaly)을 방지하기 위해 수행합니다.\r\n\r\n### 함수 종속성 (Functional Dependency)\r\n정규화의 기반이 되는 개념으로, 속성 X의 값이 속성 Y의 값을 유일하게 결정할 때 \"Y는 X에 함수적으로 종속된다\"고 합니다 (X → Y).\r\n\r\n### 정규형의 종류\r\n\r\n**제1정규형 (1NF)**\r\n모든 속성의 도메인이 원자값(Atomic Value)만을 가져야 합니다. 하나의 컬럼에 여러 값을 넣으면 안 됩니다.\r\n\r\n```\r\n// 위반 예시\r\n| 학생 | 수강과목 |\r\n|------|----------------|\r\n| 홍길동 | 수학, 영어, 과학 |\r\n\r\n// 1NF 만족\r\n| 학생 | 수강과목 |\r\n|------|---------|\r\n| 홍길동 | 수학 |\r\n| 홍길동 | 영어 |\r\n| 홍길동 | 과학 |\r\n```\r\n\r\n**제2정규형 (2NF)**\r\n1NF를 만족하고, 부분 함수 종속을 제거합니다. 기본키의 일부분에만 종속되는 속성이 없어야 합니다.\r\n\r\n```\r\n// 위반 예시 (학번, 과목코드가 복합키)\r\n| 학번 | 과목코드 | 성적 | 학과 |\r\n// 학과는 학번에만 종속 → 부분 함수 종속\r\n\r\n// 2NF 만족: 테이블 분리\r\n학생(학번, 학과)\r\n수강(학번, 과목코드, 성적)\r\n```\r\n\r\n**제3정규형 (3NF)**\r\n2NF를 만족하고, 이행적 함수 종속을 제거합니다. A → B → C일 때, A → C인 이행적 종속이 없어야 합니다.\r\n\r\n```\r\n// 위반 예시\r\n| 학번 | 학과 | 학과사무실 |\r\n// 학번 → 학과 → 학과사무실 (이행적 종속)\r\n\r\n// 3NF 만족\r\n학생(학번, 학과)\r\n학과(학과, 학과사무실)\r\n```\r\n\r\n**BCNF (Boyce-Codd Normal Form)**\r\n3NF를 만족하고, 모든 결정자가 후보키여야 합니다. 3NF보다 더 엄격한 조건입니다.\r\n\r\n### 반정규화 (Denormalization)\r\n정규화가 항상 최선은 아닙니다. 과도한 정규화는 JOIN 연산을 증가시켜 성능을 저하시킬 수 있습니다. 읽기 성능이 중요한 경우 의도적으로 중복을 허용하는 반정규화를 수행하기도 합니다.\r\n\r\n### 면접 팁\r\n정규화의 목적(이상 현상 방지)과 트레이드오프(성능 저하)를 함께 설명하세요. 실무에서는 보통 3NF까지 적용하고, 성능이 필요한 부분에서 선택적으로 반정규화를 수행합니다.", + "difficulty": "BASIC", + "tags": "정규화, 제1정규형, 제2정규형, 제3정규형, 함수 종속성", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 32, + "title": "이상(Anomaly)", + "content": "데이터베이스에서 이상(Anomaly)현상이란 무엇이며, 어떤 종류가 있나요?", + "answer": "이상(Anomaly)이란 데이터베이스 테이블의 설계가 잘못되어 데이터를 삽입, 갱신, 삭제할 때 발생하는 논리적 오류를 말합니다. 주로 정규화가 충분히 이루어지지 않은 테이블에서 발생합니다.\r\n\r\n### 예시 테이블\r\n\r\n| 학번 | 이름 | 학과 | 학과전화 | 과목코드 | 성적 |\r\n|------|------|------|---------|---------|------|\r\n| 100 | 홍길동 | 컴공 | 1234 | CS101 | A |\r\n| 100 | 홍길동 | 컴공 | 1234 | CS102 | B |\r\n| 200 | 김영희 | 전자 | 5678 | EE201 | A |\r\n\r\n### 이상의 종류\r\n\r\n**1. 삽입 이상 (Insertion Anomaly)**\r\n새로운 데이터를 삽입할 때 불필요한 데이터도 함께 삽입해야 하는 문제입니다.\r\n\r\n예를 들어, 새로운 학과를 등록하고 싶은데 아직 해당 학과에 속한 학생이 없으면 학번, 이름, 과목코드, 성적 등에 NULL을 넣어야 합니다. 기본키에 NULL이 들어갈 수 없다면 삽입 자체가 불가능합니다.\r\n\r\n**2. 갱신 이상 (Update Anomaly)**\r\n중복된 데이터 중 일부만 수정되어 데이터 불일치가 발생하는 문제입니다.\r\n\r\n홍길동의 학과 전화번호가 변경되면 홍길동과 관련된 모든 행(2개)을 수정해야 합니다. 하나만 수정하면 같은 학과인데 전화번호가 다른 모순이 발생합니다.\r\n\r\n```sql\r\n-- 하나만 수정하면 불일치 발생\r\nUPDATE student_course SET 학과전화 = '9999'\r\nWHERE 학번 = 100 AND 과목코드 = 'CS101';\r\n-- CS102 행은 여전히 1234\r\n```\r\n\r\n**3. 삭제 이상 (Deletion Anomaly)**\r\n데이터를 삭제할 때 의도하지 않은 다른 데이터도 함께 삭제되는 문제입니다.\r\n\r\n김영희가 EE201 수강을 취소하면 해당 행을 삭제해야 하는데, 이때 김영희의 학생 정보(이름, 학과)와 전자학과의 전화번호 정보까지 모두 사라집니다.\r\n\r\n### 해결 방법\r\n\r\n**정규화를 통한 테이블 분리:**\r\n\r\n```sql\r\n-- 학생 테이블\r\nCREATE TABLE student (\r\n 학번 INT PRIMARY KEY,\r\n 이름 VARCHAR(50),\r\n 학과코드 VARCHAR(10) REFERENCES department(학과코드)\r\n);\r\n\r\n-- 학과 테이블\r\nCREATE TABLE department (\r\n 학과코드 VARCHAR(10) PRIMARY KEY,\r\n 학과명 VARCHAR(50),\r\n 학과전화 VARCHAR(20)\r\n);\r\n\r\n-- 수강 테이블\r\nCREATE TABLE enrollment (\r\n 학번 INT REFERENCES student(학번),\r\n 과목코드 VARCHAR(10),\r\n 성적 CHAR(1),\r\n PRIMARY KEY (학번, 과목코드)\r\n);\r\n```\r\n\r\n이렇게 분리하면 각 정보가 한 곳에만 저장되어 이상 현상이 해결됩니다.\r\n\r\n### 면접 팁\r\n이상 현상은 정규화의 필요성을 설명할 때 핵심적인 근거가 됩니다. 세 가지 이상 현상을 구체적인 예시와 함께 설명하고, 정규화로 어떻게 해결하는지 연결하여 답변하세요.", + "difficulty": "INTERMEDIATE", + "tags": "이상현상, 삽입이상, 갱신이상, 삭제이상, 정규화", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 33, + "title": "인덱스(INDEX)", + "content": "데이터베이스 인덱스(Index)란 무엇이며, 어떻게 동작하나요?", + "answer": "인덱스(Index)는 데이터베이스 테이블에 대한 검색 속도를 향상시키기 위한 자료구조입니다. 책의 색인(목차)처럼 원하는 데이터의 위치를 빠르게 찾을 수 있게 해줍니다.\r\n\r\n### 동작 원리\r\n\r\n인덱스가 없으면 테이블 전체를 순차적으로 탐색(Full Table Scan)해야 합니다. 인덱스가 있으면 B-Tree 등의 자료구조를 통해 O(log N) 시간에 데이터를 찾을 수 있습니다.\r\n\r\n```sql\r\n-- 인덱스 생성\r\nCREATE INDEX idx_employee_name ON employee(name);\r\n\r\n-- 인덱스를 활용한 조회\r\nSELECT * FROM employee WHERE name = '홍길동';\r\n```\r\n\r\n### 인덱스 종류\r\n\r\n**1. B-Tree 인덱스**\r\n가장 일반적인 인덱스 구조입니다. 범위 검색, 정렬에 유리합니다. 리프 노드에 실제 데이터의 포인터가 저장됩니다.\r\n\r\n**2. 해시 인덱스**\r\n해시 함수를 사용하여 O(1) 시간에 검색합니다. 동등 비교(=)에는 빠르지만 범위 검색(>, <)에는 사용할 수 없습니다.\r\n\r\n**3. 클러스터드 인덱스 (Clustered Index)**\r\n테이블의 물리적 정렬 순서와 인덱스 순서가 동일합니다. 테이블당 하나만 생성 가능하며, 보통 기본키에 자동으로 생성됩니다.\r\n\r\n**4. 논클러스터드 인덱스 (Non-clustered Index)**\r\n물리적 정렬과 독립적으로 별도의 공간에 인덱스를 저장합니다. 테이블당 여러 개 생성 가능합니다.\r\n\r\n### 인덱스 사용 시 고려사항\r\n\r\n**인덱스가 효과적인 경우:**\r\n- WHERE 절에 자주 사용되는 컬럼\r\n- JOIN에 사용되는 컬럼\r\n- ORDER BY, GROUP BY에 사용되는 컬럼\r\n- 카디널리티(Cardinality)가 높은 컬럼 (고유한 값이 많은 컬럼)\r\n\r\n**인덱스가 비효율적인 경우:**\r\n- 데이터가 적은 테이블\r\n- INSERT, UPDATE, DELETE가 빈번한 테이블\r\n- 카디널리티가 낮은 컬럼 (성별 등)\r\n\r\n### 인덱스의 단점\r\n- 추가적인 저장 공간이 필요합니다 (테이블 크기의 약 10%)\r\n- 데이터 변경(INSERT, UPDATE, DELETE) 시 인덱스도 함께 갱신되어 성능이 저하됩니다\r\n- 잘못된 인덱스는 오히려 성능을 악화시킬 수 있습니다\r\n\r\n```sql\r\n-- 실행 계획으로 인덱스 사용 여부 확인\r\nEXPLAIN SELECT * FROM employee WHERE name = '홍길동';\r\n```\r\n\r\n### 면접 팁\r\n인덱스의 내부 구조(B-Tree)를 이해하고, 인덱스를 사용해야 할 때와 사용하지 않아야 할 때를 구분할 수 있어야 합니다. 커버링 인덱스, 복합 인덱스의 컬럼 순서 등 심화 주제도 준비하세요.", + "difficulty": "INTERMEDIATE", + "tags": "인덱스, B-Tree, 클러스터 인덱스, 해시 인덱스, 쿼리 최적화", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 34, + "title": "트랜잭션(Transaction)", + "content": "트랜잭션이란 무엇이며, ACID 속성에 대해 설명해주세요.", + "answer": "트랜잭션(Transaction)은 데이터베이스의 상태를 변환시키는 하나의 논리적 작업 단위입니다. 여러 개의 SQL 연산이 하나의 작업으로 묶여, 모두 성공하거나 모두 실패해야 합니다.\r\n\r\n### 트랜잭션 예시: 계좌 이체\r\n\r\n```sql\r\nBEGIN TRANSACTION;\r\n\r\n-- A 계좌에서 10만원 출금\r\nUPDATE account SET balance = balance - 100000 WHERE id = 'A';\r\n\r\n-- B 계좌에 10만원 입금\r\nUPDATE account SET balance = balance + 100000 WHERE id = 'B';\r\n\r\n-- 모두 성공하면 커밋\r\nCOMMIT;\r\n\r\n-- 하나라도 실패하면 롤백\r\n-- ROLLBACK;\r\n```\r\n\r\n### ACID 속성\r\n\r\n**1. 원자성 (Atomicity)**\r\n트랜잭션의 모든 연산이 완전히 수행되거나, 전혀 수행되지 않아야 합니다. \"All or Nothing\" 원칙입니다. 중간에 실패하면 이전 상태로 롤백됩니다.\r\n\r\n**2. 일관성 (Consistency)**\r\n트랜잭션 수행 전후에 데이터베이스가 일관된 상태를 유지해야 합니다. 예를 들어, 계좌 이체 후 두 계좌의 합은 이전과 동일해야 합니다.\r\n\r\n**3. 격리성 (Isolation)**\r\n동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않아야 합니다. 각 트랜잭션은 독립적으로 실행되는 것처럼 보여야 합니다.\r\n\r\n**4. 지속성 (Durability)**\r\n성공적으로 완료(COMMIT)된 트랜잭션의 결과는 영구적으로 데이터베이스에 반영되어야 합니다. 시스템 장애가 발생해도 유지됩니다.\r\n\r\n### 트랜잭션 상태\r\n\r\n```\r\nActive → Partially Committed → Committed\r\n ↓ ↓\r\nFailed Failed\r\n ↓ ↓\r\nAborted Aborted\r\n```\r\n\r\n- **Active**: 트랜잭션이 실행 중인 상태\r\n- **Partially Committed**: 마지막 연산까지 실행했지만 아직 커밋하지 않은 상태\r\n- **Committed**: 트랜잭션이 성공적으로 완료된 상태\r\n- **Failed**: 오류가 발생하여 중단된 상태\r\n- **Aborted**: 트랜잭션이 취소되고 롤백된 상태\r\n\r\n### 트랜잭션 관리 (Spring)\r\n\r\n```java\r\n@Transactional\r\npublic void transfer(String from, String to, int amount) {\r\n accountRepository.withdraw(from, amount);\r\n accountRepository.deposit(to, amount);\r\n}\r\n// 메서드 내에서 예외 발생 시 자동 롤백\r\n```\r\n\r\n### 면접 팁\r\nACID 각 속성의 정의뿐만 아니라, 각 속성이 어떤 메커니즘으로 보장되는지 설명할 수 있으면 좋습니다. 예를 들어 원자성은 Undo Log, 지속성은 Redo Log를 통해 보장됩니다.", + "difficulty": "INTERMEDIATE", + "tags": "트랜잭션, ACID, 커밋, 롤백, 원자성", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 35, + "title": "트랜잭션 격리 수준", + "content": "트랜잭션의 격리 수준(Isolation Level)에 대해 설명하고, 각 수준에서 발생할 수 있는 문제점을 말해주세요.", + "answer": "트랜잭션 격리 수준(Isolation Level)은 동시에 여러 트랜잭션이 실행될 때 각 트랜잭션이 다른 트랜잭션의 변경 사항을 어느 정도까지 볼 수 있는지를 결정하는 정책입니다.\r\n\r\n### 동시성 문제\r\n\r\n**1. Dirty Read**\r\n커밋되지 않은 다른 트랜잭션의 데이터를 읽는 현상입니다. 해당 트랜잭션이 롤백되면 잘못된 데이터를 읽은 것이 됩니다.\r\n\r\n**2. Non-repeatable Read**\r\n같은 트랜잭션 내에서 같은 쿼리를 두 번 실행했을 때, 다른 트랜잭션의 수정으로 인해 다른 결과가 나오는 현상입니다.\r\n\r\n**3. Phantom Read**\r\n같은 쿼리를 두 번 실행했을 때, 다른 트랜잭션의 삽입/삭제로 인해 결과 집합의 행 수가 달라지는 현상입니다.\r\n\r\n### 격리 수준\r\n\r\n**1. READ UNCOMMITTED (레벨 0)**\r\n커밋되지 않은 데이터도 읽을 수 있습니다. 가장 낮은 격리 수준으로, Dirty Read가 발생합니다.\r\n\r\n```sql\r\nSET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;\r\n-- 트랜잭션 B가 커밋하지 않은 데이터도 조회 가능\r\n```\r\n\r\n**2. READ COMMITTED (레벨 1)**\r\n커밋된 데이터만 읽을 수 있습니다. Dirty Read는 방지하지만 Non-repeatable Read가 발생할 수 있습니다. Oracle, PostgreSQL의 기본 격리 수준입니다.\r\n\r\n**3. REPEATABLE READ (레벨 2)**\r\n트랜잭션이 시작된 시점의 데이터를 일관되게 읽습니다. Non-repeatable Read는 방지하지만 Phantom Read가 발생할 수 있습니다. MySQL(InnoDB)의 기본 격리 수준입니다.\r\n\r\n**4. SERIALIZABLE (레벨 3)**\r\n가장 엄격한 격리 수준으로, 트랜잭션을 순차적으로 실행하는 것처럼 동작합니다. 모든 동시성 문제를 방지하지만 성능이 크게 저하됩니다.\r\n\r\n### 격리 수준별 문제 발생 여부\r\n\r\n| 격리 수준 | Dirty Read | Non-repeatable Read | Phantom Read |\r\n|-----------|-----------|-------------------|-------------|\r\n| READ UNCOMMITTED | O | O | O |\r\n| READ COMMITTED | X | O | O |\r\n| REPEATABLE READ | X | X | O (MySQL에서는 X) |\r\n| SERIALIZABLE | X | X | X |\r\n\r\n### MVCC (Multi-Version Concurrency Control)\r\nMySQL InnoDB는 MVCC를 사용하여 잠금 없이 일관된 읽기를 제공합니다. 데이터를 변경할 때 이전 버전을 Undo 영역에 보관하고, 각 트랜잭션은 자신의 시작 시점에 맞는 버전을 읽습니다.\r\n\r\n```sql\r\n-- MySQL에서 격리 수준 확인\r\nSELECT @@transaction_isolation;\r\n\r\n-- 격리 수준 변경\r\nSET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;\r\n```\r\n\r\n### 면접 팁\r\nMySQL의 REPEATABLE READ에서는 Next-Key Lock을 통해 Phantom Read까지 방지한다는 점을 알면 차별화됩니다. 격리 수준은 동시성과 일관성의 트레이드오프이므로, 실무에서 어떤 기준으로 선택하는지도 설명할 수 있으면 좋습니다.", + "difficulty": "ADVANCED", + "tags": "격리수준, Dirty Read, Phantom Read, Non-repeatable Read, MVCC", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 36, + "title": "저장 프로시저(Stored Procedure)", + "content": "저장 프로시저(Stored Procedure)란 무엇이며, 장단점에 대해 설명해주세요.", + "answer": "저장 프로시저(Stored Procedure)는 일련의 SQL 문을 하나의 함수처럼 실행하기 위해 데이터베이스에 미리 저장해 둔 SQL 집합입니다. 자주 사용하는 복잡한 쿼리를 재사용 가능한 단위로 만들어 호출합니다.\r\n\r\n### 기본 문법\r\n\r\n```sql\r\n-- 저장 프로시저 생성\r\nDELIMITER //\r\nCREATE PROCEDURE GetEmployeeByDept(IN dept_id INT)\r\nBEGIN\r\n SELECT employee_id, name, salary\r\n FROM employee\r\n WHERE department_id = dept_id\r\n ORDER BY salary DESC;\r\nEND //\r\nDELIMITER ;\r\n\r\n-- 저장 프로시저 호출\r\nCALL GetEmployeeByDept(10);\r\n```\r\n\r\n### 매개변수 유형\r\n\r\n```sql\r\nCREATE PROCEDURE TransferMoney(\r\n IN from_account INT, -- 입력 매개변수\r\n IN to_account INT,\r\n IN amount DECIMAL(10,2),\r\n OUT result VARCHAR(50) -- 출력 매개변수\r\n)\r\nBEGIN\r\n DECLARE from_balance DECIMAL(10,2);\r\n\r\n SELECT balance INTO from_balance\r\n FROM accounts WHERE id = from_account;\r\n\r\n IF from_balance >= amount THEN\r\n UPDATE accounts SET balance = balance - amount WHERE id = from_account;\r\n UPDATE accounts SET balance = balance + amount WHERE id = to_account;\r\n SET result = 'SUCCESS';\r\n ELSE\r\n SET result = 'INSUFFICIENT_BALANCE';\r\n END IF;\r\nEND;\r\n```\r\n\r\n### 장점\r\n\r\n**1. 성능 향상**\r\n- 최초 실행 시 컴파일되어 캐시에 저장되므로, 이후 호출 시 컴파일 과정 없이 빠르게 실행됩니다\r\n- 여러 SQL을 하나의 호출로 처리하여 네트워크 트래픽이 감소합니다\r\n\r\n**2. 보안 강화**\r\n- 테이블에 직접 접근하지 않고 프로시저를 통해서만 데이터에 접근하도록 제어할 수 있습니다\r\n- SQL Injection 공격을 방지하는 데 도움이 됩니다\r\n\r\n**3. 유지보수 편의성**\r\n- 비즈니스 로직 변경 시 애플리케이션을 재배포하지 않고 프로시저만 수정하면 됩니다\r\n\r\n### 단점\r\n\r\n**1. 디버깅 어려움**\r\n- IDE에서 제공하는 디버깅 도구가 부족합니다\r\n- 오류 추적이 어렵습니다\r\n\r\n**2. 데이터베이스 종속성**\r\n- DBMS마다 문법이 다르므로 이식성이 낮습니다\r\n- 데이터베이스 변경 시 프로시저를 모두 다시 작성해야 합니다\r\n\r\n**3. 버전 관리 어려움**\r\n- Git 등으로 관리하기 어렵습니다\r\n- 애플리케이션 코드와 분리되어 있어 전체 로직 파악이 어렵습니다\r\n\r\n**4. 서버 부하 집중**\r\n- 비즈니스 로직이 DB 서버에 집중되어 스케일아웃이 어렵습니다\r\n\r\n### 저장 프로시저 vs 저장 함수\r\n\r\n| 구분 | 프로시저 | 함수 |\r\n|------|---------|------|\r\n| 반환값 | 없거나 OUT 파라미터 | 반드시 하나의 값 반환 |\r\n| SQL에서 호출 | CALL로 호출 | SELECT에서 사용 가능 |\r\n| 트랜잭션 | 사용 가능 | 제한적 |\r\n\r\n### 면접 팁\r\n최근에는 JPA 등 ORM의 사용이 보편화되면서 저장 프로시저 사용이 줄어드는 추세입니다. 그러나 대량 데이터 처리나 레거시 시스템에서는 여전히 사용됩니다. 장단점을 균형 있게 설명하세요.", + "difficulty": "INTERMEDIATE", + "tags": "저장 프로시저, 함수, 트리거, 성능, SQL", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 37, + "title": "레디스(Redis)", + "content": "Redis란 무엇이며, 어떤 상황에서 사용하나요?", + "answer": "Redis(Remote Dictionary Server)는 오픈소스 인메모리 Key-Value 데이터 구조 저장소입니다. 데이터를 메모리에 저장하여 매우 빠른 읽기/쓰기 성능을 제공합니다.\r\n\r\n### 주요 특징\r\n\r\n**1. 인메모리 저장소**\r\n모든 데이터를 RAM에 저장하여 디스크 기반 DB보다 훨씬 빠릅니다. 평균 읽기/쓰기 속도가 1ms 이하입니다.\r\n\r\n**2. 다양한 자료구조 지원**\r\n\r\n```\r\n- String: 가장 기본적인 자료형\r\n SET user:1:name \"홍길동\"\r\n GET user:1:name\r\n\r\n- List: 연결 리스트\r\n LPUSH queue \"task1\"\r\n RPOP queue\r\n\r\n- Set: 중복 없는 집합\r\n SADD tags \"java\" \"spring\" \"redis\"\r\n SMEMBERS tags\r\n\r\n- Sorted Set: 점수(score) 기반 정렬 집합\r\n ZADD ranking 100 \"player1\"\r\n ZRANGE ranking 0 -1 WITHSCORES\r\n\r\n- Hash: 필드-값 쌍의 집합\r\n HSET user:1 name \"홍길동\" age 25\r\n HGETALL user:1\r\n```\r\n\r\n**3. 싱글 스레드**\r\n이벤트 루프 기반의 싱글 스레드로 동작합니다. Race Condition이 발생하지 않아 원자적 연산이 보장됩니다. 단, 오래 걸리는 명령(KEYS * 등)은 전체 서버를 블로킹하므로 주의해야 합니다.\r\n\r\n**4. 영속성 (Persistence)**\r\n인메모리이지만 데이터를 디스크에 저장하는 방법을 제공합니다:\r\n- **RDB (Snapshot)**: 특정 시점의 메모리 스냅샷을 저장\r\n- **AOF (Append Only File)**: 모든 쓰기 명령을 로그에 기록\r\n\r\n### 주요 사용 사례\r\n\r\n**1. 캐시 (Cache)**\r\n자주 조회되는 데이터를 Redis에 캐싱하여 DB 부하를 줄입니다.\r\n\r\n```java\r\n// Spring에서 Redis 캐시 사용\r\n@Cacheable(value = \"users\", key = \"#userId\")\r\npublic User getUser(Long userId) {\r\n return userRepository.findById(userId).orElseThrow();\r\n}\r\n```\r\n\r\n**2. 세션 저장소**\r\n분산 서버 환경에서 세션을 Redis에 저장하여 세션 클러스터링을 구현합니다.\r\n\r\n**3. 메시지 큐**\r\nPub/Sub 기능을 활용하여 메시지 브로커로 사용합니다.\r\n\r\n**4. 실시간 랭킹**\r\nSorted Set을 활용하여 실시간 리더보드를 구현합니다.\r\n\r\n**5. 분산 락 (Distributed Lock)**\r\n여러 서버에서 공유 자원에 대한 동기화를 구현합니다.\r\n\r\n### 주의사항\r\n- 메모리 용량이 한정적이므로 TTL(Time To Live) 설정이 중요합니다\r\n- 캐시 전략(Cache Aside, Write Through 등)을 적절히 선택해야 합니다\r\n- 캐시 침투(Cache Penetration), 캐시 눈사태(Cache Avalanche) 등의 문제를 고려해야 합니다\r\n\r\n### 면접 팁\r\nRedis를 단순히 \"빠른 캐시\"로만 설명하지 말고, 다양한 자료구조와 구체적인 사용 사례를 함께 설명하세요. 캐시 전략과 관련된 문제(Cache Aside 패턴, TTL 전략 등)도 함께 준비하면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "Redis, 인메모리, 캐시, NoSQL, 자료구조", + "categorySlug": "database", + "categoryName": "데이터베이스", + "categoryId": 1, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 38, + "title": "Java 컴파일 과정", + "content": "Java 소스 코드가 실행되기까지의 과정을 설명해주세요.", + "answer": "Java는 \"Write Once, Run Anywhere\" 철학에 따라, 소스 코드를 플랫폼 독립적인 바이트코드로 컴파일한 후 JVM에서 실행합니다.\r\n\r\n### 컴파일 및 실행 과정\r\n\r\n```\r\n1. 소스 코드 작성 (.java)\r\n ↓\r\n2. javac 컴파일러로 컴파일\r\n ↓\r\n3. 바이트코드 생성 (.class)\r\n ↓\r\n4. JVM의 클래스 로더가 바이트코드를 메모리에 로드\r\n ↓\r\n5. 바이트코드 검증 (Bytecode Verifier)\r\n ↓\r\n6. 실행 엔진 (Execution Engine)\r\n ├── 인터프리터: 바이트코드를 한 줄씩 해석/실행\r\n └── JIT 컴파일러: 자주 실행되는 코드를 네이티브 코드로 변환\r\n```\r\n\r\n### 각 단계 상세\r\n\r\n**1. javac 컴파일러**\r\n```bash\r\njavac Hello.java → Hello.class (바이트코드)\r\n```\r\n- 소스 코드의 문법 검사, 타입 체크\r\n- 바이트코드(.class) 생성\r\n- 바이트코드는 JVM이 이해할 수 있는 중간 코드\r\n\r\n**2. 클래스 로더 (Class Loader)**\r\n```\r\nBootstrap ClassLoader → Extension ClassLoader → Application ClassLoader\r\n(JDK 핵심 클래스) (확장 라이브러리) (사용자 클래스)\r\n```\r\n- 런타임에 필요한 클래스를 동적으로 로드\r\n- 위임 모델(Delegation Model): 상위 클래스 로더에 먼저 위임\r\n- Loading → Linking(Verify, Prepare, Resolve) → Initialization\r\n\r\n**3. 실행 엔진**\r\n- **인터프리터**: 바이트코드를 한 줄씩 해석하여 실행. 초기 실행은 빠르지만 반복 실행 시 느림\r\n- **JIT(Just-In-Time) 컴파일러**: 반복 실행되는 코드(핫스팟)를 감지하여 네이티브 기계어로 컴파일. 이후 실행은 매우 빠름\r\n- 인터프리터와 JIT가 함께 동작하여 최적의 성능 달성\r\n\r\n### Java vs C/C++ 컴파일 비교\r\n\r\n| 구분 | Java | C/C++ |\r\n|------|------|-------|\r\n| 컴파일 결과 | 바이트코드 | 네이티브 코드 |\r\n| 실행 환경 | JVM 필요 | OS에서 직접 실행 |\r\n| 플랫폼 의존성 | 독립적 | 종속적 |\r\n| 성능 | JIT로 준수 | 빠름 |\r\n\r\n### 면접 팁\r\n\"Java는 컴파일 언어인가 인터프리터 언어인가?\" 라는 질문에 대해, Java는 두 가지 모두에 해당한다고 답하세요. 소스 코드를 바이트코드로 컴파일하고(컴파일), 바이트코드를 JVM에서 인터프리터/JIT로 실행합니다(인터프리트).", + "difficulty": "BASIC", + "tags": "Java 컴파일, javac, 바이트코드, JIT, 클래스 로더", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 39, + "title": "Call by Value vs Call by Reference", + "content": "Java는 Call by Value인가요, Call by Reference인가요?", + "answer": "Java는 항상 **Call by Value(값에 의한 호출)**입니다. 메서드에 인자를 전달할 때 값을 복사하여 전달합니다. 객체를 전달할 때도 객체 자체가 아닌 참조값(주소값)을 복사하여 전달합니다.\r\n\r\n### 기본 타입 전달\r\n\r\n```java\r\nvoid changeValue(int num) {\r\n num = 100; // 복사된 값을 변경 → 원본에 영향 없음\r\n}\r\n\r\nint x = 10;\r\nchangeValue(x);\r\nSystem.out.println(x); // 10 (변경 안 됨)\r\n```\r\n\r\n### 객체(참조 타입) 전달\r\n\r\n```java\r\nvoid changeObject(User user) {\r\n user.setName(\"김길동\"); // 같은 객체를 참조하므로 원본 변경 가능\r\n}\r\n\r\nUser user = new User(\"홍길동\");\r\nchangeObject(user);\r\nSystem.out.println(user.getName()); // \"김길동\" (변경됨!)\r\n```\r\n\r\n**이것은 Call by Reference가 아닙니다.** 참조값(주소)이 복사된 것입니다.\r\n\r\n```java\r\nvoid replaceObject(User user) {\r\n user = new User(\"완전 새 객체\"); // 지역 변수가 새 객체를 가리킴\r\n // 원본 참조에는 영향 없음\r\n}\r\n\r\nUser user = new User(\"홍길동\");\r\nreplaceObject(user);\r\nSystem.out.println(user.getName()); // \"홍길동\" (변경 안 됨!)\r\n```\r\n\r\n### 핵심 차이\r\n\r\n```\r\nCall by Value (Java):\r\n 원본 변수의 값(또는 참조값)을 복사하여 전달\r\n 메서드 내에서 매개변수 자체를 변경해도 원본에 영향 없음\r\n\r\nCall by Reference (C++ 등):\r\n 원본 변수 자체의 참조(별명)를 전달\r\n 메서드 내에서 매개변수를 변경하면 원본도 변경됨\r\n```\r\n\r\n```\r\nJava 메모리:\r\nMain: user ──→ [User: \"홍길동\"] (Heap)\r\n ↑\r\nMethod: user ──→──────┘ (복사된 참조값이 같은 객체를 가리킴)\r\n\r\nreplaceObject 호출 후:\r\nMain: user ──→ [User: \"홍길동\"] (원본 그대로)\r\nMethod: user ──→ [User: \"완전 새 객체\"] (지역 변수가 새 객체 가리킴)\r\n```\r\n\r\n### 면접 팁\r\nJava는 \"참조를 값으로 전달(pass reference by value)\"합니다. 객체의 필드를 변경할 수 있는 것은 같은 객체를 참조하기 때문이며, 참조 변수 자체를 교체할 수는 없습니다. 이 차이를 코드 예시로 명확하게 설명하세요.", + "difficulty": "BASIC", + "tags": "Call by Value, Call by Reference, 참조, 값 복사, Java", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 40, + "title": "String & StringBuffer & StringBuilder", + "content": "String, StringBuffer, StringBuilder의 차이점에 대해 설명해주세요.", + "answer": "### String - 불변 (Immutable)\r\nString은 한 번 생성되면 값을 변경할 수 없습니다. 문자열을 조작하면 새로운 String 객체가 생성됩니다.\r\n\r\n```java\r\nString str = \"Hello\";\r\nstr = str + \" World\"; // 새로운 String 객체 생성, 기존 \"Hello\"는 GC 대상\r\n\r\n// 내부적으로:\r\n// \"Hello\" 객체 → 버려짐\r\n// \"Hello World\" 객체 → str이 새로 참조\r\n```\r\n\r\n### StringBuffer - 가변, 스레드 안전\r\n내부 버퍼를 사용하여 문자열을 변경합니다. synchronized 키워드로 스레드 안전성을 보장합니다.\r\n\r\n```java\r\nStringBuffer sb = new StringBuffer(\"Hello\");\r\nsb.append(\" World\"); // 같은 객체 내에서 변경 (새 객체 생성 안 함)\r\nSystem.out.println(sb.toString()); // \"Hello World\"\r\n```\r\n\r\n### StringBuilder - 가변, 스레드 비안전\r\nStringBuffer와 동일하지만 동기화가 없어 단일 스레드 환경에서 더 빠릅니다.\r\n\r\n```java\r\nStringBuilder sb = new StringBuilder(\"Hello\");\r\nsb.append(\" World\");\r\nsb.insert(5, \",\");\r\nsb.delete(0, 5);\r\nsb.reverse();\r\n```\r\n\r\n### 비교\r\n\r\n| 특성 | String | StringBuffer | StringBuilder |\r\n|------|--------|-------------|--------------|\r\n| 가변성 | 불변 | 가변 | 가변 |\r\n| 스레드 안전 | O (불변) | O (synchronized) | X |\r\n| 성능 | 느림 (문자열 연산) | 중간 | 빠름 |\r\n| 사용 상황 | 변경 적은 문자열 | 멀티스레드 | 단일 스레드 |\r\n\r\n### 성능 비교\r\n\r\n```java\r\n// Bad: 반복문에서 String 연결 → O(n^2)\r\nString result = \"\";\r\nfor (int i = 0; i < 10000; i++) {\r\n result += i; // 매번 새 String 객체 생성\r\n}\r\n\r\n// Good: StringBuilder 사용 → O(n)\r\nStringBuilder sb = new StringBuilder();\r\nfor (int i = 0; i < 10000; i++) {\r\n sb.append(i);\r\n}\r\nString result = sb.toString();\r\n```\r\n\r\n### String이 불변인 이유\r\n1. **String Pool**: 같은 값의 String을 공유하여 메모리 절약\r\n2. **보안**: 네트워크 연결, 파일 경로 등에 사용되는 문자열 보호\r\n3. **해시코드 캐싱**: HashMap의 키로 사용 시 해시값을 캐싱 가능\r\n4. **스레드 안전**: 불변이므로 동기화 불필요\r\n\r\n### 면접 팁\r\n\"반복문에서 문자열을 연결할 때 String 대신 StringBuilder를 사용해야 하는 이유\"는 매우 자주 출제됩니다. Java 컴파일러가 단순한 String 연결(+)을 StringBuilder로 최적화해주지만, 반복문 내에서는 매번 새 StringBuilder가 생성되므로 직접 사용해야 합니다.", + "difficulty": "BASIC", + "tags": "String, StringBuffer, StringBuilder, 불변, 가변", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 41, + "title": "JVM 구조", + "content": "JVM의 구조와 메모리 영역에 대해 설명해주세요.", + "answer": "JVM(Java Virtual Machine)은 자바 바이트코드를 실행하는 가상 머신으로, 플랫폼 독립성을 제공합니다.\r\n\r\n### JVM 구조\r\n\r\n```\r\n┌─────────────────────────────────────┐\r\n│ JVM │\r\n│ ┌──────────────────────┐ │\r\n│ │ Class Loader │ │\r\n│ └──────────┬───────────┘ │\r\n│ ↓ │\r\n│ ┌──────────────────────────────┐ │\r\n│ │ Runtime Data Area │ │\r\n│ │ ┌────────┬────────────────┐ │ │\r\n│ │ │ Method │ Heap │ │ │\r\n│ │ │ Area │ │ │ │\r\n│ │ ├────────┼────────────────┤ │ │\r\n│ │ │ Stack │ PC Register │ │ │\r\n│ │ │ │ Native Stack │ │ │\r\n│ │ └────────┴────────────────┘ │ │\r\n│ └──────────────────────────────┘ │\r\n│ ↓ │\r\n│ ┌──────────────────────┐ │\r\n│ │ Execution Engine │ │\r\n│ │ Interpreter + JIT │ │\r\n│ │ + GC │ │\r\n│ └──────────────────────┘ │\r\n└─────────────────────────────────────┘\r\n```\r\n\r\n### 런타임 데이터 영역\r\n\r\n**1. 메소드 영역 (Method Area / Metaspace)**\r\n- 클래스 정보, 정적 변수, 상수 풀, 메서드 정보 저장\r\n- 모든 스레드가 공유\r\n- Java 8부터 Metaspace로 변경 (네이티브 메모리 사용)\r\n\r\n**2. 힙 (Heap)**\r\n- 객체와 배열이 저장되는 영역\r\n- 모든 스레드가 공유\r\n- GC(Garbage Collection)의 대상\r\n\r\n```\r\nHeap 구조:\r\n┌──────────────────────────────┐\r\n│ Young Generation │\r\n│ ┌──────┬───────┬──────┐ │\r\n│ │ Eden │ S0 │ S1 │ │ ← Minor GC\r\n│ └──────┴───────┴──────┘ │\r\n├──────────────────────────────┤\r\n│ Old Generation │ ← Major GC (Full GC)\r\n└──────────────────────────────┘\r\n```\r\n\r\n**3. 스택 (Stack)**\r\n- 메서드 호출 시 생성되는 스택 프레임 저장\r\n- 지역 변수, 매개변수, 리턴 주소\r\n- 스레드마다 독립적\r\n\r\n```\r\n스택 프레임:\r\n┌─────────────────┐\r\n│ Local Variables │ (지역 변수)\r\n├─────────────────┤\r\n│ Operand Stack │ (연산용 스택)\r\n├─────────────────┤\r\n│ Frame Data │ (리턴 주소 등)\r\n└─────────────────┘\r\n```\r\n\r\n**4. PC 레지스터**\r\n- 현재 실행 중인 명령어의 주소 저장\r\n- 스레드마다 독립적\r\n\r\n**5. 네이티브 메서드 스택**\r\n- JNI를 통해 호출되는 네이티브 코드용 스택\r\n\r\n### 메모리 할당 예시\r\n\r\n```java\r\npublic class Example {\r\n static int classVar = 10; // Method Area\r\n\r\n public void method() {\r\n int localVar = 20; // Stack\r\n String str = \"hello\"; // str은 Stack, \"hello\"는 String Pool\r\n Object obj = new Object(); // obj는 Stack, 객체는 Heap\r\n }\r\n}\r\n```\r\n\r\n### 면접 팁\r\nJVM 메모리 구조에서 \"스레드 공유 영역\"(Heap, Method Area)과 \"스레드 독립 영역\"(Stack, PC Register)을 구분하세요. Heap의 Young/Old Generation 구조와 GC의 동작 원리도 중요합니다.", + "difficulty": "INTERMEDIATE", + "tags": "JVM, 메모리 구조, 힙, 스택, 메소드 영역, 가비지 컬렉션", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 42, + "title": "Casting(형변환)", + "content": "Java에서 업캐스팅과 다운캐스팅에 대해 설명해주세요.", + "answer": "캐스팅(형변환)은 데이터의 타입을 변환하는 것입니다. Java에서 참조 타입의 캐스팅은 상속 관계에서 업캐스팅과 다운캐스팅으로 나뉩니다.\r\n\r\n### 업캐스팅 (Upcasting)\r\n하위 클래스 타입을 상위 클래스 타입으로 변환합니다. **자동(묵시적)**으로 수행됩니다.\r\n\r\n```java\r\nclass Animal {\r\n void eat() { System.out.println(\"먹는다\"); }\r\n}\r\n\r\nclass Dog extends Animal {\r\n void bark() { System.out.println(\"멍멍\"); }\r\n}\r\n\r\n// 업캐스팅: 자동 변환\r\nAnimal animal = new Dog(); // Dog → Animal\r\nanimal.eat(); // O - Animal의 메서드\r\n// animal.bark(); // X - Animal 타입이므로 Dog의 메서드 접근 불가\r\n```\r\n\r\n### 다운캐스팅 (Downcasting)\r\n상위 클래스 타입을 하위 클래스 타입으로 변환합니다. **명시적**으로 수행해야 합니다.\r\n\r\n```java\r\nAnimal animal = new Dog(); // 업캐스팅\r\nDog dog = (Dog) animal; // 다운캐스팅: 명시적 변환 필요\r\ndog.bark(); // O - Dog의 메서드 사용 가능\r\n\r\n// 잘못된 다운캐스팅 → ClassCastException\r\nAnimal animal2 = new Animal();\r\nDog dog2 = (Dog) animal2; // 런타임 에러! Animal은 Dog가 아님\r\n```\r\n\r\n### instanceof로 안전한 다운캐스팅\r\n\r\n```java\r\nif (animal instanceof Dog) {\r\n Dog dog = (Dog) animal;\r\n dog.bark();\r\n}\r\n\r\n// Java 16+ Pattern Matching\r\nif (animal instanceof Dog dog) {\r\n dog.bark(); // 자동 캐스팅\r\n}\r\n```\r\n\r\n### 기본 타입 형변환\r\n\r\n```java\r\n// 자동 형변환 (Widening): 작은 → 큰\r\nint intVal = 10;\r\ndouble doubleVal = intVal; // int → double (자동)\r\n\r\n// 강제 형변환 (Narrowing): 큰 → 작은\r\ndouble d = 3.14;\r\nint i = (int) d; // double → int (명시적, 소수점 손실)\r\n\r\n// 형변환 순서\r\n// byte → short → int → long → float → double\r\n// char ↗\r\n```\r\n\r\n### 면접 팁\r\n업캐스팅은 다형성의 기반입니다. `Animal animal = new Dog()`에서 animal.eat()을 호출하면 Dog의 오버라이딩된 메서드가 실행된다는 점(런타임 다형성)을 설명할 수 있어야 합니다.", + "difficulty": "BASIC", + "tags": "캐스팅, 업캐스팅, 다운캐스팅, 형변환, instanceof", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 43, + "title": "오토 박싱 & 오토언박싱", + "content": "오토 박싱과 오토언박싱이란 무엇이며, 주의사항은 무엇인가요?", + "answer": "### 오토박싱 (Auto-boxing)\r\n기본 타입을 래퍼 클래스로 자동 변환하는 것입니다.\r\n\r\n```java\r\nint primitive = 10;\r\nInteger wrapper = primitive; // 오토박싱: int → Integer\r\n// 내부적으로: Integer wrapper = Integer.valueOf(primitive);\r\n```\r\n\r\n### 오토언박싱 (Auto-unboxing)\r\n래퍼 클래스를 기본 타입으로 자동 변환하는 것입니다.\r\n\r\n```java\r\nInteger wrapper = Integer.valueOf(20);\r\nint primitive = wrapper; // 오토언박싱: Integer → int\r\n// 내부적으로: int primitive = wrapper.intValue();\r\n```\r\n\r\n### 기본 타입과 래퍼 클래스 대응\r\n\r\n| 기본 타입 | 래퍼 클래스 |\r\n|-----------|-----------|\r\n| byte | Byte |\r\n| short | Short |\r\n| int | Integer |\r\n| long | Long |\r\n| float | Float |\r\n| double | Double |\r\n| char | Character |\r\n| boolean | Boolean |\r\n\r\n### 주의사항\r\n\r\n**1. 성능 이슈**\r\n```java\r\n// Bad: 오토박싱으로 인한 성능 저하\r\nLong sum = 0L;\r\nfor (long i = 0; i < 1000000; i++) {\r\n sum += i; // 매번 오토박싱 발생! Long 객체 생성\r\n}\r\n\r\n// Good: 기본 타입 사용\r\nlong sum = 0L;\r\nfor (long i = 0; i < 1000000; i++) {\r\n sum += i; // 기본 타입 연산\r\n}\r\n```\r\n\r\n**2. == 비교 주의**\r\n```java\r\nInteger a = 127;\r\nInteger b = 127;\r\nSystem.out.println(a == b); // true (Integer 캐시: -128 ~ 127)\r\n\r\nInteger c = 128;\r\nInteger d = 128;\r\nSystem.out.println(c == d); // false! (캐시 범위 밖)\r\nSystem.out.println(c.equals(d)); // true (값 비교)\r\n```\r\n\r\n**3. NullPointerException**\r\n```java\r\nInteger wrapper = null;\r\nint primitive = wrapper; // NPE! null을 기본 타입으로 변환 불가\r\n```\r\n\r\n**4. 컬렉션에서의 사용**\r\n```java\r\n// 컬렉션은 기본 타입을 직접 저장 불가\r\nList list = new ArrayList<>(); // Integer 사용\r\nlist.add(10); // 오토박싱\r\nint value = list.get(0); // 오토언박싱\r\n```\r\n\r\n### 면접 팁\r\nInteger 캐시(-128~127)로 인해 `==` 비교 결과가 달라지는 것은 자주 출제됩니다. 래퍼 클래스 비교 시 반드시 `equals()`를 사용해야 합니다. 반복문에서 오토박싱으로 인한 성능 저하도 중요한 포인트입니다.", + "difficulty": "BASIC", + "tags": "오토박싱, 오토언박싱, Wrapper 클래스, 기본 타입, 성능", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 44, + "title": "Thread 활용", + "content": "Java에서 스레드를 생성하고 활용하는 방법에 대해 설명해주세요.", + "answer": "### 스레드 생성 방법\r\n\r\n**1. Thread 상속**\r\n```java\r\nclass MyThread extends Thread {\r\n public void run() {\r\n System.out.println(\"Thread: \" + Thread.currentThread().getName());\r\n }\r\n}\r\nnew MyThread().start();\r\n```\r\n\r\n**2. Runnable 인터페이스 구현 (권장)**\r\n```java\r\nRunnable task = () -> System.out.println(\"Runnable: \" + Thread.currentThread().getName());\r\nnew Thread(task).start();\r\n```\r\n\r\n**3. ExecutorService (스레드 풀) - 실무 권장**\r\n```java\r\nExecutorService executor = Executors.newFixedThreadPool(5);\r\n\r\n// Runnable 제출\r\nexecutor.submit(() -> System.out.println(\"Task executed\"));\r\n\r\n// Callable 제출 (결과 반환)\r\nFuture future = executor.submit(() -> {\r\n Thread.sleep(1000);\r\n return \"Result\";\r\n});\r\nString result = future.get(); // 결과 대기\r\n\r\nexecutor.shutdown();\r\n```\r\n\r\n**4. CompletableFuture (비동기 프로그래밍)**\r\n```java\r\nCompletableFuture future = CompletableFuture\r\n .supplyAsync(() -> fetchData()) // 비동기 실행\r\n .thenApply(data -> processData(data)) // 결과 변환\r\n .thenApply(result -> format(result)) // 추가 변환\r\n .exceptionally(ex -> \"Error: \" + ex.getMessage()); // 예외 처리\r\n\r\n// 여러 비동기 작업 병렬 실행\r\nCompletableFuture.allOf(future1, future2, future3)\r\n .thenRun(() -> System.out.println(\"모두 완료\"));\r\n```\r\n\r\n### 스레드 상태\r\n\r\n```\r\nNEW → RUNNABLE ⇄ BLOCKED/WAITING/TIMED_WAITING → TERMINATED\r\n\r\nnew Thread() → start() → run()완료 → TERMINATED\r\n ↕\r\n BLOCKED (synchronized 대기)\r\n WAITING (wait(), join())\r\n TIMED_WAITING (sleep(), wait(timeout))\r\n```\r\n\r\n### 동기화\r\n\r\n```java\r\n// synchronized 메서드\r\npublic synchronized void increment() {\r\n count++;\r\n}\r\n\r\n// synchronized 블록\r\npublic void increment() {\r\n synchronized (lock) {\r\n count++;\r\n }\r\n}\r\n\r\n// ReentrantLock\r\nprivate final ReentrantLock lock = new ReentrantLock();\r\npublic void increment() {\r\n lock.lock();\r\n try {\r\n count++;\r\n } finally {\r\n lock.unlock();\r\n }\r\n}\r\n```\r\n\r\n### 스레드 풀 종류\r\n\r\n```java\r\nExecutors.newFixedThreadPool(n); // 고정 크기 풀\r\nExecutors.newCachedThreadPool(); // 필요에 따라 생성\r\nExecutors.newSingleThreadExecutor(); // 단일 스레드\r\nExecutors.newScheduledThreadPool(n); // 스케줄링 가능\r\n\r\n// 실무: 직접 ThreadPoolExecutor 설정 권장\r\nnew ThreadPoolExecutor(\r\n corePoolSize, maxPoolSize,\r\n keepAliveTime, TimeUnit.SECONDS,\r\n new LinkedBlockingQueue<>(queueCapacity)\r\n);\r\n```\r\n\r\n### 면접 팁\r\n실무에서는 `new Thread()`를 직접 사용하지 않고 스레드 풀(ExecutorService)을 사용합니다. 스레드 풀의 적절한 크기 설정(CPU bound vs I/O bound)과 CompletableFuture를 활용한 비동기 프로그래밍을 이해하세요.", + "difficulty": "INTERMEDIATE", + "tags": "스레드, Runnable, ExecutorService, 동기화, 스레드 풀", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 45, + "title": "고유 락(Intrinsic Lock)", + "content": "Java의 고유 락(Intrinsic Lock)과 모니터(Monitor)에 대해 설명해주세요.", + "answer": "Java의 모든 객체는 고유 락(Intrinsic Lock, 모니터 락)을 하나씩 가지고 있습니다. synchronized 키워드를 사용하면 이 고유 락을 획득하고 해제하는 방식으로 상호 배제를 구현합니다.\r\n\r\n### 고유 락(Monitor) 동작\r\n\r\n```java\r\n// 모든 Java 객체는 모니터를 가짐\r\nObject lock = new Object();\r\n\r\nsynchronized (lock) { // lock 객체의 고유 락 획득\r\n // 임계 구역\r\n} // 고유 락 자동 해제\r\n\r\n// 메서드 수준 - this 객체의 고유 락\r\npublic synchronized void method() {\r\n // this의 고유 락 획득\r\n}\r\n\r\n// 정적 메서드 - 클래스 객체의 고유 락\r\npublic static synchronized void staticMethod() {\r\n // MyClass.class의 고유 락 획득\r\n}\r\n```\r\n\r\n### 재진입성 (Reentrancy)\r\n\r\n고유 락은 재진입이 가능합니다. 같은 스레드가 이미 보유한 락을 다시 획득할 수 있습니다.\r\n\r\n```java\r\npublic synchronized void outer() {\r\n inner(); // 같은 스레드가 다시 락 획득 가능 (재진입)\r\n}\r\n\r\npublic synchronized void inner() {\r\n // 이미 outer()에서 this의 락을 보유하고 있으므로 진입 가능\r\n}\r\n```\r\n\r\n### wait / notify / notifyAll\r\n\r\n고유 락과 함께 사용되는 스레드 간 통신 메커니즘입니다.\r\n\r\n```java\r\nclass SharedBuffer {\r\n private Queue queue = new LinkedList<>();\r\n private int capacity;\r\n\r\n public synchronized void produce(int item) throws InterruptedException {\r\n while (queue.size() == capacity) {\r\n wait(); // 락을 해제하고 대기\r\n }\r\n queue.add(item);\r\n notifyAll(); // 대기 중인 스레드 깨움\r\n }\r\n\r\n public synchronized int consume() throws InterruptedException {\r\n while (queue.isEmpty()) {\r\n wait();\r\n }\r\n int item = queue.poll();\r\n notifyAll();\r\n return item;\r\n }\r\n}\r\n```\r\n\r\n### synchronized vs ReentrantLock\r\n\r\n```java\r\n// ReentrantLock: 더 세밀한 제어 가능\r\nReentrantLock lock = new ReentrantLock();\r\n\r\nlock.lock();\r\ntry {\r\n // 임계 구역\r\n} finally {\r\n lock.unlock(); // 반드시 finally에서 해제\r\n}\r\n\r\n// tryLock: 락 획득 시도 (타임아웃 설정 가능)\r\nif (lock.tryLock(1, TimeUnit.SECONDS)) {\r\n try { ... } finally { lock.unlock(); }\r\n} else {\r\n // 락 획득 실패 처리\r\n}\r\n```\r\n\r\n| 구분 | synchronized | ReentrantLock |\r\n|------|-------------|--------------|\r\n| 사용법 | 키워드 | API 호출 |\r\n| 해제 | 자동 | 수동 (finally) |\r\n| tryLock | 불가 | 가능 |\r\n| 공정성 | 불공정 | 공정/불공정 선택 |\r\n| Condition | wait/notify | Condition 객체 |\r\n| 인터럽트 | 불가 | lockInterruptibly() |\r\n\r\n### JVM 내부: 락 최적화\r\n\r\nJVM은 성능을 위해 락을 최적화합니다.\r\n- **바이어스드 락(Biased Lock)**: 단일 스레드가 반복 접근 시 락 오버헤드 제거\r\n- **경량 락(Lightweight Lock)**: CAS 연산으로 락 획득\r\n- **중량 락(Heavyweight Lock)**: OS 뮤텍스 사용 (경합이 심한 경우)\r\n\r\n### 면접 팁\r\nsynchronized의 동작 원리(모니터 락)와 ReentrantLock의 차이를 명확히 설명하세요. wait/notify의 사용법과 spurious wakeup을 방지하기 위해 while 루프에서 조건을 확인해야 하는 이유도 중요합니다.", + "difficulty": "ADVANCED", + "tags": "고유 락, 모니터, synchronized, ReentrantLock, 동기화", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 46, + "title": "문자열 클래스", + "content": "Java의 String 클래스의 특징과 String Pool에 대해 설명해주세요.", + "answer": "### String의 불변성\r\n\r\nString은 final 클래스이며, 내부 char 배열(Java 9+부터 byte 배열)도 final입니다. 한 번 생성되면 값을 변경할 수 없습니다.\r\n\r\n```java\r\nString str = \"Hello\";\r\nstr.concat(\" World\"); // 새로운 String 반환, str은 변경 안 됨\r\nSystem.out.println(str); // \"Hello\"\r\n\r\nstr = str.concat(\" World\"); // 새 객체를 str에 재할당\r\nSystem.out.println(str); // \"Hello World\"\r\n```\r\n\r\n### String Pool\r\n\r\nJVM이 문자열 리터럴을 관리하기 위해 힙 영역에 유지하는 특별한 공간입니다. 동일한 문자열 리터럴은 하나의 인스턴스만 생성됩니다.\r\n\r\n```java\r\nString s1 = \"Hello\"; // String Pool에 저장\r\nString s2 = \"Hello\"; // Pool에서 같은 인스턴스 재사용\r\nString s3 = new String(\"Hello\"); // Heap에 새 객체 생성\r\n\r\nSystem.out.println(s1 == s2); // true (같은 참조)\r\nSystem.out.println(s1 == s3); // false (다른 참조)\r\nSystem.out.println(s1.equals(s3)); // true (같은 값)\r\n\r\n// intern(): String Pool에 등록\r\nString s4 = s3.intern();\r\nSystem.out.println(s1 == s4); // true (Pool의 같은 인스턴스)\r\n```\r\n\r\n### 문자열 비교\r\n\r\n```java\r\n// == : 참조(주소) 비교\r\n// equals() : 값(내용) 비교\r\n\r\nString a = \"hello\";\r\nString b = \"hello\";\r\nString c = new String(\"hello\");\r\n\r\na == b // true (String Pool)\r\na == c // false (다른 객체)\r\na.equals(c) // true (같은 값)\r\n\"hello\".equals(a) // true (NullPointerException 방지 팁)\r\n```\r\n\r\n### 주요 String 메서드\r\n\r\n```java\r\nString str = \"Hello, World!\";\r\n\r\nstr.length(); // 13\r\nstr.charAt(0); // 'H'\r\nstr.substring(0, 5); // \"Hello\"\r\nstr.indexOf(\"World\"); // 7\r\nstr.contains(\"World\"); // true\r\nstr.replace(\"World\", \"Java\"); // \"Hello, Java!\"\r\nstr.toUpperCase(); // \"HELLO, WORLD!\"\r\nstr.trim(); // 앞뒤 공백 제거\r\nstr.split(\",\"); // [\"Hello\", \" World!\"]\r\nstr.isEmpty(); // false\r\nstr.isBlank(); // false (Java 11+)\r\n```\r\n\r\n### 문자열 생성 방법별 메모리\r\n\r\n```\r\n\"Hello\" → String Pool에서 관리, 재사용\r\nnew String(\"Hello\") → Heap에 새 객체 + Pool에도 \"Hello\" 존재\r\n```\r\n\r\n### 면접 팁\r\n`==`과 `equals()`의 차이, String Pool의 동작 원리, String이 불변인 이유(보안, 캐싱, 스레드 안전)를 확실히 이해하세요. `new String(\"abc\")`가 최대 몇 개의 객체를 생성하는지도 자주 출제됩니다.", + "difficulty": "BASIC", + "tags": "String, 문자열, String Pool, intern, 불변 객체", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 47, + "title": "Garbage Collection", + "content": "Java의 가비지 컬렉션(GC)의 동작 원리에 대해 설명해주세요.", + "answer": "가비지 컬렉션(Garbage Collection)은 JVM이 사용하지 않는 객체의 메모리를 자동으로 회수하는 메커니즘입니다.\r\n\r\n### GC 기본 동작: Mark and Sweep\r\n\r\n```\r\n1. Mark : GC Root에서 시작하여 참조를 따라가며 살아있는 객체를 표시\r\n2. Sweep : 표시되지 않은 객체(가비지)의 메모리를 회수\r\n3. Compact: (선택) 살아남은 객체를 한쪽으로 모아 단편화 해결\r\n```\r\n\r\n**GC Root:**\r\n- 스택의 지역 변수\r\n- 정적 변수\r\n- JNI 참조\r\n- 실행 중인 스레드\r\n\r\n### 세대별 GC (Generational GC)\r\n\r\n대부분의 객체는 짧은 시간만 살아있다는 가설(Weak Generational Hypothesis)에 기반합니다.\r\n\r\n```\r\nHeap 구조:\r\n┌──────────────────────────────┐\r\n│ Young Generation │\r\n│ ┌──────┬───────┬──────┐ │\r\n│ │ Eden │ S0 │ S1 │ │\r\n│ └──────┴───────┴──────┘ │\r\n├──────────────────────────────┤\r\n│ Old Generation │\r\n└──────────────────────────────┘\r\n```\r\n\r\n**Minor GC (Young Generation)**\r\n1. 새 객체는 Eden에 생성\r\n2. Eden이 가득 차면 Minor GC 발생\r\n3. 살아남은 객체는 Survivor(S0 또는 S1)로 이동\r\n4. Survivor에서 일정 횟수(age) 이상 살아남으면 Old로 이동(Promotion)\r\n\r\n**Major GC / Full GC (Old Generation)**\r\n- Old 영역이 가득 차면 발생\r\n- Minor GC보다 시간이 오래 걸림\r\n- Stop-the-World 발생\r\n\r\n### Stop-the-World\r\nGC를 수행하기 위해 모든 애플리케이션 스레드를 일시 정지하는 것입니다. GC 튜닝의 목표는 이 시간을 최소화하는 것입니다.\r\n\r\n### GC 종류\r\n\r\n**1. Serial GC**\r\n단일 스레드로 GC 수행. 소규모 애플리케이션용.\r\n\r\n**2. Parallel GC**\r\n여러 스레드로 GC 수행. Java 8 기본 GC.\r\n\r\n**3. G1 GC (Garbage First)**\r\n힙을 균등한 Region으로 나누어 관리. Java 9+ 기본 GC.\r\n```\r\n가비지가 가장 많은 Region을 우선 수집 → 이름의 유래\r\n```\r\n\r\n**4. ZGC**\r\n매우 낮은 지연 시간(10ms 이하). 대용량 힙(TB 규모)에 적합.\r\n\r\n### GC 튜닝\r\n\r\n```bash\r\n# JVM 옵션\r\n-Xms512m -Xmx2g # 힙 크기 설정\r\n-XX:+UseG1GC # G1 GC 사용\r\n-XX:MaxGCPauseMillis=200 # 목표 STW 시간\r\n-verbose:gc # GC 로그 출력\r\n-XX:+PrintGCDetails # 상세 GC 로그\r\n```\r\n\r\n### 면접 팁\r\nGC의 기본 원리(Mark-Sweep-Compact)와 세대별 GC의 동작 과정을 설명할 수 있어야 합니다. G1 GC의 특징과 Stop-the-World를 최소화하는 방법도 알아두세요.", + "difficulty": "INTERMEDIATE", + "tags": "가비지 컬렉션, GC, 힙, Young Generation, Old Generation", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 48, + "title": "Promotion & Casting", + "content": "Java에서 자동 타입 변환(Promotion)과 강제 타입 변환(Casting)에 대해 설명해주세요.", + "answer": "### 자동 타입 변환 (Promotion / Widening)\r\n작은 크기의 타입이 큰 크기의 타입에 저장될 때 자동으로 변환됩니다. 데이터 손실이 없습니다.\r\n\r\n```java\r\nbyte → short → int → long → float → double\r\n char ↗\r\n\r\nbyte b = 10;\r\nint i = b; // byte → int (자동)\r\nlong l = i; // int → long (자동)\r\nfloat f = l; // long → float (자동, 정밀도 손실 가능)\r\ndouble d = f; // float → double (자동)\r\n```\r\n\r\n### 강제 타입 변환 (Casting / Narrowing)\r\n큰 크기의 타입을 작은 크기의 타입으로 변환할 때 명시적으로 캐스팅해야 합니다. 데이터 손실이 발생할 수 있습니다.\r\n\r\n```java\r\nint i = 300;\r\nbyte b = (byte) i; // int → byte (강제), 300은 byte 범위 초과\r\nSystem.out.println(b); // 44 (데이터 손실!)\r\n\r\ndouble d = 3.14;\r\nint n = (int) d; // double → int (강제), 소수점 이하 버림\r\nSystem.out.println(n); // 3\r\n\r\nlong l = 100L;\r\nint m = (int) l; // long → int (강제), 범위 내면 안전\r\n```\r\n\r\n### 연산 시 자동 타입 변환\r\n\r\n```java\r\n// 피연산자 중 큰 타입으로 자동 변환 후 연산\r\nint a = 10;\r\ndouble b = 5.5;\r\ndouble result = a + b; // int가 double로 자동 변환: 15.5\r\n\r\n// 정수 나눗셈 주의\r\nint x = 7;\r\nint y = 2;\r\nint z = x / y; // 3 (정수 나눗셈)\r\ndouble w = x / y; // 3.0 (이미 3으로 계산된 후 double로 변환)\r\ndouble v = (double) x / y; // 3.5 (x를 먼저 double로 변환)\r\n\r\n// byte, short 연산 결과는 int\r\nbyte b1 = 10;\r\nbyte b2 = 20;\r\n// byte b3 = b1 + b2; // 컴파일 에러! 결과가 int\r\nint b3 = b1 + b2; // OK\r\nbyte b4 = (byte)(b1 + b2); // OK (강제 캐스팅)\r\n```\r\n\r\n### char와 int의 변환\r\n\r\n```java\r\nchar c = 'A';\r\nint num = c; // 65 (자동 변환)\r\nchar c2 = (char) 66; // 'B' (강제 변환)\r\n\r\n// char 연산\r\nchar ch = 'A';\r\n// char result = ch + 1; // 컴파일 에러 (결과가 int)\r\nchar result = (char)(ch + 1); // 'B'\r\n```\r\n\r\n### 면접 팁\r\n연산 시 자동 형변환 규칙과 정수 나눗셈의 주의점을 기억하세요. `byte + byte = int`인 이유는 JVM이 연산을 int 단위로 수행하기 때문입니다. long에서 float으로의 자동 변환 시 정밀도 손실이 발생할 수 있다는 점도 알아두세요.", + "difficulty": "BASIC", + "tags": "자동 형변환, 강제 형변환, 프로모션, 데이터 손실", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 49, + "title": "Primitive type & Reference type", + "content": "Java의 기본 타입(Primitive type)과 참조 타입(Reference type)의 차이점에 대해 설명해주세요.", + "answer": "### 기본 타입 (Primitive Type) - 8가지\r\n\r\n| 타입 | 크기 | 기본값 | 범위 |\r\n|------|------|--------|------|\r\n| byte | 1바이트 | 0 | -128 ~ 127 |\r\n| short | 2바이트 | 0 | -32,768 ~ 32,767 |\r\n| int | 4바이트 | 0 | 약 -21억 ~ 21억 |\r\n| long | 8바이트 | 0L | 매우 큼 |\r\n| float | 4바이트 | 0.0f | IEEE 754 |\r\n| double | 8바이트 | 0.0d | IEEE 754 |\r\n| char | 2바이트 | '\\u0000' | 0 ~ 65,535 |\r\n| boolean | 1비트* | false | true/false |\r\n\r\n### 참조 타입 (Reference Type)\r\n\r\n클래스, 인터페이스, 배열, 열거형 등 기본 타입을 제외한 모든 타입입니다.\r\n\r\n```java\r\nString str = \"Hello\"; // 참조 타입\r\nint[] arr = {1, 2, 3}; // 배열 (참조 타입)\r\nUser user = new User(); // 클래스 (참조 타입)\r\nList list = new ArrayList<>(); // 인터페이스 (참조 타입)\r\n```\r\n\r\n### 주요 차이점\r\n\r\n| 구분 | 기본 타입 | 참조 타입 |\r\n|------|-----------|-----------|\r\n| 저장 값 | 실제 값 | 메모리 주소(참조) |\r\n| 저장 위치 | Stack | 참조는 Stack, 객체는 Heap |\r\n| null | 불가 | 가능 |\r\n| 기본값 | 0, false 등 | null |\r\n| 제네릭 | 사용 불가 | 사용 가능 |\r\n| 성능 | 빠름 | 느림 (간접 참조) |\r\n\r\n### 메모리 구조\r\n\r\n```java\r\nint num = 10; // Stack에 값 10 직접 저장\r\nString str = \"Hello\"; // Stack에 참조값, Heap에 \"Hello\" 객체\r\n\r\nStack: Heap:\r\n┌──────────┐ ┌─────────────┐\r\n│ num = 10 │ │ \"Hello\" │\r\n│ str = ───┼───→│ │\r\n└──────────┘ └─────────────┘\r\n```\r\n\r\n### null 처리\r\n\r\n```java\r\nint num = 0; // 기본 타입은 null 불가\r\nInteger wrap = null; // 참조 타입은 null 가능\r\n\r\n// NullPointerException 주의\r\nString str = null;\r\nstr.length(); // NPE 발생!\r\n\r\n// null-safe 처리\r\nif (str != null) {\r\n str.length();\r\n}\r\n\r\n// Optional 사용 (권장)\r\nOptional.ofNullable(str)\r\n .map(String::length)\r\n .orElse(0);\r\n```\r\n\r\n### 비교 연산\r\n\r\n```java\r\n// 기본 타입: == 으로 값 비교\r\nint a = 10;\r\nint b = 10;\r\na == b; // true (값 비교)\r\n\r\n// 참조 타입: == 은 참조 비교, equals()로 값 비교\r\nString s1 = new String(\"hello\");\r\nString s2 = new String(\"hello\");\r\ns1 == s2; // false (참조 비교)\r\ns1.equals(s2); // true (값 비교)\r\n```\r\n\r\n### 면접 팁\r\n기본 타입은 스택에 직접 저장되어 접근이 빠르고, 참조 타입은 힙에 객체가 저장되어 간접 참조 오버헤드가 있습니다. 성능이 중요한 경우 기본 타입을 사용하고, 제네릭이나 null이 필요한 경우 래퍼 클래스를 사용합니다.", + "difficulty": "BASIC", + "tags": "기본 타입, 참조 타입, 메모리, null, 성능", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 50, + "title": "직렬화(Serialization)", + "content": "Java의 직렬화(Serialization)란 무엇이며, 어떻게 사용하나요?", + "answer": "직렬화(Serialization)는 객체의 상태를 바이트 스트림으로 변환하는 과정이며, 역직렬화(Deserialization)는 바이트 스트림을 다시 객체로 복원하는 과정입니다.\r\n\r\n### Java 직렬화\r\n\r\n```java\r\n// Serializable 인터페이스 구현\r\npublic class User implements Serializable {\r\n private static final long serialVersionUID = 1L;\r\n\r\n private String name;\r\n private int age;\r\n private transient String password; // 직렬화에서 제외\r\n\r\n // 생성자, getter, setter\r\n}\r\n\r\n// 직렬화 (객체 → 바이트)\r\ntry (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(\"user.dat\"))) {\r\n User user = new User(\"홍길동\", 25);\r\n oos.writeObject(user);\r\n}\r\n\r\n// 역직렬화 (바이트 → 객체)\r\ntry (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(\"user.dat\"))) {\r\n User user = (User) ois.readObject();\r\n System.out.println(user.getName()); // \"홍길동\"\r\n System.out.println(user.getPassword()); // null (transient)\r\n}\r\n```\r\n\r\n### serialVersionUID\r\n\r\n클래스의 버전을 관리하는 고유 식별자입니다. 직렬화/역직렬화 시 동일한 클래스인지 확인하는 데 사용됩니다.\r\n\r\n```java\r\nprivate static final long serialVersionUID = 1L;\r\n// 명시하지 않으면 컴파일러가 자동 생성 → 클래스 변경 시 다른 값 생성 → InvalidClassException\r\n```\r\n\r\n### transient 키워드\r\n\r\n직렬화에서 제외할 필드에 사용합니다.\r\n```java\r\nprivate transient String password; // 보안 정보\r\nprivate transient Connection conn; // 직렬화 불가능한 객체\r\n```\r\n\r\n### JSON 직렬화 (실무에서 주로 사용)\r\n\r\n```java\r\n// Jackson\r\nObjectMapper mapper = new ObjectMapper();\r\n\r\n// 직렬화: 객체 → JSON\r\nString json = mapper.writeValueAsString(user);\r\n// {\"name\":\"홍길동\",\"age\":25}\r\n\r\n// 역직렬화: JSON → 객체\r\nUser user = mapper.readValue(json, User.class);\r\n\r\n// Spring에서는 @RestController에서 자동 처리\r\n@GetMapping(\"/users/{id}\")\r\npublic User getUser(@PathVariable Long id) {\r\n return userService.findById(id); // 자동으로 JSON 직렬화\r\n}\r\n```\r\n\r\n### Java 직렬화 vs JSON 직렬화\r\n\r\n| 구분 | Java 직렬화 | JSON 직렬화 |\r\n|------|-----------|------------|\r\n| 형식 | 바이너리 | 텍스트 (JSON) |\r\n| 가독성 | 없음 | 높음 |\r\n| 호환성 | Java만 | 언어 무관 |\r\n| 크기 | 큼 | 작음 |\r\n| 보안 | 취약점 많음 | 상대적 안전 |\r\n| 실무 사용 | 줄어드는 추세 | 주류 |\r\n\r\n### 면접 팁\r\nJava 기본 직렬화는 보안 취약점이 많아 실무에서는 JSON(Jackson, Gson)이나 Protocol Buffers를 주로 사용합니다. serialVersionUID의 역할과 transient 키워드의 사용 사례를 설명할 수 있으면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "직렬화, 역직렬화, Serializable, serialVersionUID, JSON", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 51, + "title": "Error & Exception", + "content": "Java의 Error와 Exception의 차이, Checked Exception과 Unchecked Exception의 차이에 대해 설명해주세요.", + "answer": "### 예외 계층 구조\r\n\r\n```\r\nThrowable\r\n├── Error (시스템 레벨, 복구 불가)\r\n│ ├── OutOfMemoryError\r\n│ ├── StackOverflowError\r\n│ └── ...\r\n└── Exception\r\n ├── Checked Exception (컴파일 시 확인)\r\n │ ├── IOException\r\n │ ├── SQLException\r\n │ └── ...\r\n └── RuntimeException (Unchecked Exception)\r\n ├── NullPointerException\r\n ├── ArrayIndexOutOfBoundsException\r\n ├── IllegalArgumentException\r\n └── ...\r\n```\r\n\r\n### Error vs Exception\r\n\r\n| 구분 | Error | Exception |\r\n|------|-------|-----------|\r\n| 원인 | JVM 시스템 레벨 문제 | 애플리케이션 로직 문제 |\r\n| 복구 | 불가 | 가능 |\r\n| 처리 | 처리하면 안 됨 | 적절히 처리해야 함 |\r\n| 예시 | OOM, SOF | NPE, IOException |\r\n\r\n### Checked vs Unchecked Exception\r\n\r\n| 구분 | Checked | Unchecked |\r\n|------|---------|-----------|\r\n| 확인 시점 | 컴파일 타임 | 런타임 |\r\n| 처리 의무 | 반드시 처리 (try-catch or throws) | 선택적 |\r\n| 상위 클래스 | Exception | RuntimeException |\r\n| 예시 | IOException, SQLException | NPE, IAE |\r\n\r\n```java\r\n// Checked Exception: 반드시 처리해야 함\r\npublic void readFile() throws IOException { // throws 선언 필수\r\n FileReader reader = new FileReader(\"file.txt\");\r\n}\r\n\r\n// Unchecked Exception: 처리 선택\r\npublic void divide(int a, int b) {\r\n int result = a / b; // ArithmeticException 발생 가능하지만 선언 불필요\r\n}\r\n```\r\n\r\n### 예외 처리 방법\r\n\r\n```java\r\n// try-catch-finally\r\ntry {\r\n riskyOperation();\r\n} catch (SpecificException e) {\r\n // 구체적 예외 처리\r\n log.error(\"Error: {}\", e.getMessage());\r\n} catch (Exception e) {\r\n // 일반 예외 처리\r\n} finally {\r\n // 항상 실행 (자원 해제)\r\n}\r\n\r\n// try-with-resources (Java 7+)\r\ntry (BufferedReader br = new BufferedReader(new FileReader(\"file.txt\"))) {\r\n String line = br.readLine();\r\n} // AutoCloseable 구현 객체 자동 close()\r\n```\r\n\r\n### 커스텀 예외\r\n\r\n```java\r\n// Unchecked 커스텀 예외 (권장)\r\npublic class UserNotFoundException extends RuntimeException {\r\n public UserNotFoundException(Long userId) {\r\n super(\"User not found: \" + userId);\r\n }\r\n}\r\n\r\n// 사용\r\npublic User findById(Long id) {\r\n return userRepository.findById(id)\r\n .orElseThrow(() -> new UserNotFoundException(id));\r\n}\r\n```\r\n\r\n### 예외 처리 Best Practice\r\n\r\n1. **구체적인 예외를 잡기**: catch(Exception e)보다 구체적인 예외\r\n2. **예외 삼키지 않기**: 빈 catch 블록 금지\r\n3. **의미 있는 메시지 포함**: 디버깅에 도움되는 정보\r\n4. **Checked보다 Unchecked 선호**: 불필요한 throws 전파 방지\r\n5. **예외를 제어 흐름에 사용하지 않기**: 예외는 예외적 상황에만\r\n\r\n### 면접 팁\r\nChecked와 Unchecked의 차이를 확실히 이해하고, 실무에서 커스텀 예외를 RuntimeException으로 만드는 이유(Checked의 불편함)를 설명할 수 있어야 합니다. Spring의 @ExceptionHandler를 이용한 전역 예외 처리도 알아두세요.", + "difficulty": "INTERMEDIATE", + "tags": "Error, Exception, Checked, Unchecked, 예외 처리", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 52, + "title": "Stream API", + "content": "Java의 Stream API에 대해 설명하고, 주요 연산에 대해 설명해주세요.", + "answer": "Stream API는 Java 8에서 도입된 함수형 스타일의 데이터 처리 API입니다. 컬렉션 데이터를 선언적으로 처리할 수 있으며, 내부 반복을 사용합니다.\r\n\r\n### 기본 사용\r\n\r\n```java\r\nList names = Arrays.asList(\"김철수\", \"이영희\", \"박민수\", \"김영수\", \"이수진\");\r\n\r\nList result = names.stream() // 스트림 생성\r\n .filter(name -> name.startsWith(\"김\")) // 중간 연산: 필터링\r\n .map(String::toUpperCase) // 중간 연산: 변환\r\n .sorted() // 중간 연산: 정렬\r\n .collect(Collectors.toList()); // 최종 연산: 수집\r\n```\r\n\r\n### 주요 중간 연산\r\n\r\n```java\r\n// filter: 조건에 맞는 요소만 선택\r\nstream.filter(x -> x > 10)\r\n\r\n// map: 각 요소를 변환\r\nstream.map(user -> user.getName())\r\nstream.map(User::getName) // 메서드 참조\r\n\r\n// flatMap: 중첩 구조를 평탄화\r\nstream.flatMap(list -> list.stream())\r\n\r\n// sorted: 정렬\r\nstream.sorted()\r\nstream.sorted(Comparator.comparing(User::getAge).reversed())\r\n\r\n// distinct: 중복 제거\r\nstream.distinct()\r\n\r\n// peek: 중간에 처리 (디버깅용)\r\nstream.peek(System.out::println)\r\n\r\n// limit / skip\r\nstream.limit(5) // 처음 5개\r\nstream.skip(3) // 처음 3개 건너뜀\r\n```\r\n\r\n### 주요 최종 연산\r\n\r\n```java\r\n// collect: 결과를 컬렉션으로 수집\r\nList list = stream.collect(Collectors.toList());\r\nSet set = stream.collect(Collectors.toSet());\r\nMap map = stream.collect(Collectors.toMap(User::getId, Function.identity()));\r\nString joined = stream.collect(Collectors.joining(\", \"));\r\n\r\n// groupingBy: 그룹핑\r\nMap> byDept = users.stream()\r\n .collect(Collectors.groupingBy(User::getDepartment));\r\n\r\n// forEach: 각 요소에 대해 수행\r\nstream.forEach(System.out::println);\r\n\r\n// reduce: 누적 연산\r\nint sum = numbers.stream().reduce(0, Integer::sum);\r\nOptional max = numbers.stream().reduce(Integer::max);\r\n\r\n// count, min, max, anyMatch, allMatch\r\nlong count = stream.count();\r\nboolean hasAdult = users.stream().anyMatch(u -> u.getAge() >= 18);\r\n```\r\n\r\n### 지연 평가 (Lazy Evaluation)\r\n\r\n중간 연산은 최종 연산이 호출될 때까지 실행되지 않습니다.\r\n\r\n```java\r\nList result = names.stream()\r\n .filter(name -> {\r\n System.out.println(\"filter: \" + name); // 최종 연산 전에는 실행 안 됨\r\n return name.length() > 2;\r\n })\r\n .map(name -> {\r\n System.out.println(\"map: \" + name);\r\n return name.toUpperCase();\r\n })\r\n .collect(Collectors.toList()); // 이 시점에 위의 연산들이 실행됨\r\n```\r\n\r\n### 병렬 스트림\r\n\r\n```java\r\n// 병렬 처리로 대용량 데이터 처리 속도 향상\r\nlong count = bigList.parallelStream()\r\n .filter(item -> item.getValue() > 100)\r\n .count();\r\n\r\n// 주의: 순서 보장 안 됨, 스레드 안전한 연산만 사용\r\n```\r\n\r\n### 면접 팁\r\nStream의 특징(일회성, 지연 평가, 내부 반복)을 이해하고, 실무에서 자주 사용하는 패턴(filter-map-collect, groupingBy)을 코드로 작성할 수 있어야 합니다. 병렬 스트림의 주의사항(오버헤드, 스레드 안전성)도 알아두세요.", + "difficulty": "INTERMEDIATE", + "tags": "Stream, 람다, 함수형, 중간 연산, 최종 연산", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 53, + "title": "Record", + "content": "Java의 Record란 무엇이며, 어떤 상황에서 사용하나요?", + "answer": "Record는 Java 16에서 정식 도입된 불변 데이터 클래스입니다. 데이터를 담는 것이 주 목적인 클래스를 간결하게 선언할 수 있습니다.\r\n\r\n### 기본 사용\r\n\r\n```java\r\n// Record 정의\r\npublic record UserDto(Long id, String name, String email) {}\r\n\r\n// 위 한 줄이 아래 전체와 동일\r\npublic final class UserDto {\r\n private final Long id;\r\n private final String name;\r\n private final String email;\r\n\r\n public UserDto(Long id, String name, String email) {\r\n this.id = id;\r\n this.name = name;\r\n this.email = email;\r\n }\r\n\r\n public Long id() { return id; } // getter (get 접두사 없음)\r\n public String name() { return name; }\r\n public String email() { return email; }\r\n\r\n @Override public boolean equals(Object o) { ... }\r\n @Override public int hashCode() { ... }\r\n @Override public String toString() { ... }\r\n}\r\n```\r\n\r\n### Record가 자동 생성하는 것\r\n- **private final 필드**: 모든 컴포넌트\r\n- **생성자**: 모든 필드를 인자로 받는 표준 생성자\r\n- **접근자 메서드**: 필드명과 동일한 메서드 (id(), name())\r\n- **equals() / hashCode()**: 모든 필드 기반\r\n- **toString()**: 모든 필드 포함\r\n\r\n### Compact Constructor (검증 로직)\r\n\r\n```java\r\npublic record UserDto(Long id, String name, String email) {\r\n // Compact Constructor: 매개변수 목록 없이 선언\r\n public UserDto {\r\n if (name == null || name.isBlank()) {\r\n throw new IllegalArgumentException(\"이름은 필수입니다.\");\r\n }\r\n if (email != null) {\r\n email = email.toLowerCase(); // 값 변환 가능\r\n }\r\n }\r\n}\r\n```\r\n\r\n### Record의 제약사항\r\n- **불변**: 필드를 변경할 수 없음 (setter 없음)\r\n- **상속 불가**: final 클래스, 다른 클래스를 상속할 수 없음\r\n- **인터페이스 구현**: 가능\r\n- **정적 필드/메서드**: 가능\r\n- **인스턴스 필드 추가**: 불가\r\n\r\n```java\r\n// 인터페이스 구현 가능\r\npublic record Point(int x, int y) implements Comparable {\r\n @Override\r\n public int compareTo(Point other) {\r\n return Integer.compare(this.x, other.x);\r\n }\r\n\r\n // 정적 메서드 가능\r\n public static Point origin() {\r\n return new Point(0, 0);\r\n }\r\n\r\n // 커스텀 메서드 가능\r\n public double distanceTo(Point other) {\r\n return Math.sqrt(Math.pow(x - other.x, 2) + Math.pow(y - other.y, 2));\r\n }\r\n}\r\n```\r\n\r\n### 실무 활용\r\n\r\n```java\r\n// DTO로 활용\r\npublic record CreateUserRequest(\r\n @NotBlank String name,\r\n @Email String email,\r\n @Min(0) int age\r\n) {}\r\n\r\n// Spring Controller에서 사용\r\n@PostMapping(\"/users\")\r\npublic ResponseEntity create(@Valid @RequestBody CreateUserRequest request) {\r\n User user = userService.create(request);\r\n return ResponseEntity.ok(UserResponse.from(user));\r\n}\r\n\r\npublic record UserResponse(Long id, String name, String email) {\r\n public static UserResponse from(User user) {\r\n return new UserResponse(user.getId(), user.getName(), user.getEmail());\r\n }\r\n}\r\n```\r\n\r\n### 면접 팁\r\nRecord는 DTO, 값 객체(Value Object) 등 데이터를 운반하는 목적의 클래스에 적합합니다. JPA 엔티티에는 사용할 수 없습니다(기본 생성자, setter 필요). Lombok의 @Data와 비교하여 설명할 수 있으면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "Record, 불변 객체, DTO, Java 16, 데이터 클래스", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 54, + "title": "Interned String", + "content": "Java의 String.intern() 메서드와 Interned String에 대해 설명해주세요.", + "answer": "String.intern()은 문자열을 String Pool에 등록하고, Pool에 이미 같은 값이 있으면 그 참조를 반환하는 메서드입니다.\r\n\r\n### intern() 동작 원리\r\n\r\n```java\r\nString s1 = new String(\"hello\"); // Heap에 새 객체 생성\r\nString s2 = s1.intern(); // String Pool에 \"hello\" 등록 (이미 있으면 기존 참조 반환)\r\nString s3 = \"hello\"; // String Pool의 \"hello\" 참조\r\n\r\nSystem.out.println(s1 == s2); // false (s1은 Heap, s2는 Pool)\r\nSystem.out.println(s2 == s3); // true (둘 다 Pool의 같은 참조)\r\n```\r\n\r\n### 메모리 구조\r\n\r\n```\r\n리터럴 방식: \"hello\"\r\n→ String Pool에서 관리, 같은 리터럴은 같은 참조\r\n\r\nnew 방식: new String(\"hello\")\r\n→ Heap에 새 객체 생성 + Pool에도 \"hello\" 존재\r\n\r\nintern() 호출:\r\n→ Pool에 해당 문자열이 있으면 Pool 참조 반환\r\n→ 없으면 Pool에 추가 후 참조 반환\r\n```\r\n\r\n### 활용 사례\r\n\r\n```java\r\n// 동일한 문자열이 대량으로 생성되는 경우 메모리 절약\r\nMap> groupByCity = new HashMap<>();\r\n\r\nfor (Record record : records) {\r\n String city = record.getCity().intern(); // Pool에서 재사용\r\n groupByCity.computeIfAbsent(city, k -> new ArrayList<>()).add(record);\r\n}\r\n// 같은 도시명이 수만 번 반복되면 메모리 절약 효과 큼\r\n```\r\n\r\n### 주의사항\r\n\r\n**1. 성능 오버헤드**\r\n- intern()은 Pool 검색에 시간이 소요됩니다\r\n- 대량 호출 시 오히려 성능 저하 가능\r\n\r\n**2. 메모리 누수 가능성**\r\n- Java 7 이전: String Pool이 PermGen에 위치 → 크기 제한\r\n- Java 7 이후: String Pool이 Heap에 위치 → GC 대상\r\n\r\n**3. 동등성 비교에 intern() 사용은 권장하지 않음**\r\n```java\r\n// Bad: intern()으로 == 비교\r\nif (input.intern() == \"expected\") { ... }\r\n\r\n// Good: equals() 사용\r\nif (\"expected\".equals(input)) { ... }\r\n```\r\n\r\n### String Pool 내부\r\n\r\nJava 7+에서 String Pool은 HashMap 기반의 해시 테이블로 구현되어 있습니다.\r\n\r\n```bash\r\n# Pool 크기 조정 (JVM 옵션)\r\n-XX:StringTableSize=60013 # 기본값: 60013 (소수)\r\n```\r\n\r\n### 면접 팁\r\nintern()은 메모리 최적화를 위한 도구이지만, 무분별한 사용은 오히려 성능을 저하시킵니다. 문자열 비교에는 항상 equals()를 사용하는 것이 올바른 방법입니다. Java의 문자열 리터럴이 자동으로 intern된다는 점도 기억하세요.", + "difficulty": "INTERMEDIATE", + "tags": "String intern, String Pool, 메모리 최적화, 문자열 비교", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 55, + "title": "Composition(조합)", + "content": "상속(Inheritance)보다 조합(Composition)을 선호하는 이유에 대해 설명해주세요.", + "answer": "\"상속보다 조합을 선호하라(Favor composition over inheritance)\"는 GoF 디자인 패턴에서 제시한 핵심 원칙입니다. 코드 재사용을 위해 상속을 사용하는 것보다 조합(has-a 관계)을 통한 위임이 더 유연하고 안전합니다.\r\n\r\n### 상속의 문제점\r\n\r\n**1. 캡슐화 깨짐**\r\n```java\r\n// HashSet을 상속하여 추가된 원소 수를 세는 클래스\r\npublic class InstrumentedHashSet extends HashSet {\r\n private int addCount = 0;\r\n\r\n @Override\r\n public boolean add(E e) {\r\n addCount++;\r\n return super.add(e);\r\n }\r\n\r\n @Override\r\n public boolean addAll(Collection c) {\r\n addCount += c.size();\r\n return super.addAll(c); // 내부에서 add()를 호출!\r\n }\r\n}\r\n\r\nInstrumentedHashSet s = new InstrumentedHashSet<>();\r\ns.addAll(Arrays.asList(\"A\", \"B\", \"C\"));\r\n// 기대: addCount = 3\r\n// 실제: addCount = 6 (addAll이 내부에서 add를 호출하므로 중복 카운트)\r\n```\r\n\r\n**2. 상위 클래스 변경의 영향**\r\n상위 클래스에 새 메서드가 추가되면 하위 클래스에 영향을 줄 수 있습니다.\r\n\r\n**3. 단일 상속 제한**\r\nJava는 다중 상속을 지원하지 않으므로, 한 번 상속하면 다른 클래스를 상속할 수 없습니다.\r\n\r\n### 조합으로 해결\r\n\r\n```java\r\n// 조합 + 위임 방식\r\npublic class InstrumentedSet implements Set {\r\n private final Set set; // has-a 관계 (조합)\r\n private int addCount = 0;\r\n\r\n public InstrumentedSet(Set set) {\r\n this.set = set; // 어떤 Set 구현체든 주입 가능\r\n }\r\n\r\n @Override\r\n public boolean add(E e) {\r\n addCount++;\r\n return set.add(e); // 위임\r\n }\r\n\r\n @Override\r\n public boolean addAll(Collection c) {\r\n addCount += c.size();\r\n return set.addAll(c); // 위임 (내부 구현에 의존하지 않음)\r\n }\r\n\r\n // 나머지 Set 메서드들도 위임\r\n @Override public int size() { return set.size(); }\r\n @Override public boolean contains(Object o) { return set.contains(o); }\r\n // ...\r\n}\r\n\r\n// 사용: 어떤 Set 구현체든 감쌀 수 있음\r\nSet s = new InstrumentedSet<>(new HashSet<>());\r\nSet s2 = new InstrumentedSet<>(new TreeSet<>());\r\n```\r\n\r\n### 상속 vs 조합\r\n\r\n| 구분 | 상속 (is-a) | 조합 (has-a) |\r\n|------|-----------|-------------|\r\n| 관계 | \"~이다\" | \"~을 가진다\" |\r\n| 결합도 | 강한 결합 | 느슨한 결합 |\r\n| 유연성 | 컴파일 타임 결정 | 런타임 변경 가능 |\r\n| 다중 사용 | 단일 상속 제한 | 여러 객체 조합 가능 |\r\n| 캡슐화 | 깨질 수 있음 | 유지됨 |\r\n\r\n### 상속이 적합한 경우\r\n- 진정한 \"is-a\" 관계일 때 (Dog is an Animal)\r\n- 상위 클래스가 확장을 위해 설계되었을 때 (추상 클래스)\r\n- 상위 클래스와 하위 클래스를 같은 팀이 관리할 때\r\n\r\n### Spring에서의 조합\r\n\r\n```java\r\n@Service\r\npublic class OrderService {\r\n private final OrderRepository orderRepository; // 조합\r\n private final PaymentService paymentService; // 조합\r\n private final NotificationService notificationService; // 조합\r\n\r\n // 생성자 주입 = 조합 패턴\r\n public OrderService(OrderRepository orderRepository,\r\n PaymentService paymentService,\r\n NotificationService notificationService) {\r\n this.orderRepository = orderRepository;\r\n this.paymentService = paymentService;\r\n this.notificationService = notificationService;\r\n }\r\n}\r\n```\r\n\r\n### 면접 팁\r\nEffective Java의 \"상속보다 조합을 사용하라\" 항목의 핵심 논거를 이해하세요. HashSet 예시는 대표적인 상속의 문제를 보여줍니다. Spring의 DI 자체가 조합 패턴의 구현이라는 점도 연결하면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "조합, 상속, has-a, is-a, 위임, 느슨한 결합", + "categorySlug": "java", + "categoryName": "자바", + "categoryId": 8, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 56, + "title": "OSI 7계층", + "content": "OSI 7계층 모델에 대해 설명해주세요.", + "answer": "OSI(Open Systems Interconnection) 7계층 모델은 네트워크 통신을 7개의 계층으로 나누어 설명하는 국제 표준 참조 모델입니다. 각 계층은 독립적인 역할을 수행하며, 상위 계층에 서비스를 제공합니다.\r\n\r\n### 7계층 구조\r\n\r\n```\r\n7. 응용 계층 (Application) - HTTP, FTP, SMTP, DNS\r\n6. 표현 계층 (Presentation) - 암호화, 압축, 인코딩\r\n5. 세션 계층 (Session) - 세션 관리, 동기화\r\n4. 전송 계층 (Transport) - TCP, UDP (포트 번호)\r\n3. 네트워크 계층 (Network) - IP, ICMP, 라우팅\r\n2. 데이터링크 계층 (Data Link) - MAC 주소, 이더넷, 스위치\r\n1. 물리 계층 (Physical) - 전기 신호, 케이블, 허브\r\n```\r\n\r\n### 각 계층의 역할\r\n\r\n**7. 응용 계층**: 사용자와 직접 상호작용하는 프로토콜을 제공합니다 (HTTP, FTP, DNS 등).\r\n\r\n**6. 표현 계층**: 데이터의 형식 변환, 암호화, 압축을 담당합니다 (JPEG, MPEG, SSL/TLS).\r\n\r\n**5. 세션 계층**: 통신 세션을 설정, 관리, 종료합니다. 동기화 지점을 설정하여 전송 중 오류 복구를 지원합니다.\r\n\r\n**4. 전송 계층**: 종단 간(End-to-End) 신뢰성 있는 데이터 전송을 담당합니다. 포트 번호로 프로세스를 구분합니다. PDU: 세그먼트(TCP) / 데이터그램(UDP)\r\n\r\n**3. 네트워크 계층**: IP 주소를 기반으로 최적 경로를 선택(라우팅)하여 패킷을 전달합니다. PDU: 패킷. 장비: 라우터\r\n\r\n**2. 데이터링크 계층**: MAC 주소를 기반으로 인접 노드 간의 데이터 전송을 담당합니다. 오류 검출과 흐름 제어를 수행합니다. PDU: 프레임. 장비: 스위치\r\n\r\n**1. 물리 계층**: 비트를 전기 신호나 광 신호로 변환하여 전송합니다. PDU: 비트. 장비: 허브, 리피터\r\n\r\n### 캡슐화 / 역캡슐화\r\n\r\n```\r\n송신 (캡슐화):\r\n응용 데이터 → [헤더|데이터] → [헤더|세그먼트] → [헤더|패킷] → [헤더|프레임|트레일러]\r\n\r\n수신 (역캡슐화):\r\n각 계층에서 헤더를 제거하고 상위 계층에 전달\r\n```\r\n\r\n### TCP/IP 4계층과 비교\r\n\r\n| TCP/IP | OSI |\r\n|--------|-----|\r\n| 응용 계층 | 응용 + 표현 + 세션 |\r\n| 전송 계층 | 전송 |\r\n| 인터넷 계층 | 네트워크 |\r\n| 네트워크 접근 계층 | 데이터링크 + 물리 |\r\n\r\n### 면접 팁\r\n실제로는 TCP/IP 4계층이 더 실용적으로 사용됩니다. 각 계층의 대표 프로토콜과 장비, PDU를 연결하여 설명하세요. \"웹 브라우저에서 URL을 입력하면 어떤 과정을 거치나요?\"라는 질문에 OSI 계층별로 설명할 수 있으면 좋습니다.", + "difficulty": "BASIC", + "tags": "OSI, 7계층, TCP/IP, 프로토콜, 캡슐화", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 57, + "title": "TCP 3-way & 4-way Handshake", + "content": "TCP의 3-way handshake와 4-way handshake에 대해 설명해주세요.", + "answer": "TCP는 신뢰성 있는 연결을 보장하기 위해, 연결을 설정할 때 3-way handshake, 해제할 때 4-way handshake를 수행합니다.\r\n\r\n### 3-way Handshake (연결 설정)\r\n\r\n```\r\nClient Server\r\n | |\r\n | 1. SYN (seq=x) |\r\n | ──────────────────────────→ | SYN_SENT → SYN_RECEIVED\r\n | |\r\n | 2. SYN + ACK (seq=y, ack=x+1)|\r\n | ←────────────────────────── |\r\n | |\r\n | 3. ACK (ack=y+1) |\r\n | ──────────────────────────→ | ESTABLISHED\r\n | ESTABLISHED |\r\n```\r\n\r\n**과정 설명:**\r\n1. **SYN**: 클라이언트가 서버에 연결 요청 (시퀀스 번호 x)\r\n2. **SYN + ACK**: 서버가 요청을 수락하고 응답 (시퀀스 번호 y, 확인 번호 x+1)\r\n3. **ACK**: 클라이언트가 서버의 응답을 확인 (확인 번호 y+1)\r\n\r\n**왜 3-way인가?**\r\n양쪽 모두 데이터를 보내고 받을 수 있는 상태임을 확인하기 위해서입니다. 2-way만으로는 서버가 클라이언트의 수신 능력을 확인할 수 없습니다.\r\n\r\n### 4-way Handshake (연결 해제)\r\n\r\n```\r\nClient Server\r\n | |\r\n | 1. FIN (seq=u) |\r\n | ──────────────────────────→ | FIN_WAIT_1 → CLOSE_WAIT\r\n | |\r\n | 2. ACK (ack=u+1) |\r\n | ←────────────────────────── | FIN_WAIT_2\r\n | |\r\n | (서버가 남은 데이터 전송) |\r\n | |\r\n | 3. FIN (seq=w) |\r\n | ←────────────────────────── | LAST_ACK\r\n | |\r\n | 4. ACK (ack=w+1) |\r\n | ──────────────────────────→ | CLOSED\r\n | TIME_WAIT |\r\n | (2MSL 대기 후 CLOSED) |\r\n```\r\n\r\n**과정 설명:**\r\n1. **FIN**: 클라이언트가 연결 종료 요청\r\n2. **ACK**: 서버가 종료 요청 확인 (아직 보낼 데이터가 있을 수 있음)\r\n3. **FIN**: 서버가 남은 데이터를 모두 보낸 후 종료 요청\r\n4. **ACK**: 클라이언트가 종료 확인\r\n\r\n**왜 4-way인가?**\r\n서버가 FIN을 받아도 아직 전송 중인 데이터가 있을 수 있으므로, ACK와 FIN을 별도로 보냅니다.\r\n\r\n### TIME_WAIT 상태\r\n\r\n클라이언트는 마지막 ACK를 보낸 후 바로 CLOSED가 아닌 TIME_WAIT 상태에서 일정 시간(2MSL) 대기합니다.\r\n\r\n**이유:**\r\n1. 마지막 ACK가 손실되었을 때 서버의 FIN 재전송을 처리하기 위해\r\n2. 이전 연결의 지연된 패킷이 새로운 연결에 영향을 주지 않도록 보장\r\n\r\n### 면접 팁\r\n3-way와 4-way 각각의 단계를 정확히 설명하고, \"왜 3번인지/4번인지\"의 이유를 논리적으로 설명하세요. TIME_WAIT의 존재 이유도 자주 출제됩니다. SYN Flood 공격에 대해서도 알아두면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "TCP, 3-way handshake, 4-way handshake, SYN, ACK, FIN", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 58, + "title": "TCP 흐름제어 & 혼잡제어", + "content": "TCP의 흐름제어와 혼잡제어에 대해 설명해주세요.", + "answer": "### 흐름제어 (Flow Control)\r\n\r\n송신자가 수신자의 처리 능력을 초과하여 데이터를 보내지 않도록 조절하는 메커니즘입니다. 수신자가 감당할 수 있는 속도로 데이터를 전송합니다.\r\n\r\n**슬라이딩 윈도우 (Sliding Window)**\r\n수신자가 자신의 수신 버퍼 크기(Window Size)를 송신자에게 알려주면, 송신자는 윈도우 크기만큼만 데이터를 전송합니다.\r\n\r\n```\r\n송신자 윈도우:\r\n[1][2][3][4][5][6][7][8][9][10]...\r\n ├──────────┤\r\n 윈도우 크기 = 4\r\n ACK 2 수신 → 윈도우 이동\r\n[1][2][3][4][5][6][7][8][9][10]...\r\n ├──────────┤\r\n```\r\n\r\n- ACK를 받으면 윈도우가 오른쪽으로 이동\r\n- 수신자가 Window Size = 0을 보내면 전송 중단\r\n- 수신자의 버퍼가 비면 Window Update로 재개\r\n\r\n### 혼잡제어 (Congestion Control)\r\n\r\n네트워크의 혼잡 상태를 감지하고, 송신 속도를 조절하여 네트워크 붕괴를 방지하는 메커니즘입니다.\r\n\r\n**혼잡 윈도우 (cwnd - Congestion Window)**: 송신자가 관리하는 윈도우 크기\r\n**실제 전송량** = min(수신자 윈도우, 혼잡 윈도우)\r\n\r\n### 혼잡제어 알고리즘\r\n\r\n**1. Slow Start (느린 시작)**\r\n- 초기 cwnd = 1 MSS(Maximum Segment Size)\r\n- ACK를 받을 때마다 cwnd를 2배로 증가 (지수적 증가)\r\n- ssthresh(Slow Start Threshold)에 도달하면 혼잡 회피로 전환\r\n\r\n```\r\nRTT 1: cwnd = 1 → 1개 세그먼트 전송\r\nRTT 2: cwnd = 2 → 2개 세그먼트 전송\r\nRTT 3: cwnd = 4 → 4개 세그먼트 전송\r\nRTT 4: cwnd = 8 → ssthresh 도달 → 혼잡 회피\r\n```\r\n\r\n**2. 혼잡 회피 (Congestion Avoidance) - AIMD**\r\n- cwnd를 RTT마다 1 MSS씩 선형적으로 증가 (Additive Increase)\r\n- 패킷 손실 발생 시 cwnd를 절반으로 감소 (Multiplicative Decrease)\r\n\r\n```\r\ncwnd 변화:\r\n ^\r\ncwnd | /\\ /\\\r\n | / \\ / \\\r\n | / \\/ \\\r\n | / \\\r\n |/________________\\___→ time\r\n ssthresh\r\n```\r\n\r\n**3. 빠른 재전송 (Fast Retransmit)**\r\n- 3개의 중복 ACK를 받으면 타임아웃을 기다리지 않고 즉시 재전송\r\n\r\n**4. 빠른 회복 (Fast Recovery)**\r\n- 3개의 중복 ACK 시 cwnd를 절반으로 줄이고 혼잡 회피 단계로 진입\r\n- 타임아웃 발생 시에만 Slow Start로 돌아감\r\n\r\n### TCP Tahoe vs TCP Reno\r\n\r\n| 이벤트 | Tahoe | Reno |\r\n|--------|-------|------|\r\n| 3 중복 ACK | cwnd=1, Slow Start | cwnd 절반, Fast Recovery |\r\n| Timeout | cwnd=1, Slow Start | cwnd=1, Slow Start |\r\n\r\n### 면접 팁\r\n흐름제어와 혼잡제어를 혼동하지 마세요. 흐름제어는 \"수신자 보호\"(수신자가 처리할 수 있는 속도로), 혼잡제어는 \"네트워크 보호\"(네트워크가 감당할 수 있는 속도로)입니다.", + "difficulty": "ADVANCED", + "tags": "흐름제어, 혼잡제어, 슬라이딩 윈도우, AIMD, Slow Start", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 59, + "title": "UDP", + "content": "UDP란 무엇이며, TCP와의 차이점에 대해 설명해주세요.", + "answer": "UDP(User Datagram Protocol)는 전송 계층의 비연결형 프로토콜로, 별도의 연결 설정 없이 데이터를 전송합니다. 신뢰성보다 속도가 중요한 애플리케이션에서 사용합니다.\r\n\r\n### UDP 헤더 구조\r\n\r\n```\r\n 0 8 16 24 32 bits\r\n├──────┼──────┼──────┼──────┤\r\n│ 출발지 포트 │ 목적지 포트 │\r\n├──────────────┼──────────────┤\r\n│ 전체 길이 │ 체크섬 │\r\n├──────────────┴──────────────┤\r\n│ 데이터 │\r\n└─────────────────────────────┘\r\n(헤더 크기: 8바이트 - TCP의 20바이트보다 훨씬 작음)\r\n```\r\n\r\n### UDP 특징\r\n\r\n**1. 비연결형**: 3-way handshake 없이 바로 데이터 전송\r\n**2. 비신뢰성**: 순서 보장 없음, 손실 감지/재전송 없음\r\n**3. 낮은 오버헤드**: 헤더가 작고 처리가 간단\r\n**4. 빠른 전송**: 연결 설정/해제 과정 없음\r\n**5. 브로드캐스트/멀티캐스트 지원**\r\n\r\n### TCP vs UDP 비교\r\n\r\n| 특성 | TCP | UDP |\r\n|------|-----|-----|\r\n| 연결 방식 | 연결형 (Connection-oriented) | 비연결형 (Connectionless) |\r\n| 신뢰성 | 보장 (ACK, 재전송) | 보장하지 않음 |\r\n| 순서 보장 | 보장 | 보장하지 않음 |\r\n| 흐름/혼잡 제어 | 있음 | 없음 |\r\n| 전송 속도 | 상대적으로 느림 | 빠름 |\r\n| 헤더 크기 | 20~60바이트 | 8바이트 |\r\n| 통신 방식 | 1:1 | 1:1, 1:N, N:N |\r\n\r\n### UDP 활용 사례\r\n\r\n**1. 실시간 스트리밍**\r\n- 동영상, 음악 스트리밍\r\n- 약간의 패킷 손실은 허용, 지연이 더 중요\r\n\r\n**2. 온라인 게임**\r\n- 캐릭터 위치 등 빈번한 업데이트\r\n- 오래된 데이터를 재전송하는 것보다 새 데이터가 중요\r\n\r\n**3. DNS (Domain Name System)**\r\n- 작은 크기의 질의/응답\r\n- 빠른 응답이 중요\r\n\r\n**4. VoIP (인터넷 전화)**\r\n- 실시간 음성 전달\r\n- 지연보다 약간의 손실이 나음\r\n\r\n**5. IoT, 센서 데이터**\r\n- 대량의 작은 데이터를 빠르게 전송\r\n- 일부 손실 허용\r\n\r\n### UDP 위에서 신뢰성 구현\r\n\r\nUDP 자체는 신뢰성이 없지만, 애플리케이션 레벨에서 구현할 수 있습니다.\r\n- **QUIC**: Google이 개발한 UDP 기반 프로토콜 (HTTP/3에서 사용)\r\n- **RTP**: 실시간 미디어 전송 프로토콜\r\n- **게임 프로토콜**: 자체적인 ACK, 재전송 로직 구현\r\n\r\n### 면접 팁\r\n\"UDP는 신뢰성이 없어서 나쁘다\"가 아니라, 상황에 따라 TCP보다 적합할 수 있다는 점을 강조하세요. HTTP/3이 UDP 기반의 QUIC를 사용한다는 최신 트렌드도 알아두면 좋습니다.", + "difficulty": "BASIC", + "tags": "UDP, 비연결형, 데이터그램, 실시간, TCP 비교", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 60, + "title": "대칭키 & 공개키 암호화", + "content": "대칭키 암호화와 공개키(비대칭키) 암호화의 차이점을 설명해주세요.", + "answer": "### 대칭키 암호화 (Symmetric Key Encryption)\r\n\r\n암호화와 복호화에 동일한 키를 사용하는 방식입니다.\r\n\r\n```\r\n[평문] → 대칭키로 암호화 → [암호문] → 같은 대칭키로 복호화 → [평문]\r\n\r\n송신자: \"Hello\" + Key → \"x7f9a...\"\r\n수신자: \"x7f9a...\" + Key → \"Hello\"\r\n```\r\n\r\n**대표 알고리즘**: AES, DES, 3DES, SEED\r\n\r\n**장점:**\r\n- 암호화/복호화 속도가 빠름\r\n- 대용량 데이터 처리에 적합\r\n\r\n**단점:**\r\n- **키 배포 문제**: 상대방에게 키를 안전하게 전달하는 것이 어려움\r\n- 통신 상대가 많을수록 관리할 키가 급증 (n명이면 n(n-1)/2개의 키 필요)\r\n\r\n### 공개키(비대칭키) 암호화 (Asymmetric Key Encryption)\r\n\r\n암호화와 복호화에 서로 다른 키(공개키/개인키)를 사용합니다. 공개키로 암호화한 데이터는 개인키로만 복호화할 수 있습니다.\r\n\r\n```\r\n[평문] → 수신자의 공개키로 암호화 → [암호문] → 수신자의 개인키로 복호화 → [평문]\r\n\r\nA→B: \"Hello\" + B의 공개키 → \"k3m8...\" → B의 개인키 → \"Hello\"\r\n```\r\n\r\n**대표 알고리즘**: RSA, ECC, ElGamal\r\n\r\n**장점:**\r\n- 키 배포 문제 해결 (공개키는 누구에게나 공개 가능)\r\n- 디지털 서명에 활용 가능\r\n\r\n**단점:**\r\n- 암호화/복호화 속도가 느림 (대칭키 대비 약 1000배)\r\n- 대용량 데이터 처리에 부적합\r\n\r\n### 디지털 서명\r\n\r\n개인키로 서명하고, 공개키로 검증합니다.\r\n\r\n```\r\n서명: 원본 데이터의 해시 → 송신자의 개인키로 암호화 = 서명\r\n검증: 서명 → 송신자의 공개키로 복호화 = 해시 → 원본 해시와 비교\r\n```\r\n\r\n이를 통해 **인증**(누가 보냈는지)과 **무결성**(변조되지 않았는지)을 보장합니다.\r\n\r\n### 비교\r\n\r\n| 구분 | 대칭키 | 비대칭키 |\r\n|------|--------|---------|\r\n| 키 수 | 1개 (공유) | 2개 (공개키 + 개인키) |\r\n| 속도 | 빠름 | 느림 |\r\n| 키 배포 | 어려움 | 쉬움 |\r\n| 키 관리 | n(n-1)/2개 | 2n개 |\r\n| 용도 | 데이터 암호화 | 키 교환, 디지털 서명 |\r\n| 대표 알고리즘 | AES, DES | RSA, ECC |\r\n\r\n### 하이브리드 방식 (실무)\r\n\r\nHTTPS/TLS에서는 두 방식을 결합하여 사용합니다:\r\n1. 비대칭키로 대칭키를 안전하게 교환\r\n2. 교환된 대칭키로 실제 데이터를 암호화\r\n\r\n```\r\n1. 서버가 공개키 전송\r\n2. 클라이언트가 대칭키 생성\r\n3. 공개키로 대칭키를 암호화하여 서버에 전송\r\n4. 이후 대칭키로 데이터 암호화 통신\r\n```\r\n\r\n### 면접 팁\r\n실무에서는 두 방식을 결합한 하이브리드 방식을 사용한다는 점이 핵심입니다. HTTPS에서 어떻게 적용되는지 구체적으로 설명할 수 있으면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "대칭키, 공개키, 비대칭키, RSA, AES, 암호화", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 61, + "title": "HTTP & HTTPS", + "content": "HTTP와 HTTPS의 차이점에 대해 설명해주세요.", + "answer": "HTTP(HyperText Transfer Protocol)는 웹에서 클라이언트와 서버 간에 데이터를 주고받기 위한 프로토콜이며, HTTPS는 HTTP에 보안(SSL/TLS)을 추가한 프로토콜입니다.\r\n\r\n### HTTP 특징\r\n\r\n**1. Stateless (무상태)**\r\n각 요청은 독립적이며 이전 요청의 정보를 기억하지 않습니다. 상태 유지가 필요하면 쿠키, 세션, 토큰을 사용합니다.\r\n\r\n**2. 비연결형 (Connectionless)**\r\n요청-응답 후 연결을 끊습니다. (HTTP/1.1부터는 Keep-Alive로 연결 유지 가능)\r\n\r\n**3. 요청 메서드**\r\n```\r\nGET - 리소스 조회\r\nPOST - 리소스 생성\r\nPUT - 리소스 전체 수정\r\nPATCH - 리소스 부분 수정\r\nDELETE - 리소스 삭제\r\n```\r\n\r\n**4. 주요 상태 코드**\r\n```\r\n1xx - 정보 (처리 중)\r\n2xx - 성공\r\n 200 OK, 201 Created, 204 No Content\r\n3xx - 리다이렉션\r\n 301 Moved Permanently, 302 Found, 304 Not Modified\r\n4xx - 클라이언트 오류\r\n 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found\r\n5xx - 서버 오류\r\n 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable\r\n```\r\n\r\n### HTTP vs HTTPS\r\n\r\n| 구분 | HTTP | HTTPS |\r\n|------|------|-------|\r\n| 포트 | 80 | 443 |\r\n| 보안 | 없음 (평문 전송) | SSL/TLS 암호화 |\r\n| 속도 | 약간 빠름 | 약간 느림 (암호화 오버헤드) |\r\n| 인증서 | 불필요 | SSL 인증서 필요 |\r\n| URL | http:// | https:// |\r\n\r\n### HTTPS 동작 원리\r\n\r\n```\r\n1. 클라이언트가 서버에 HTTPS 요청\r\n2. 서버가 SSL 인증서(공개키 포함)를 전송\r\n3. 클라이언트가 인증서의 유효성을 CA를 통해 검증\r\n4. 클라이언트가 대칭키를 생성하여 서버의 공개키로 암호화하여 전송\r\n5. 서버가 개인키로 복호화하여 대칭키를 획득\r\n6. 대칭키를 사용하여 암호화된 통신 시작\r\n```\r\n\r\n### HTTPS가 필요한 이유\r\n- **기밀성**: 제3자가 데이터를 도청할 수 없음\r\n- **무결성**: 데이터가 전송 중 변조되지 않음을 보장\r\n- **인증**: 서버의 신원을 인증서로 확인\r\n\r\n### HTTP 버전별 발전\r\n\r\n```\r\nHTTP/1.0 - 요청마다 연결 생성/종료\r\nHTTP/1.1 - Keep-Alive, 파이프라이닝\r\nHTTP/2 - 멀티플렉싱, 헤더 압축, 서버 푸시\r\nHTTP/3 - QUIC(UDP 기반), 더 빠른 연결 설정\r\n```\r\n\r\n### 면접 팁\r\nHTTPS의 동작 원리(SSL/TLS handshake)를 순서대로 설명할 수 있어야 합니다. HTTP/2와 HTTP/3의 개선점도 알아두면 좋습니다. REST API와 연관하여 HTTP 메서드와 상태 코드를 설명할 수 있으면 좋습니다.", + "difficulty": "BASIC", + "tags": "HTTP, HTTPS, SSL, TLS, 상태코드, REST", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 62, + "title": "TLS/SSL Handshake", + "content": "TLS/SSL Handshake의 동작 과정에 대해 설명해주세요.", + "answer": "TLS(Transport Layer Security)는 SSL(Secure Sockets Layer)의 후속 프로토콜로, 네트워크 통신의 기밀성과 무결성을 보장합니다. TLS Handshake는 암호화된 통신을 시작하기 위한 초기 협상 과정입니다.\r\n\r\n### TLS 1.2 Handshake 과정\r\n\r\n```\r\nClient Server\r\n | |\r\n | 1. ClientHello |\r\n | (TLS 버전, 암호화 스위트 목록, |\r\n | 클라이언트 랜덤값) |\r\n | ──────────────────────────────────→ |\r\n | |\r\n | 2. ServerHello |\r\n | (선택된 암호화 스위트, 서버 랜덤값) |\r\n | 3. Certificate (SSL 인증서) |\r\n | 4. ServerHelloDone |\r\n | ←────────────────────────────────── |\r\n | |\r\n | 5. ClientKeyExchange |\r\n | (Pre-Master Secret을 공개키로 암호화)|\r\n | 6. ChangeCipherSpec |\r\n | 7. Finished |\r\n | ──────────────────────────────────→ |\r\n | |\r\n | 8. ChangeCipherSpec |\r\n | 9. Finished |\r\n | ←────────────────────────────────── |\r\n | |\r\n | 암호화된 데이터 통신 시작 |\r\n```\r\n\r\n### 상세 과정\r\n\r\n**1. ClientHello**\r\n클라이언트가 지원하는 TLS 버전, 암호화 스위트 목록, 클라이언트 랜덤값을 서버에 전송합니다.\r\n\r\n**2-4. ServerHello ~ ServerHelloDone**\r\n서버가 사용할 암호화 스위트를 선택하고, SSL 인증서(공개키 포함)를 전송합니다.\r\n\r\n**5. 인증서 검증 및 키 교환**\r\n- 클라이언트가 인증서의 유효성을 CA(인증 기관)를 통해 검증\r\n- Pre-Master Secret을 생성하고 서버의 공개키로 암호화하여 전송\r\n- 양쪽 모두 클라이언트 랜덤 + 서버 랜덤 + Pre-Master Secret으로 세션 키(대칭키) 생성\r\n\r\n**6-9. 암호화 전환**\r\n양쪽 모두 세션 키를 사용한 암호화 통신으로 전환합니다.\r\n\r\n### SSL 인증서 구조\r\n\r\n```\r\n인증서 내용:\r\n- 서버 도메인 이름\r\n- 서버 공개키\r\n- 인증기관(CA) 정보\r\n- 유효 기간\r\n- 디지털 서명 (CA의 개인키로 서명)\r\n```\r\n\r\n### 인증서 검증 체인 (Certificate Chain)\r\n\r\n```\r\n루트 CA (자체 서명) → 중간 CA → 서버 인증서\r\n브라우저에 루트 CA 인증서가 내장되어 있어 신뢰 가능\r\n```\r\n\r\n### TLS 1.3 개선사항\r\n\r\nTLS 1.3은 핸드셰이크를 1-RTT로 줄여 속도를 개선했습니다.\r\n\r\n```\r\nTLS 1.2: 2-RTT (왕복 2번)\r\nTLS 1.3: 1-RTT (왕복 1번)\r\nTLS 1.3 + 0-RTT: 이전 연결 정보 재사용\r\n```\r\n\r\n- 안전하지 않은 알고리즘 제거 (RC4, DES, MD5 등)\r\n- 키 교환과 암호화 스위트 간소화\r\n- Forward Secrecy 필수 (Diffie-Hellman 기반)\r\n\r\n### 암호화 스위트 예시\r\n\r\n```\r\nTLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\r\n│ │ │ │ │ │\r\n│ │ │ │ │ └─ 해시 함수\r\n│ │ │ │ └─ 운영 모드\r\n│ │ │ └─ 대칭키 알고리즘/키 크기\r\n│ │ └─ 인증 알고리즘\r\n│ └─ 키 교환 알고리즘\r\n└─ 프로토콜\r\n```\r\n\r\n### 면접 팁\r\nTLS Handshake에서 비대칭키가 대칭키를 교환하는 데 사용되고, 이후 대칭키로 실제 통신이 이루어진다는 하이브리드 방식을 이해하세요. Forward Secrecy의 개념과 TLS 1.3의 개선점도 알아두면 좋습니다.", + "difficulty": "ADVANCED", + "tags": "TLS, SSL, handshake, 인증서, 암호화 스위트", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 63, + "title": "로드 밸런싱(Load Balancing)", + "content": "로드 밸런싱이란 무엇이며, 어떤 알고리즘들이 있나요?", + "answer": "로드 밸런싱(Load Balancing)은 들어오는 네트워크 트래픽을 여러 서버에 분산시켜 시스템의 가용성과 성능을 높이는 기술입니다.\r\n\r\n### 로드 밸런서 종류\r\n\r\n**L4 로드 밸런서 (전송 계층)**\r\n- IP 주소와 포트 번호를 기반으로 트래픽 분산\r\n- 패킷의 내용을 확인하지 않아 속도가 빠름\r\n- 비교적 간단한 분산만 가능\r\n\r\n**L7 로드 밸런서 (응용 계층)**\r\n- HTTP 헤더, URL, 쿠키 등을 분석하여 트래픽 분산\r\n- 콘텐츠 기반의 지능적인 분산 가능\r\n- SSL 종료(SSL Termination) 처리 가능\r\n\r\n```\r\n예: L7 로드 밸런서\r\n/api/* → API 서버 그룹\r\n/images/* → 이미지 서버 그룹\r\n/static/* → CDN\r\n```\r\n\r\n### 로드 밸런싱 알고리즘\r\n\r\n**1. 라운드 로빈 (Round Robin)**\r\n요청을 순서대로 각 서버에 분배합니다.\r\n```\r\n요청 1 → 서버 A\r\n요청 2 → 서버 B\r\n요청 3 → 서버 C\r\n요청 4 → 서버 A (다시 처음부터)\r\n```\r\n\r\n**2. 가중치 라운드 로빈 (Weighted Round Robin)**\r\n서버의 처리 능력에 따라 가중치를 부여합니다.\r\n```\r\n서버 A (가중치 5): 5개 요청 처리\r\n서버 B (가중치 3): 3개 요청 처리\r\n서버 C (가중치 2): 2개 요청 처리\r\n```\r\n\r\n**3. 최소 연결 (Least Connection)**\r\n현재 연결 수가 가장 적은 서버에 요청을 전달합니다. 세션이 긴 요청에 적합합니다.\r\n\r\n**4. IP 해시 (IP Hash)**\r\n클라이언트 IP를 해시하여 항상 같은 서버에 연결합니다. 세션 유지에 유리합니다.\r\n\r\n**5. 최소 응답 시간 (Least Response Time)**\r\n응답 시간이 가장 짧고 연결 수가 적은 서버를 선택합니다.\r\n\r\n### 헬스 체크 (Health Check)\r\n\r\n로드 밸런서는 주기적으로 서버의 상태를 확인합니다.\r\n```\r\n- TCP 연결 확인\r\n- HTTP 요청 전송 후 상태 코드 확인 (200 OK)\r\n- 특정 URL 응답 확인 (/health 엔드포인트)\r\n\r\n장애 서버 감지 → 트래픽 분배에서 제외\r\n복구 감지 → 트래픽 분배에 다시 포함\r\n```\r\n\r\n### 세션 유지 (Session Persistence)\r\n\r\n로드 밸런싱 환경에서 같은 사용자의 요청이 다른 서버로 분산되면 세션 문제가 발생합니다.\r\n\r\n**해결 방법:**\r\n1. **Sticky Session**: 특정 사용자를 항상 같은 서버에 연결\r\n2. **세션 클러스터링**: 서버 간 세션 공유 (Redis 등)\r\n3. **JWT 토큰**: Stateless 인증으로 세션 문제 회피\r\n\r\n### 실무 구성 예시\r\n```\r\n ┌─── 서버 A\r\n사용자 → CDN → L7 LB ├─── 서버 B\r\n └─── 서버 C\r\n\r\n대표 제품: AWS ALB/NLB, Nginx, HAProxy\r\n```\r\n\r\n### 면접 팁\r\nL4와 L7의 차이, 알고리즘별 적합한 상황, 세션 유지 문제와 해결 방법을 함께 설명하세요. 실무에서 Nginx나 AWS ELB를 사용한 경험이 있다면 함께 언급하면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "로드 밸런싱, L4, L7, 라운드 로빈, 헬스 체크", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 64, + "title": "Blocking/Non-blocking & Sync/Async", + "content": "Blocking과 Non-blocking, Synchronous와 Asynchronous의 차이를 설명해주세요.", + "answer": "이 네 가지 개념은 서로 다른 관점에서 I/O 동작을 설명합니다. Blocking/Non-blocking은 **제어권**에 관한 것이고, Sync/Async는 **결과 처리 방식**에 관한 것입니다.\r\n\r\n### Blocking vs Non-blocking\r\n\r\n**관점: 호출된 함수가 제어권을 언제 돌려주는가?**\r\n\r\n**Blocking**: 호출된 함수가 작업이 완료될 때까지 제어권을 돌려주지 않습니다. 호출자는 대기합니다.\r\n\r\n**Non-blocking**: 호출된 함수가 즉시 제어권을 돌려줍니다. 호출자는 다른 작업을 할 수 있습니다.\r\n\r\n```java\r\n// Blocking\r\nInputStream is = socket.getInputStream();\r\nint data = is.read(); // 데이터가 올 때까지 블로킹\r\n\r\n// Non-blocking\r\nchannel.configureBlocking(false);\r\nint bytesRead = channel.read(buffer); // 즉시 반환 (데이터 없으면 0 또는 -1)\r\n```\r\n\r\n### Synchronous vs Asynchronous\r\n\r\n**관점: 작업 완료를 누가 확인하는가?**\r\n\r\n**Synchronous**: 호출자가 작업 완료를 직접 확인합니다. 결과를 기다리거나 주기적으로 확인합니다.\r\n\r\n**Asynchronous**: 호출된 측이 작업 완료 시 콜백으로 알려줍니다. 호출자는 결과를 신경 쓰지 않습니다.\r\n\r\n```java\r\n// Synchronous\r\nFuture future = executor.submit(task);\r\nResult result = future.get(); // 결과를 직접 확인\r\n\r\n// Asynchronous\r\nCompletableFuture.supplyAsync(() -> doWork())\r\n .thenAccept(result -> handleResult(result)); // 콜백으로 결과 처리\r\n```\r\n\r\n### 4가지 조합\r\n\r\n**1. Sync + Blocking (가장 일반적)**\r\n함수 호출 후 결과가 올 때까지 대기합니다.\r\n```\r\nA: 작업 요청 → 대기... → 결과 받음\r\n```\r\n예: 일반적인 파일 읽기, JDBC 쿼리\r\n\r\n**2. Sync + Non-blocking**\r\n함수는 즉시 반환하지만, 호출자가 주기적으로 완료 여부를 확인합니다.\r\n```\r\nA: 작업 요청 → 다른 일 → \"완료?\" → \"아직\" → 다른 일 → \"완료?\" → \"완료!\"\r\n```\r\n예: 폴링(Polling) 방식\r\n\r\n**3. Async + Non-blocking (이상적)**\r\n함수는 즉시 반환하고, 완료 시 콜백으로 알려줍니다.\r\n```\r\nA: 작업 요청 → 다른 일 → ... → 콜백 호출됨!\r\n```\r\n예: Node.js I/O, Java NIO + CompletableFuture\r\n\r\n**4. Async + Blocking (비효율적)**\r\n비동기 호출을 했지만 결국 블로킹되는 경우입니다.\r\n```\r\nA: 비동기 작업 요청 → 결국 블로킹 대기...\r\n```\r\n예: Node.js에서 MySQL 드라이버가 내부적으로 Blocking I/O 사용\r\n\r\n### 비유로 이해하기\r\n\r\n```\r\n카페에서 커피를 주문하는 상황:\r\n\r\nSync + Blocking: 카운터 앞에서 커피 나올 때까지 줄 서서 기다림\r\nSync + Non-blocking: 진동벨 없이 주기적으로 \"커피 됐나요?\" 확인\r\nAsync + Non-blocking: 진동벨 받고 자리에서 다른 일 하다가 벨 울리면 가져감\r\nAsync + Blocking: 진동벨 받았지만 카운터 앞에서 계속 기다림\r\n```\r\n\r\n### 면접 팁\r\n두 개념의 구분 기준을 명확히 하세요: Blocking/Non-blocking은 \"제어권 반환 시점\", Sync/Async는 \"결과 확인 주체\"입니다. 실무에서는 Async + Non-blocking 조합이 가장 효율적이며, 이것이 Node.js와 Spring WebFlux의 핵심 원리입니다.", + "difficulty": "INTERMEDIATE", + "tags": "블로킹, 논블로킹, 동기, 비동기, I/O 모델", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 65, + "title": "Blocking & Non-Blocking I/O", + "content": "운영체제의 I/O 모델(Blocking I/O, Non-blocking I/O, I/O Multiplexing 등)에 대해 설명해주세요.", + "answer": "운영체제에서 I/O 작업은 크게 두 단계로 나뉩니다:\r\n1. **데이터 준비**: 커널이 데이터를 준비하는 단계\r\n2. **데이터 복사**: 커널 버퍼에서 사용자 버퍼로 복사하는 단계\r\n\r\n각 I/O 모델은 이 두 단계에서의 동작이 다릅니다.\r\n\r\n### I/O 모델 종류\r\n\r\n**1. Blocking I/O**\r\n```\r\nApplication Kernel\r\n | read() 호출 |\r\n |───────────────→|\r\n | (블로킹...) | 데이터 준비 중...\r\n | | 데이터 준비 완료\r\n | | 커널→유저 복사\r\n |←───────────────|\r\n | 데이터 반환 |\r\n```\r\n- 가장 단순한 모델\r\n- 스레드가 I/O 완료까지 차단됨\r\n- 동시 처리를 위해 스레드를 늘려야 함 → 자원 낭비\r\n\r\n**2. Non-blocking I/O**\r\n```\r\nApplication Kernel\r\n | read() 호출 |\r\n |───────────────→|\r\n |←── EAGAIN ─────| 데이터 없음\r\n | read() 재호출 |\r\n |───────────────→|\r\n |←── EAGAIN ─────| 아직 없음\r\n | read() 재호출 |\r\n |───────────────→|\r\n | | 데이터 준비 완료\r\n | | 커널→유저 복사 (블로킹)\r\n |←───────────────|\r\n```\r\n- 호출 즉시 반환, 데이터가 없으면 에러 반환\r\n- 반복적인 시스템 콜이 필요 (busy-waiting) → CPU 낭비\r\n\r\n**3. I/O Multiplexing (select, poll, epoll)**\r\n```\r\nApplication Kernel\r\n | select() 호출 |\r\n |───────────────→|\r\n | (블로킹...) | 여러 fd 모니터링\r\n |←───────────────| fd 준비됨\r\n | read() 호출 |\r\n |───────────────→|\r\n |←───────────────| 데이터 반환\r\n```\r\n- 하나의 스레드로 여러 I/O를 동시에 감시\r\n- 준비된 I/O만 처리하여 효율적\r\n\r\n```c\r\n// select 예시 (최대 FD_SETSIZE개 감시)\r\nfd_set readfds;\r\nFD_ZERO(&readfds);\r\nFD_SET(sockfd, &readfds);\r\nselect(sockfd + 1, &readfds, NULL, NULL, &timeout);\r\n\r\n// epoll 예시 (Linux, 대규모에 효율적)\r\nint epfd = epoll_create1(0);\r\nstruct epoll_event ev;\r\nev.events = EPOLLIN;\r\nev.data.fd = sockfd;\r\nepoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);\r\nint nfds = epoll_wait(epfd, events, MAX_EVENTS, timeout);\r\n```\r\n\r\n**select vs epoll:**\r\n\r\n| 특성 | select | epoll |\r\n|------|--------|-------|\r\n| fd 제한 | 1024개 | 없음 |\r\n| 동작 | 전체 fd 스캔 O(n) | 이벤트 기반 O(1) |\r\n| 커널-유저 복사 | 매번 전체 | 필요한 것만 |\r\n| 적합한 상황 | 소규모 연결 | 대규모 연결 (C10K) |\r\n\r\n**4. Asynchronous I/O (AIO)**\r\n```\r\nApplication Kernel\r\n | aio_read() |\r\n |───────────────→|\r\n |←── 즉시 반환 ───|\r\n | (다른 작업 수행) | 데이터 준비 + 복사\r\n | |\r\n |←── 시그널/콜백 ──| 완료 통지\r\n```\r\n- 두 단계 모두 비동기로 처리\r\n- 가장 효율적이지만 구현이 복잡\r\n\r\n### 실무 적용\r\n\r\n```\r\nNginx: epoll 기반 이벤트 드리븐 → 수만 동시 연결 처리\r\nNode.js: libuv의 이벤트 루프 → Non-blocking I/O\r\nNetty: Java NIO 기반 → 높은 동시성\r\nSpring WebFlux: Reactor 패턴 → 리액티브 프로그래밍\r\n```\r\n\r\n### 면접 팁\r\nC10K 문제(동시 1만 연결 처리)를 해결하기 위해 I/O Multiplexing과 이벤트 드리븐 아키텍처가 등장했다는 맥락을 설명하세요. 실무에서 사용하는 Nginx, Node.js가 어떤 I/O 모델을 사용하는지 연결하면 좋습니다.", + "difficulty": "ADVANCED", + "tags": "I/O 모델, select, epoll, 이벤트 드리븐, 리액터 패턴", + "categorySlug": "network", + "categoryName": "네트워크", + "categoryId": 4, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 66, + "title": "운영체제란", + "content": "운영체제(OS)란 무엇이며, 주요 역할에 대해 설명해주세요.", + "answer": "운영체제(Operating System)는 컴퓨터 하드웨어와 사용자(응용 프로그램) 사이에서 중재자 역할을 하는 시스템 소프트웨어입니다. 컴퓨터 시스템의 자원을 효율적으로 관리하고, 사용자에게 편리한 인터페이스를 제공합니다.\r\n\r\n### 운영체제의 주요 역할\r\n\r\n**1. 프로세스 관리**\r\n- 프로세스의 생성, 실행, 종료를 관리합니다\r\n- CPU 스케줄링을 통해 여러 프로세스에 CPU 시간을 할당합니다\r\n- 프로세스 간 동기화와 통신(IPC)을 지원합니다\r\n\r\n**2. 메모리 관리**\r\n- 프로세스에 메모리 공간을 할당하고 회수합니다\r\n- 가상 메모리를 통해 물리 메모리보다 큰 주소 공간을 제공합니다\r\n- 페이징, 세그먼테이션 기법으로 메모리를 관리합니다\r\n\r\n**3. 파일 시스템 관리**\r\n- 파일과 디렉토리의 생성, 삭제, 접근을 관리합니다\r\n- 디스크 공간을 할당하고 관리합니다\r\n- 파일 접근 권한을 제어합니다\r\n\r\n**4. 입출력(I/O) 관리**\r\n- 다양한 입출력 장치를 제어하고 관리합니다\r\n- 디바이스 드라이버를 통해 하드웨어 추상화를 제공합니다\r\n- 버퍼링, 캐싱, 스풀링으로 I/O 효율을 높입니다\r\n\r\n**5. 보안 및 보호**\r\n- 사용자 인증과 접근 제어를 수행합니다\r\n- 프로세스 간 메모리 보호를 제공합니다\r\n\r\n### 커널 (Kernel)\r\n운영체제의 핵심 부분으로, 항상 메모리에 상주하며 하드웨어와 직접 상호작용합니다.\r\n\r\n```\r\n[사용자 응용 프로그램]\r\n ↕ (시스템 콜)\r\n [커널(Kernel)]\r\n ↕\r\n [하드웨어]\r\n```\r\n\r\n**커널의 종류:**\r\n- **모놀리식 커널**: 모든 서비스가 커널 공간에서 실행 (Linux)\r\n- **마이크로 커널**: 최소한의 기능만 커널에, 나머지는 사용자 공간에서 실행 (Mach)\r\n- **하이브리드 커널**: 두 방식의 혼합 (Windows NT)\r\n\r\n### 사용자 모드 vs 커널 모드\r\n- **사용자 모드(User Mode)**: 제한된 명령만 실행 가능\r\n- **커널 모드(Kernel Mode)**: 모든 하드웨어 접근이 가능\r\n- 시스템 콜을 통해 사용자 모드에서 커널 모드로 전환됩니다\r\n\r\n### 면접 팁\r\n운영체제의 목적을 \"자원 관리의 효율성\"과 \"사용자 편의성\" 두 가지 관점에서 설명하세요. 커널 모드와 사용자 모드의 분리가 왜 필요한지(보안, 안정성)도 설명할 수 있으면 좋습니다.", + "difficulty": "BASIC", + "tags": "운영체제, 커널, 시스템 자원 관리, 인터페이스", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 67, + "title": "프로세스 vs 스레드", + "content": "프로세스와 스레드의 차이점에 대해 설명해주세요.", + "answer": "### 프로세스 (Process)\r\n프로세스는 실행 중인 프로그램으로, 운영체제로부터 자원을 할당받는 작업의 단위입니다.\r\n\r\n**특징:**\r\n- 독립적인 메모리 공간을 가짐 (Code, Data, Stack, Heap)\r\n- 프로세스 간에는 메모리를 공유하지 않음\r\n- 프로세스 간 통신에는 IPC(Inter-Process Communication)가 필요\r\n- 하나의 프로세스가 다른 프로세스에 영향을 주지 않음\r\n\r\n### 스레드 (Thread)\r\n스레드는 프로세스 내에서 실행되는 흐름의 단위입니다.\r\n\r\n**특징:**\r\n- 프로세스 내에서 Code, Data, Heap 영역을 공유\r\n- 각 스레드는 독립적인 Stack과 PC(Program Counter)를 가짐\r\n- 스레드 간 자원 공유로 통신이 간단\r\n- 하나의 스레드에 문제가 생기면 같은 프로세스의 다른 스레드에도 영향\r\n\r\n```\r\n프로세스 메모리 구조:\r\n┌─────────────────────────┐\r\n│ Code 영역 │ ← 스레드 간 공유\r\n├─────────────────────────┤\r\n│ Data 영역 │ ← 스레드 간 공유\r\n├─────────────────────────┤\r\n│ Heap 영역 │ ← 스레드 간 공유\r\n├──────┬──────┬───────────┤\r\n│Stack1│Stack2│ Stack3 │ ← 스레드별 독립\r\n└──────┴──────┴───────────┘\r\n Thread1 Thread2 Thread3\r\n```\r\n\r\n### 비교\r\n\r\n| 구분 | 프로세스 | 스레드 |\r\n|------|---------|--------|\r\n| 메모리 | 독립 공간 | 공유 (Stack만 독립) |\r\n| 통신 | IPC 필요 | 공유 메모리로 간단 |\r\n| 생성 비용 | 높음 | 낮음 |\r\n| 컨텍스트 스위칭 | 비용 높음 | 비용 낮음 |\r\n| 안정성 | 하나가 죽어도 다른 프로세스 영향 없음 | 하나가 죽으면 전체 영향 |\r\n\r\n### 멀티프로세스 vs 멀티스레드\r\n\r\n**멀티프로세스:**\r\n- 안정성이 높음 (프로세스 간 독립)\r\n- 메모리 사용량이 큼\r\n- 컨텍스트 스위칭 비용이 큼\r\n- 예: Chrome 브라우저 (탭마다 별도 프로세스)\r\n\r\n**멀티스레드:**\r\n- 자원 공유로 효율적\r\n- 컨텍스트 스위칭 비용이 적음\r\n- 동기화 문제에 주의 필요 (Race Condition, Deadlock)\r\n- 예: 웹 서버의 요청 처리\r\n\r\n```java\r\n// 멀티스레드 예시\r\nclass MyThread extends Thread {\r\n public void run() {\r\n System.out.println(\"Thread: \" + Thread.currentThread().getName());\r\n }\r\n}\r\n\r\n// 메인에서 스레드 생성\r\nMyThread t1 = new MyThread();\r\nMyThread t2 = new MyThread();\r\nt1.start();\r\nt2.start();\r\n```\r\n\r\n### 면접 팁\r\n\"프로세스는 자원 할당의 단위, 스레드는 CPU 스케줄링의 단위\"라는 핵심을 기억하세요. 스레드가 Stack만 독립적으로 가지는 이유(각 스레드가 독립적인 실행 흐름을 가지므로)도 설명할 수 있으면 좋습니다.", + "difficulty": "BASIC", + "tags": "프로세스, 스레드, 멀티프로세스, 멀티스레드, 컨텍스트 스위칭", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 68, + "title": "프로세스 주소 공간", + "content": "프로세스의 주소 공간 구조에 대해 설명해주세요.", + "answer": "프로세스가 생성되면 운영체제로부터 독립적인 주소 공간을 할당받습니다. 이 주소 공간은 크게 Code, Data, Heap, Stack 네 가지 영역으로 나뉩니다.\r\n\r\n### 프로세스 주소 공간 구조\r\n\r\n```\r\n높은 주소 ┌─────────────────┐\r\n │ Stack 영역 │ ↓ 아래로 성장\r\n │ │\r\n ├─────────────────┤\r\n │ (빈 공간) │ ← Stack과 Heap 사이\r\n ├─────────────────┤\r\n │ Heap 영역 │ ↑ 위로 성장\r\n │ │\r\n ├─────────────────┤\r\n │ BSS 영역 │ 초기화되지 않은 전역 변수\r\n ├─────────────────┤\r\n │ Data 영역 │ 초기화된 전역/정적 변수\r\n ├─────────────────┤\r\n낮은 주소 │ Code(Text) │ 실행 코드\r\n └─────────────────┘\r\n```\r\n\r\n### 각 영역의 역할\r\n\r\n**1. Code(Text) 영역**\r\n- 실행할 프로그램의 기계어 코드가 저장됩니다\r\n- 읽기 전용(Read-Only)으로 프로그램 실행 중 변경되지 않습니다\r\n- 여러 프로세스가 같은 프로그램을 실행할 때 Code 영역을 공유할 수 있습니다\r\n\r\n**2. Data 영역**\r\n- 전역 변수와 정적(static) 변수가 저장됩니다\r\n- **Data**: 초기화된 전역/정적 변수\r\n- **BSS(Block Started by Symbol)**: 초기화되지 않은 전역/정적 변수 (0으로 초기화)\r\n- 프로그램 시작 시 할당되고 종료 시 해제됩니다\r\n\r\n```java\r\nstatic int initialized = 10; // Data 영역\r\nstatic int uninitialized; // BSS 영역\r\n```\r\n\r\n**3. Heap 영역**\r\n- 동적으로 할당되는 메모리 공간입니다\r\n- 프로그래머가 직접 관리합니다 (Java에서는 GC가 관리)\r\n- 낮은 주소에서 높은 주소 방향으로 성장합니다\r\n- 런타임에 크기가 결정됩니다\r\n\r\n```java\r\n// Heap에 할당\r\nObject obj = new Object(); // Heap\r\nint[] arr = new int[100]; // Heap\r\nString str = new String(\"hello\"); // Heap\r\n```\r\n\r\n**4. Stack 영역**\r\n- 함수 호출 시 지역 변수, 매개변수, 리턴 주소가 저장됩니다\r\n- 함수 호출 시 push, 반환 시 pop 됩니다\r\n- 높은 주소에서 낮은 주소 방향으로 성장합니다\r\n- 컴파일 시 크기가 결정됩니다\r\n- 재귀가 너무 깊으면 Stack Overflow가 발생합니다\r\n\r\n```java\r\nvoid function() {\r\n int localVar = 10; // Stack\r\n int[] ref = new int[5]; // ref는 Stack, 배열 자체는 Heap\r\n}\r\n```\r\n\r\n### 영역을 분리하는 이유\r\n\r\n1. **메모리 효율성**: Code 영역은 같은 프로그램 실행 시 공유 가능\r\n2. **보안**: Code 영역을 읽기 전용으로 보호\r\n3. **관리 용이성**: 각 영역의 특성에 맞는 메모리 관리 전략 적용\r\n\r\n### 면접 팁\r\nStack과 Heap의 차이를 명확히 구분하세요. Java에서 기본 타입은 Stack에, 객체는 Heap에 할당된다는 점, 그리고 Stack Overflow와 Out of Memory의 차이를 설명할 수 있어야 합니다.", + "difficulty": "INTERMEDIATE", + "tags": "프로세스 주소 공간, 코드 영역, 데이터 영역, 스택, 힙", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 69, + "title": "인터럽트(Interrupt)", + "content": "인터럽트란 무엇이며, 처리 과정에 대해 설명해주세요.", + "answer": "인터럽트(Interrupt)는 CPU가 현재 실행 중인 작업을 일시 중단하고 우선적으로 처리해야 할 이벤트가 발생했을 때, 해당 이벤트를 처리한 후 다시 원래 작업으로 복귀하는 메커니즘입니다.\r\n\r\n### 인터럽트의 종류\r\n\r\n**1. 하드웨어 인터럽트**\r\n외부 하드웨어 장치에 의해 발생합니다.\r\n- 키보드 입력, 마우스 클릭\r\n- 디스크 I/O 완료\r\n- 타이머 인터럽트 (프로세스 스케줄링에 사용)\r\n- 네트워크 패킷 도착\r\n\r\n**2. 소프트웨어 인터럽트 (Trap)**\r\n소프트웨어적으로 발생하는 인터럽트입니다.\r\n- **시스템 콜**: 프로그램이 OS 서비스를 요청\r\n- **예외(Exception)**: 0으로 나누기, 잘못된 메모리 접근\r\n- **디버거 브레이크포인트**\r\n\r\n### 인터럽트 처리 과정\r\n\r\n```\r\n1. 인터럽트 발생\r\n2. CPU: 현재 실행 중인 명령어 완료\r\n3. CPU: 현재 상태(PC, 레지스터 등)를 스택에 저장\r\n4. 인터럽트 벡터 테이블에서 해당 ISR의 주소를 찾음\r\n5. ISR(Interrupt Service Routine) 실행\r\n6. 이전 상태를 복원하고 원래 작업으로 복귀\r\n```\r\n\r\n```\r\n인터럽트 벡터 테이블:\r\n┌─────────┬──────────────────┐\r\n│ 번호 │ ISR 주소 │\r\n├─────────┼──────────────────┤\r\n│ 0 │ 0x0000 (0 나누기) │\r\n│ 1 │ 0x0100 (키보드) │\r\n│ 2 │ 0x0200 (타이머) │\r\n│ ... │ ... │\r\n└─────────┴──────────────────┘\r\n```\r\n\r\n### 인터럽트와 폴링 비교\r\n\r\n**인터럽트 방식:**\r\n- 이벤트 발생 시 CPU에 알림\r\n- CPU가 다른 작업을 수행할 수 있음\r\n- 효율적이고 반응이 빠름\r\n\r\n**폴링(Polling) 방식:**\r\n- CPU가 주기적으로 상태를 확인\r\n- CPU 자원을 낭비할 수 있음\r\n- 구현이 간단\r\n\r\n### 인터럽트 우선순위\r\n여러 인터럽트가 동시에 발생할 경우 우선순위에 따라 처리합니다.\r\n```\r\n전원 이상 > 기계 장애 > 외부 인터럽트 > I/O > 프로그램 검사 > SVC\r\n```\r\n\r\n인터럽트 처리 중에 더 높은 우선순위의 인터럽트가 발생하면 현재 ISR을 중단하고 새로운 인터럽트를 먼저 처리합니다 (중첩 인터럽트).\r\n\r\n### 면접 팁\r\n인터럽트는 OS의 핵심 동작 원리입니다. 특히 시스템 콜이 소프트웨어 인터럽트(Trap)를 통해 구현된다는 점을 연결하여 설명하면 좋습니다. 인터럽트 없이는 멀티태스킹이 불가능합니다.", + "difficulty": "INTERMEDIATE", + "tags": "인터럽트, 하드웨어 인터럽트, 소프트웨어 인터럽트, ISR, 인터럽트 벡터", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 70, + "title": "시스템 콜(System Call)", + "content": "시스템 콜이란 무엇이며, 어떻게 동작하나요?", + "answer": "시스템 콜(System Call)은 운영체제가 제공하는 서비스에 접근하기 위해 사용자 프로그램이 커널에 요청하는 인터페이스입니다. 사용자 모드에서 직접 접근할 수 없는 하드웨어 자원이나 커널 기능을 사용할 때 필요합니다.\r\n\r\n### 동작 과정\r\n\r\n```\r\n1. 사용자 프로그램에서 시스템 콜 호출\r\n2. 소프트웨어 인터럽트(Trap) 발생\r\n3. 사용자 모드 → 커널 모드 전환\r\n4. 시스템 콜 번호를 기반으로 해당 핸들러 실행\r\n5. 커널이 요청된 작업 수행\r\n6. 결과를 반환하고 커널 모드 → 사용자 모드 전환\r\n7. 사용자 프로그램 계속 실행\r\n```\r\n\r\n```\r\n[사용자 프로그램] ─── printf(\"hello\") ───→\r\n[C 라이브러리] ─── write() 호출 ───→\r\n[시스템 콜 인터페이스] ─── Trap (인터럽트) ───→\r\n[커널] ─── sys_write() 실행\r\n ← 결과 반환 ───\r\n```\r\n\r\n### 시스템 콜의 유형\r\n\r\n**1. 프로세스 제어**\r\n```c\r\nfork() // 새 프로세스 생성\r\nexec() // 프로세스 실행\r\nexit() // 프로세스 종료\r\nwait() // 자식 프로세스 대기\r\ngetpid() // 프로세스 ID 반환\r\n```\r\n\r\n**2. 파일 관리**\r\n```c\r\nopen() // 파일 열기\r\nclose() // 파일 닫기\r\nread() // 파일 읽기\r\nwrite() // 파일 쓰기\r\nlseek() // 파일 포인터 이동\r\n```\r\n\r\n**3. 장치 관리**\r\n```c\r\nioctl() // 장치 제어\r\nread() // 장치에서 읽기\r\nwrite() // 장치에 쓰기\r\n```\r\n\r\n**4. 정보 유지**\r\n```c\r\ntime() // 시간 정보\r\ngetpid() // 프로세스 정보\r\n```\r\n\r\n**5. 통신**\r\n```c\r\npipe() // 파이프 생성\r\nshmget() // 공유 메모리 생성\r\nsocket() // 소켓 생성\r\n```\r\n\r\n### 시스템 콜과 라이브러리 함수의 차이\r\n\r\n```c\r\n// 라이브러리 함수: 사용자 모드에서 실행\r\nprintf(\"Hello\"); // 내부적으로 write() 시스템 콜 호출\r\n\r\n// 시스템 콜: 커널 모드에서 실행\r\nwrite(1, \"Hello\", 5);\r\n```\r\n\r\n| 구분 | 라이브러리 함수 | 시스템 콜 |\r\n|------|--------------|----------|\r\n| 실행 모드 | 사용자 모드 | 커널 모드 |\r\n| 오버헤드 | 적음 | 모드 전환 비용 |\r\n| 예시 | printf, malloc | write, brk |\r\n\r\n### 시스템 콜이 필요한 이유\r\n- 사용자 프로그램이 직접 하드웨어에 접근하면 보안 문제 발생\r\n- 여러 프로그램이 동시에 자원에 접근할 때 충돌 방지\r\n- 하드웨어 추상화를 통한 이식성 확보\r\n\r\n### 면접 팁\r\n시스템 콜은 OS의 \"문지기\" 역할을 합니다. 사용자 프로그램이 아무리 복잡해도 결국 시스템 콜을 통해 OS 서비스를 이용합니다. fork()와 exec()의 차이를 이해하고, 모드 전환의 오버헤드에 대해서도 설명할 수 있으면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "시스템 콜, 커널 모드, 사용자 모드, 트랩, API", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 71, + "title": "PCB와 Context Switching", + "content": "PCB(Process Control Block)와 Context Switching에 대해 설명해주세요.", + "answer": "### PCB (Process Control Block)\r\nPCB는 운영체제가 각 프로세스를 관리하기 위해 유지하는 자료구조입니다. 프로세스가 생성되면 PCB가 만들어지고, 종료되면 제거됩니다.\r\n\r\n**PCB에 저장되는 정보:**\r\n\r\n```\r\n┌─────────────────────────┐\r\n│ 프로세스 ID (PID) │\r\n│ 프로세스 상태 │ (Ready, Running, Waiting 등)\r\n│ 프로그램 카운터 (PC) │ 다음 실행할 명령어 주소\r\n│ CPU 레지스터 정보 │ 레지스터 값들\r\n│ CPU 스케줄링 정보 │ 우선순위, 스케줄링 큐 포인터\r\n│ 메모리 관리 정보 │ 페이지 테이블, 세그먼트 테이블\r\n│ I/O 상태 정보 │ 할당된 I/O 장치, 열린 파일 목록\r\n│ 계정 정보 │ CPU 사용 시간, 시간 제한\r\n└─────────────────────────┘\r\n```\r\n\r\n### Context Switching (문맥 교환)\r\n\r\n현재 실행 중인 프로세스(또는 스레드)의 상태를 PCB에 저장하고, 다음 실행할 프로세스의 상태를 PCB에서 복원하여 CPU를 할당하는 과정입니다.\r\n\r\n**발생 시점:**\r\n- 프로세스의 타임 슬라이스가 만료되었을 때\r\n- I/O 요청으로 프로세스가 대기 상태로 전환될 때\r\n- 더 높은 우선순위의 프로세스가 도착했을 때\r\n- 인터럽트가 발생했을 때\r\n\r\n**Context Switching 과정:**\r\n\r\n```\r\n[프로세스 A 실행 중]\r\n ↓ 인터럽트 또는 시스템 콜\r\n[프로세스 A의 상태를 PCB_A에 저장]\r\n - PC, 레지스터, 스택 포인터 등\r\n ↓\r\n[스케줄러가 다음 프로세스 B를 선택]\r\n ↓\r\n[PCB_B에서 프로세스 B의 상태를 복원]\r\n - PC, 레지스터, 스택 포인터 등\r\n ↓\r\n[프로세스 B 실행 시작]\r\n```\r\n\r\n### Context Switching 오버헤드\r\n\r\nContext Switching 자체는 유용한 작업을 수행하지 않는 순수한 오버헤드입니다.\r\n\r\n**오버헤드 원인:**\r\n1. PCB 저장 및 복원에 소요되는 시간\r\n2. CPU 캐시(Cache) 무효화 - 새 프로세스의 데이터로 캐시를 다시 채워야 함\r\n3. TLB(Translation Lookaside Buffer) 플러시\r\n4. 파이프라인 플러시\r\n\r\n**프로세스 vs 스레드의 Context Switching:**\r\n- **프로세스**: 메모리 주소 공간이 다르므로 페이지 테이블, TLB 등도 교체 → 비용 큼\r\n- **스레드**: 같은 프로세스 내 스레드는 메모리를 공유하므로 → 비용 작음\r\n\r\n### 오버헤드 최소화 방법\r\n- 스레드 사용 (메모리 공유로 비용 감소)\r\n- 적절한 스케줄링 알고리즘 선택\r\n- 너무 잦은 Context Switching 방지 (타임 슬라이스 조절)\r\n\r\n### 면접 팁\r\nContext Switching의 비용이 왜 발생하는지(캐시 무효화, TLB 플러시)를 구체적으로 설명할 수 있으면 좋습니다. 프로세스 간 Context Switching이 스레드 간보다 비용이 큰 이유를 메모리 구조의 차이와 연결하여 설명하세요.", + "difficulty": "INTERMEDIATE", + "tags": "PCB, 컨텍스트 스위칭, 프로세스 제어 블록, 오버헤드, 스케줄링", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 72, + "title": "IPC (Inter-Process Communication)", + "content": "IPC(프로세스 간 통신)란 무엇이며, 어떤 방법들이 있나요?", + "answer": "IPC(Inter-Process Communication)는 프로세스들 사이에 데이터를 주고받는 메커니즘입니다. 프로세스는 독립적인 메모리 공간을 가지므로 직접적인 데이터 공유가 불가능하여 IPC가 필요합니다.\r\n\r\n### IPC 방법\r\n\r\n**1. 파이프 (Pipe)**\r\n한 방향으로만 데이터가 흐르는 통신 방법입니다.\r\n\r\n```c\r\n// 익명 파이프 (부모-자식 프로세스 간)\r\nint fd[2];\r\npipe(fd); // fd[0]: 읽기, fd[1]: 쓰기\r\n\r\nif (fork() == 0) { // 자식\r\n close(fd[1]);\r\n read(fd[0], buf, sizeof(buf));\r\n} else { // 부모\r\n close(fd[0]);\r\n write(fd[1], \"Hello\", 5);\r\n}\r\n```\r\n\r\n- **익명 파이프**: 부모-자식 관계에서만 사용 가능, 단방향\r\n- **Named Pipe(FIFO)**: 관계없는 프로세스 간에도 사용 가능\r\n\r\n**2. 메시지 큐 (Message Queue)**\r\n메시지를 큐에 넣고 받는 방식으로, 커널이 관리합니다.\r\n- 비동기 통신 가능\r\n- 메시지에 타입을 지정하여 선택적 수신 가능\r\n- 파이프보다 유연하지만 커널 메모리 사용\r\n\r\n**3. 공유 메모리 (Shared Memory)**\r\n여러 프로세스가 동일한 메모리 영역을 공유합니다.\r\n- 가장 빠른 IPC 방법 (커널 개입 없이 직접 접근)\r\n- 동기화가 필요 (세마포어, 뮤텍스 사용)\r\n- 대용량 데이터 공유에 적합\r\n\r\n```c\r\n// 공유 메모리 생성\r\nint shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);\r\nchar *data = shmat(shmid, NULL, 0);\r\n\r\n// 데이터 쓰기\r\nstrcpy(data, \"Hello from Process A\");\r\n\r\n// 다른 프로세스에서 읽기\r\nprintf(\"%s\\n\", data); // \"Hello from Process A\"\r\n```\r\n\r\n**4. 소켓 (Socket)**\r\n네트워크를 통한 프로세스 간 통신 방법으로, 다른 시스템의 프로세스와도 통신 가능합니다.\r\n- TCP/UDP 소켓\r\n- Unix Domain Socket (같은 시스템 내)\r\n- 양방향 통신\r\n\r\n**5. 시그널 (Signal)**\r\n프로세스에 이벤트를 알리는 소프트웨어 인터럽트입니다.\r\n- SIGKILL, SIGTERM, SIGINT 등\r\n- 간단한 알림에 적합, 데이터 전달에는 부적합\r\n\r\n**6. 메모리 맵 파일 (Memory-Mapped File)**\r\n파일을 메모리에 매핑하여 프로세스 간 공유합니다.\r\n\r\n### IPC 방법 비교\r\n\r\n| 방법 | 속도 | 범위 | 양방향 | 동기화 |\r\n|------|------|------|--------|--------|\r\n| 파이프 | 중간 | 같은 시스템 | 단방향 | 불필요 |\r\n| 메시지 큐 | 중간 | 같은 시스템 | 양방향 | 불필요 |\r\n| 공유 메모리 | 빠름 | 같은 시스템 | 양방향 | 필요 |\r\n| 소켓 | 느림 | 다른 시스템 | 양방향 | 불필요 |\r\n| 시그널 | 빠름 | 같은 시스템 | 단방향 | 불필요 |\r\n\r\n### 면접 팁\r\n각 IPC 방법의 장단점을 비교하여 상황에 맞는 방법을 선택할 수 있어야 합니다. 공유 메모리가 가장 빠르지만 동기화 문제가 있고, 소켓은 네트워크를 통해 분산 시스템에서도 사용할 수 있다는 점을 기억하세요.", + "difficulty": "INTERMEDIATE", + "tags": "IPC, 파이프, 메시지 큐, 공유 메모리, 소켓", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 73, + "title": "CPU 스케줄링", + "content": "CPU 스케줄링 알고리즘의 종류와 특징에 대해 설명해주세요.", + "answer": "CPU 스케줄링은 Ready Queue에 있는 프로세스 중 어떤 프로세스에 CPU를 할당할지 결정하는 정책입니다. 멀티프로그래밍 환경에서 CPU 이용률을 극대화하기 위해 필수적입니다.\r\n\r\n### 선점형 vs 비선점형\r\n\r\n**비선점형(Non-preemptive)**: 프로세스가 CPU를 자발적으로 반납할 때까지 빼앗을 수 없음\r\n**선점형(Preemptive)**: 운영체제가 필요에 따라 CPU를 강제로 회수할 수 있음\r\n\r\n### 스케줄링 알고리즘\r\n\r\n**1. FCFS (First Come First Served) - 비선점**\r\n도착 순서대로 처리합니다.\r\n- 장점: 구현이 간단, 공정\r\n- 단점: 호위 효과(Convoy Effect) - 긴 작업 뒤의 짧은 작업이 오래 대기\r\n\r\n```\r\n프로세스: P1(24ms), P2(3ms), P3(3ms)\r\n도착 순서: P1 → P2 → P3\r\n실행: |---P1(24ms)---|P2(3ms)|P3(3ms)|\r\n평균 대기 시간: (0 + 24 + 27) / 3 = 17ms\r\n```\r\n\r\n**2. SJF (Shortest Job First) - 비선점/선점(SRTF)**\r\n실행 시간이 가장 짧은 프로세스를 먼저 실행합니다.\r\n- 장점: 평균 대기 시간 최소화 (이론상 최적)\r\n- 단점: 기아(Starvation) 발생 가능, 실행 시간 예측 어려움\r\n\r\n**3. Priority Scheduling - 비선점/선점**\r\n우선순위가 높은 프로세스를 먼저 실행합니다.\r\n- 단점: 기아 문제 → **에이징(Aging)** 기법으로 해결 (대기 시간에 따라 우선순위 증가)\r\n\r\n**4. Round Robin (RR) - 선점**\r\n각 프로세스에 동일한 시간 할당량(Time Quantum)을 부여하고 순환 실행합니다.\r\n\r\n```\r\n프로세스: P1(24ms), P2(3ms), P3(3ms), Time Quantum = 4ms\r\n실행: |P1(4)|P2(3)|P3(3)|P1(4)|P1(4)|P1(4)|P1(4)|P1(4)|\r\n```\r\n\r\n- 장점: 응답 시간이 짧음, 공정\r\n- 단점: Time Quantum 크기에 따라 성능이 달라짐\r\n - 너무 크면 FCFS와 같아짐\r\n - 너무 작으면 Context Switching 오버헤드 증가\r\n\r\n**5. 멀티레벨 큐 (Multilevel Queue)**\r\n프로세스를 여러 그룹으로 분류하고 각 그룹별 다른 스케줄링 적용합니다.\r\n\r\n```\r\n높은 우선순위 ← [시스템 프로세스] (RR)\r\n [대화형 프로세스] (RR)\r\n [배치 프로세스] (FCFS)\r\n낮은 우선순위 ← [학생 프로세스] (FCFS)\r\n```\r\n\r\n**6. 멀티레벨 피드백 큐 (MLFQ)**\r\n프로세스가 큐 사이를 이동할 수 있는 유연한 스케줄링입니다. 가장 일반적으로 사용됩니다.\r\n\r\n### 평가 기준\r\n- **CPU 이용률**: CPU가 작업하는 시간의 비율\r\n- **처리량(Throughput)**: 단위 시간당 완료된 프로세스 수\r\n- **대기 시간**: Ready Queue에서 대기한 시간\r\n- **응답 시간**: 요청부터 첫 응답까지의 시간\r\n- **반환 시간(Turnaround Time)**: 제출부터 완료까지의 시간\r\n\r\n### 면접 팁\r\n실제 OS에서는 단일 알고리즘보다 MLFQ 같은 복합적인 방식을 사용합니다. 각 알고리즘의 장단점과 기아 문제 해결 방법(에이징)을 함께 설명하세요.", + "difficulty": "INTERMEDIATE", + "tags": "CPU 스케줄링, FCFS, SJF, RR, 우선순위, 선점형", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 74, + "title": "데드락(Deadlock)", + "content": "데드락(교착 상태)이란 무엇이며, 발생 조건과 해결 방법에 대해 설명해주세요.", + "answer": "데드락(Deadlock)은 두 개 이상의 프로세스가 서로가 가진 자원을 기다리면서 무한히 대기하는 상태입니다.\r\n\r\n### 데드락 예시\r\n```\r\n프로세스 A: 자원 1을 보유, 자원 2를 대기\r\n프로세스 B: 자원 2를 보유, 자원 1을 대기\r\n\r\n→ 서로 상대방의 자원을 기다리며 무한 대기\r\n```\r\n\r\n```java\r\n// 데드락 발생 예시\r\nThread t1 = new Thread(() -> {\r\n synchronized (lockA) { // lockA 획득\r\n Thread.sleep(100);\r\n synchronized (lockB) { // lockB 대기 → 데드락!\r\n // ...\r\n }\r\n }\r\n});\r\n\r\nThread t2 = new Thread(() -> {\r\n synchronized (lockB) { // lockB 획득\r\n Thread.sleep(100);\r\n synchronized (lockA) { // lockA 대기 → 데드락!\r\n // ...\r\n }\r\n }\r\n});\r\n```\r\n\r\n### 데드락 발생 조건 (4가지 모두 만족해야 발생)\r\n\r\n1. **상호 배제 (Mutual Exclusion)**: 자원은 한 번에 하나의 프로세스만 사용 가능\r\n2. **점유 대기 (Hold and Wait)**: 자원을 보유한 상태에서 다른 자원을 대기\r\n3. **비선점 (No Preemption)**: 다른 프로세스가 보유한 자원을 강제로 빼앗을 수 없음\r\n4. **환형 대기 (Circular Wait)**: 프로세스 간 자원 대기 관계가 원형을 이룸\r\n\r\n### 데드락 해결 방법\r\n\r\n**1. 예방 (Prevention)**\r\n4가지 조건 중 하나 이상을 원천적으로 차단합니다.\r\n- 점유 대기 제거: 모든 자원을 한꺼번에 요청\r\n- 비선점 제거: 자원을 강제로 회수 가능하게 함\r\n- 환형 대기 제거: 자원에 순서를 부여하여 순서대로만 요청\r\n\r\n```java\r\n// 환형 대기 제거: 항상 lockA → lockB 순서로 획득\r\nsynchronized (lockA) {\r\n synchronized (lockB) {\r\n // 안전\r\n }\r\n}\r\n```\r\n\r\n**2. 회피 (Avoidance)**\r\n시스템이 안전한 상태(Safe State)를 유지하도록 자원 할당을 결정합니다.\r\n- **은행원 알고리즘(Banker's Algorithm)**: 자원 요청 시 안전 상태를 유지할 수 있으면 할당, 아니면 대기\r\n\r\n**3. 탐지 (Detection)**\r\n데드락 발생을 허용하고, 주기적으로 탐지합니다.\r\n- 자원 할당 그래프에서 사이클 탐지\r\n- 탐지 알고리즘을 주기적으로 실행\r\n\r\n**4. 회복 (Recovery)**\r\n데드락이 탐지되면 해결합니다.\r\n- 프로세스 종료 (일부 또는 전체)\r\n- 자원 강제 선점 (일부 프로세스의 자원을 회수)\r\n- 롤백 (체크포인트로 되돌림)\r\n\r\n### 면접 팁\r\n데드락의 4가지 조건을 정확히 기억하고, 각 조건을 제거하여 예방하는 방법을 설명하세요. 실무에서는 주로 데이터베이스의 Lock 경합으로 데드락이 발생하며, DB는 보통 탐지 후 하나의 트랜잭션을 롤백하는 방식으로 해결합니다.", + "difficulty": "INTERMEDIATE", + "tags": "데드락, 교착상태, 상호배제, 점유대기, 환형대기", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 75, + "title": "Race Condition", + "content": "Race Condition이란 무엇이며, 어떻게 해결하나요?", + "answer": "Race Condition(경쟁 상태)은 여러 프로세스나 스레드가 공유 자원에 동시에 접근할 때, 실행 순서에 따라 결과가 달라지는 문제입니다. 데이터의 일관성이 깨질 수 있어 매우 위험합니다.\r\n\r\n### Race Condition 예시\r\n\r\n```java\r\n// 공유 변수\r\nint count = 0;\r\n\r\n// 스레드 1, 2가 동시에 실행\r\nvoid increment() {\r\n count++; // 원자적이지 않은 연산!\r\n}\r\n\r\n// count++ 내부 동작:\r\n// 1. count 값을 레지스터에 로드\r\n// 2. 레지스터 값을 1 증가\r\n// 3. 레지스터 값을 count에 저장\r\n\r\n// 스레드 1: LOAD count(0) → INC → STORE(1)\r\n// 스레드 2: LOAD count(0) → INC → STORE(1)\r\n// 결과: count = 1 (기대값: 2)\r\n```\r\n\r\n### 임계 구역 (Critical Section)\r\n공유 자원에 접근하는 코드 영역을 임계 구역이라 합니다. 한 번에 하나의 스레드만 실행되어야 합니다.\r\n\r\n**임계 구역 문제 해결의 3가지 조건:**\r\n1. **상호 배제(Mutual Exclusion)**: 한 스레드가 임계 구역에 있으면 다른 스레드는 진입 불가\r\n2. **진행(Progress)**: 임계 구역이 비어있으면 진입 가능해야 함\r\n3. **한정 대기(Bounded Waiting)**: 무한정 대기하지 않아야 함\r\n\r\n### 해결 방법\r\n\r\n**1. synchronized (Java)**\r\n```java\r\npublic synchronized void increment() {\r\n count++;\r\n}\r\n\r\n// 또는 블록 수준\r\npublic void increment() {\r\n synchronized (this) {\r\n count++;\r\n }\r\n}\r\n```\r\n\r\n**2. Lock (ReentrantLock)**\r\n```java\r\nprivate final Lock lock = new ReentrantLock();\r\n\r\npublic void increment() {\r\n lock.lock();\r\n try {\r\n count++;\r\n } finally {\r\n lock.unlock();\r\n }\r\n}\r\n```\r\n\r\n**3. Atomic 클래스**\r\n```java\r\nprivate AtomicInteger count = new AtomicInteger(0);\r\n\r\npublic void increment() {\r\n count.incrementAndGet(); // CAS 연산으로 원자적 증가\r\n}\r\n```\r\n\r\n**4. volatile 키워드**\r\n```java\r\nprivate volatile boolean flag = false;\r\n// 메모리 가시성은 보장하지만, 원자성은 보장하지 않음\r\n```\r\n\r\n### Race Condition 발생 시나리오\r\n- **커널 모드**: 여러 프로세스가 커널 데이터를 동시에 수정\r\n- **프로세스 간**: 공유 메모리(IPC)를 동시에 접근\r\n- **스레드 간**: 같은 프로세스의 전역 변수를 동시에 접근\r\n\r\n### 면접 팁\r\nRace Condition은 재현이 어렵고 디버깅하기 까다로운 버그를 유발합니다. synchronized와 Lock의 차이, CAS(Compare-And-Swap) 연산의 원리를 이해하면 좋습니다. 실무에서는 가능하면 불변 객체를 사용하거나 스레드 로컬 변수를 활용하여 근본적으로 공유를 피하는 것이 가장 좋은 해결책입니다.", + "difficulty": "INTERMEDIATE", + "tags": "경쟁 상태, 동기화, 임계 구역, 원자적 연산, 동시성", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 76, + "title": "세마포어(Semaphore) & 뮤텍스(Mutex)", + "content": "세마포어와 뮤텍스의 차이점에 대해 설명해주세요.", + "answer": "### 뮤텍스 (Mutex - Mutual Exclusion)\r\n뮤텍스는 임계 구역에 대한 상호 배제를 보장하는 동기화 도구입니다. 한 번에 하나의 스레드만 자원에 접근할 수 있습니다.\r\n\r\n**특징:**\r\n- 잠금(Lock)과 해제(Unlock) 두 가지 연산만 존재\r\n- **소유권 개념이 있음**: 잠금을 획득한 스레드만 해제 가능\r\n- 이진 상태 (잠김/해제)\r\n\r\n```java\r\n// Java에서 뮤텍스 역할\r\nprivate final Object mutex = new Object();\r\n\r\npublic void criticalSection() {\r\n synchronized (mutex) {\r\n // 임계 구역 - 한 스레드만 진입 가능\r\n }\r\n}\r\n```\r\n\r\n### 세마포어 (Semaphore)\r\n세마포어는 정수 변수와 P(wait), V(signal) 두 가지 원자적 연산으로 구성된 동기화 도구입니다.\r\n\r\n**P(wait) 연산**: 세마포어 값을 1 감소. 값이 음수이면 대기\r\n**V(signal) 연산**: 세마포어 값을 1 증가. 대기 중인 프로세스를 깨움\r\n\r\n```\r\nP(S) {\r\n S--;\r\n if (S < 0) block(); // 대기 큐에 추가\r\n}\r\n\r\nV(S) {\r\n S++;\r\n if (S <= 0) wakeup(); // 대기 큐에서 하나 깨움\r\n}\r\n```\r\n\r\n**이진 세마포어(Binary Semaphore)**: 0 또는 1만 가짐 (뮤텍스와 유사)\r\n**카운팅 세마포어(Counting Semaphore)**: 여러 개의 자원에 대한 동시 접근 제어\r\n\r\n```java\r\n// Java 세마포어 - 동시에 3개 스레드만 접근 허용\r\nSemaphore semaphore = new Semaphore(3);\r\n\r\npublic void accessResource() throws InterruptedException {\r\n semaphore.acquire(); // P 연산\r\n try {\r\n // 임계 구역 - 최대 3개 스레드 동시 진입 가능\r\n doWork();\r\n } finally {\r\n semaphore.release(); // V 연산\r\n }\r\n}\r\n```\r\n\r\n### 뮤텍스 vs 세마포어 비교\r\n\r\n| 구분 | 뮤텍스 | 세마포어 |\r\n|------|--------|---------|\r\n| 동기화 대상 수 | 1개 | N개 |\r\n| 소유권 | 있음 (Lock한 스레드만 Unlock) | 없음 (다른 스레드가 signal 가능) |\r\n| 목적 | 상호 배제 | 접근 순서 제어, 리소스 카운팅 |\r\n| 값 | 0 또는 1 | 0 이상의 정수 |\r\n\r\n### 활용 예시\r\n\r\n**생산자-소비자 문제:**\r\n```java\r\nSemaphore empty = new Semaphore(BUFFER_SIZE); // 빈 공간 수\r\nSemaphore full = new Semaphore(0); // 채워진 공간 수\r\nSemaphore mutex = new Semaphore(1); // 버퍼 접근 제어\r\n\r\n// 생산자\r\nvoid producer() {\r\n empty.acquire(); // 빈 공간 확인\r\n mutex.acquire(); // 버퍼 접근 잠금\r\n addToBuffer(item);\r\n mutex.release(); // 버퍼 접근 해제\r\n full.release(); // 채워진 공간 증가\r\n}\r\n\r\n// 소비자\r\nvoid consumer() {\r\n full.acquire(); // 채워진 공간 확인\r\n mutex.acquire(); // 버퍼 접근 잠금\r\n removeFromBuffer();\r\n mutex.release(); // 버퍼 접근 해제\r\n empty.release(); // 빈 공간 증가\r\n}\r\n```\r\n\r\n### 면접 팁\r\n가장 중요한 차이점은 \"소유권\"입니다. 뮤텍스는 Lock을 획득한 스레드만 해제할 수 있지만, 세마포어는 어떤 스레드든 signal을 보낼 수 있습니다. 또한 세마포어는 순서 제어에도 사용할 수 있다는 점도 기억하세요.", + "difficulty": "INTERMEDIATE", + "tags": "세마포어, 뮤텍스, 동기화, 상호배제, P/V 연산", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 77, + "title": "페이징(Paging) & 세그먼테이션(Segmentation)", + "content": "페이징과 세그먼테이션의 차이점에 대해 설명해주세요.", + "answer": "페이징과 세그먼테이션은 가상 메모리를 물리 메모리에 매핑하는 메모리 관리 기법입니다. 프로세스의 주소 공간을 물리 메모리에 불연속적으로 배치할 수 있게 해줍니다.\r\n\r\n### 페이징 (Paging)\r\n\r\n프로세스의 가상 주소 공간을 동일한 크기의 **페이지(Page)**로 나누고, 물리 메모리를 같은 크기의 **프레임(Frame)**으로 나누어 매핑합니다.\r\n\r\n```\r\n가상 주소 공간: 물리 메모리:\r\n┌─────────┐ ┌─────────┐\r\n│ Page 0 │ ──────────→ │ Frame 3 │\r\n├─────────┤ ├─────────┤\r\n│ Page 1 │ ──────→ │ Frame 0 │ ← Page 2\r\n├─────────┤ │ ├─────────┤\r\n│ Page 2 │ ──→ │ │ Frame 5 │ ← Page 1\r\n├─────────┤ │ │ ├─────────┤\r\n│ Page 3 │ │ │ │ Frame 2 │ ← Page 3\r\n└─────────┘ │ │ ├─────────┤\r\n │ └──→ │ Frame 1 │\r\n └───────→ │ Frame 4 │\r\n```\r\n\r\n**가상 주소 변환:**\r\n```\r\n가상 주소 = 페이지 번호(p) + 오프셋(d)\r\n물리 주소 = 프레임 번호(f) + 오프셋(d)\r\n페이지 테이블[p] → f\r\n```\r\n\r\n**특징:**\r\n- 외부 단편화가 발생하지 않음\r\n- 내부 단편화가 발생할 수 있음 (마지막 페이지)\r\n- 페이지 크기가 고정 (보통 4KB)\r\n- 페이지 테이블을 위한 메모리 필요\r\n\r\n### 세그먼테이션 (Segmentation)\r\n\r\n프로세스를 논리적 의미 단위인 **세그먼트(Segment)**로 나눕니다. Code, Data, Stack, Heap 등이 각각 하나의 세그먼트가 됩니다.\r\n\r\n```\r\n가상 주소: (세그먼트 번호, 오프셋)\r\n세그먼트 테이블: [기준 주소(Base), 한계(Limit)]\r\n\r\n세그먼트 테이블:\r\n| 번호 | Base | Limit |\r\n|------|------|-------|\r\n| 0 | 1400 | 1000 |\r\n| 1 | 6300 | 400 |\r\n| 2 | 4300 | 400 |\r\n```\r\n\r\n**특징:**\r\n- 논리적 단위로 분할하여 의미 있는 보호 가능\r\n- 세그먼트마다 크기가 다름\r\n- 외부 단편화가 발생할 수 있음\r\n- 내부 단편화는 없음\r\n\r\n### 비교\r\n\r\n| 특성 | 페이징 | 세그먼테이션 |\r\n|------|--------|-------------|\r\n| 분할 단위 | 고정 크기 (Page) | 가변 크기 (Segment) |\r\n| 분할 기준 | 물리적 | 논리적 |\r\n| 외부 단편화 | 없음 | 있음 |\r\n| 내부 단편화 | 있음 | 없음 |\r\n| 주소 변환 | 페이지 테이블 | 세그먼트 테이블 |\r\n| 공유/보호 | 페이지 단위 | 세그먼트 단위 (자연스러움) |\r\n\r\n### 페이지드 세그먼테이션 (Paged Segmentation)\r\n두 기법을 결합한 방식으로, 세그먼트를 다시 페이지로 나눕니다. 세그먼테이션의 논리적 분할과 페이징의 외부 단편화 해결을 동시에 달성합니다.\r\n\r\n### TLB (Translation Lookaside Buffer)\r\n페이지 테이블 참조에 의한 성능 저하를 해결하기 위한 캐시입니다. 최근 사용된 페이지-프레임 매핑을 고속 하드웨어에 캐싱합니다.\r\n\r\n### 면접 팁\r\n\"왜 현대 OS는 세그먼테이션보다 페이징을 선호하는가?\"에 대한 답을 준비하세요. 외부 단편화가 없고, 고정 크기로 관리가 용이하며, 하드웨어 지원이 효율적이기 때문입니다.", + "difficulty": "ADVANCED", + "tags": "페이징, 세그먼테이션, 가상 메모리, 페이지 테이블, 외부 단편화", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 78, + "title": "페이지 교체 알고리즘", + "content": "페이지 교체 알고리즘의 종류와 각각의 특징에 대해 설명해주세요.", + "answer": "페이지 교체 알고리즘은 물리 메모리가 부족할 때 어떤 페이지를 교체할지 결정하는 알고리즘입니다. 페이지 폴트(Page Fault)가 발생하고 빈 프레임이 없을 때 사용됩니다.\r\n\r\n### 페이지 폴트 처리 과정\r\n```\r\n1. CPU가 페이지 참조 → 페이지 테이블 확인\r\n2. 해당 페이지가 메모리에 없음 → 페이지 폴트 발생\r\n3. 디스크에서 해당 페이지를 가져옴\r\n4. 빈 프레임이 없으면 → 페이지 교체 알고리즘으로 희생 페이지 선택\r\n5. 희생 페이지를 디스크에 쓰고 새 페이지를 적재\r\n6. 페이지 테이블 갱신\r\n7. 프로세스 재실행\r\n```\r\n\r\n### 알고리즘 종류\r\n\r\n**1. OPT (Optimal)**\r\n가장 오랫동안 사용되지 않을 페이지를 교체합니다.\r\n- 이론적으로 최적이지만 미래를 예측할 수 없어 구현 불가\r\n- 다른 알고리즘의 성능 비교 기준으로 사용\r\n\r\n```\r\n참조 문자열: 7 0 1 2 0 3 0 4 2 3 (프레임 수: 3)\r\nOPT: 페이지 폴트 6회\r\n```\r\n\r\n**2. FIFO (First In First Out)**\r\n가장 먼저 들어온 페이지를 교체합니다.\r\n- 구현이 간단\r\n- **벨라디의 모순**: 프레임 수를 늘려도 페이지 폴트가 증가할 수 있음\r\n\r\n```\r\n참조 문자열: 1 2 3 4 1 2 5 1 2 3 4 5\r\n프레임 3개: 9번 페이지 폴트\r\n프레임 4개: 10번 페이지 폴트 (오히려 증가!)\r\n```\r\n\r\n**3. LRU (Least Recently Used)**\r\n가장 오랫동안 사용되지 않은 페이지를 교체합니다.\r\n- 지역성 원리에 기반한 가장 널리 사용되는 알고리즘\r\n- 벨라디의 모순이 발생하지 않음\r\n- 구현 비용이 높음 (참조 시간을 기록해야 함)\r\n\r\n```\r\n참조 문자열: 7 0 1 2 0 3 0 4 2 3 (프레임 수: 3)\r\n\r\nStep 1: [7] ← 7 적재\r\nStep 2: [7,0] ← 0 적재\r\nStep 3: [7,0,1] ← 1 적재\r\nStep 4: [2,0,1] ← 7 교체 (가장 오래전에 사용)\r\nStep 5: [2,0,1] ← 0 히트\r\nStep 6: [2,0,3] ← 1 교체 (가장 오래전에 사용)\r\n```\r\n\r\n**4. LFU (Least Frequently Used)**\r\n참조 횟수가 가장 적은 페이지를 교체합니다.\r\n- 초기에 많이 사용되었지만 이후 사용되지 않는 페이지가 남는 문제\r\n- 참조 횟수가 같으면 LRU 방식으로 결정\r\n\r\n**5. Clock Algorithm (Second Chance)**\r\nFIFO의 개선 버전으로, 참조 비트를 확인합니다.\r\n- 원형 큐에서 참조 비트가 0인 페이지를 교체\r\n- 참조 비트가 1이면 0으로 변경하고 다음 페이지 확인\r\n- LRU의 근사 알고리즘으로 실제 OS에서 많이 사용\r\n\r\n### 스래싱 (Thrashing)\r\n프로세스의 실행 시간보다 페이지 교체에 더 많은 시간을 소비하는 현상입니다. 멀티프로그래밍 정도가 과도하면 발생합니다.\r\n\r\n**해결 방법:**\r\n- Working Set 모델: 프로세스가 일정 기간 동안 참조하는 페이지 집합을 메모리에 유지\r\n- Page Fault Frequency: 페이지 폴트율을 기반으로 프레임 수 조절\r\n\r\n### 면접 팁\r\nLRU가 가장 자주 출제됩니다. 구현 방법(HashMap + DoublyLinkedList)을 코드로 작성할 수 있어야 합니다. 벨라디의 모순이 FIFO에서만 발생하고 LRU에서는 발생하지 않는 이유도 설명할 수 있으면 좋습니다.", + "difficulty": "ADVANCED", + "tags": "페이지 교체, FIFO, LRU, LFU, 페이지 폴트, 벨라디의 모순", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 79, + "title": "메모리 관리", + "content": "운영체제의 메모리 관리 기법에 대해 설명해주세요.", + "answer": "메모리 관리는 운영체제의 핵심 기능 중 하나로, 한정된 물리 메모리를 효율적으로 사용하고 프로세스 간 메모리 보호를 제공하는 것을 목표로 합니다.\r\n\r\n### 메모리 할당 방식\r\n\r\n**1. 연속 메모리 할당**\r\n각 프로세스를 메모리의 연속된 공간에 배치합니다.\r\n\r\n- **고정 분할 방식**: 메모리를 고정 크기로 분할. 내부 단편화 발생\r\n- **가변 분할 방식**: 프로세스 크기에 맞게 분할. 외부 단편화 발생\r\n\r\n가변 분할의 배치 전략:\r\n- **First Fit**: 첫 번째로 충분한 공간을 찾아 배치\r\n- **Best Fit**: 가장 작은 충분한 공간에 배치\r\n- **Worst Fit**: 가장 큰 공간에 배치\r\n\r\n**2. 비연속 메모리 할당**\r\n프로세스를 여러 조각으로 나누어 비연속적으로 배치합니다.\r\n- 페이징 (Paging): 고정 크기 분할\r\n- 세그먼테이션 (Segmentation): 가변 크기 분할\r\n\r\n### 단편화 (Fragmentation)\r\n\r\n**내부 단편화 (Internal Fragmentation)**\r\n할당된 메모리 공간 중 사용되지 않는 부분입니다. 고정 크기 분할에서 발생합니다.\r\n```\r\n할당 블록: 4KB, 실제 사용: 3KB → 1KB 낭비\r\n```\r\n\r\n**외부 단편화 (External Fragmentation)**\r\n총 빈 공간은 충분하지만 연속적이지 않아 할당할 수 없는 상태입니다.\r\n```\r\n[사용중][빈3KB][사용중][빈2KB][사용중][빈4KB]\r\n→ 총 9KB 비었지만 7KB 프로세스 배치 불가\r\n```\r\n\r\n해결: 압축(Compaction) - 사용 중인 메모리를 한쪽으로 몰아 큰 빈 공간을 만듦\r\n\r\n### 가상 메모리 (Virtual Memory)\r\n\r\n물리 메모리보다 큰 주소 공간을 프로세스에 제공하는 기법입니다. 실제로 사용하는 부분만 물리 메모리에 적재하고, 나머지는 디스크(스왑 영역)에 저장합니다.\r\n\r\n**요구 페이징 (Demand Paging)**\r\n- 필요한 페이지만 메모리에 적재하는 방식\r\n- 처음에는 아무 페이지도 적재하지 않고, 접근 시 페이지 폴트가 발생하면 적재\r\n- 지역성 원리(Locality of Reference)에 의해 효율적으로 동작\r\n\r\n```\r\n지역성 원리:\r\n- 시간적 지역성: 최근 접근한 데이터에 다시 접근할 가능성이 높음\r\n- 공간적 지역성: 인접한 데이터에 접근할 가능성이 높음\r\n```\r\n\r\n### 스와핑 (Swapping)\r\n프로세스 전체를 디스크로 내보내고(swap-out), 필요할 때 다시 메모리로 불러오는(swap-in) 기법입니다. 메모리가 부족할 때 사용합니다.\r\n\r\n### 면접 팁\r\n가상 메모리가 필요한 이유(물리 메모리의 한계, 프로세스 보호)와 요구 페이징의 동작 원리를 잘 설명하세요. 페이지 폴트 발생 시의 처리 과정도 중요합니다.", + "difficulty": "INTERMEDIATE", + "tags": "메모리 관리, 가상 메모리, 메모리 할당, 단편화, 스와핑", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 80, + "title": "파일 시스템", + "content": "파일 시스템이란 무엇이며, 주요 구현 방식에 대해 설명해주세요.", + "answer": "파일 시스템(File System)은 운영체제가 디스크에 데이터를 저장하고 관리하는 체계입니다. 사용자가 파일 이름으로 데이터에 접근할 수 있게 하며, 파일의 생성, 삭제, 읽기, 쓰기를 관리합니다.\r\n\r\n### 파일 시스템 구성요소\r\n\r\n**파일(File)**: 관련된 데이터의 논리적 집합\r\n**디렉토리(Directory)**: 파일 정보를 포함하는 특수한 파일\r\n**메타데이터**: 파일 이름, 크기, 생성일, 권한, 위치 정보 등\r\n\r\n### 디스크 할당 방식\r\n\r\n**1. 연속 할당 (Contiguous Allocation)**\r\n파일을 연속된 블록에 저장합니다.\r\n- 장점: 순차/직접 접근 모두 빠름\r\n- 단점: 외부 단편화 발생, 파일 크기 변경이 어려움\r\n\r\n**2. 연결 할당 (Linked Allocation)**\r\n각 블록이 다음 블록을 가리키는 포인터를 포함합니다.\r\n- 장점: 외부 단편화 없음, 파일 크기 유연\r\n- 단점: 직접 접근 불가(순차 접근만), 포인터 손상 시 데이터 손실\r\n- FAT 파일 시스템이 이 방식을 개선하여 사용\r\n\r\n**3. 인덱스 할당 (Indexed Allocation)**\r\n인덱스 블록에 모든 데이터 블록의 포인터를 저장합니다.\r\n- 장점: 직접 접근 가능, 외부 단편화 없음\r\n- 단점: 인덱스 블록을 위한 추가 공간 필요\r\n- Unix/Linux의 inode 방식이 이를 사용\r\n\r\n### inode (Index Node) 구조\r\n\r\nUnix/Linux 파일 시스템에서 파일의 메타데이터를 저장하는 자료구조입니다.\r\n\r\n```\r\ninode:\r\n├── 파일 타입, 권한\r\n├── 소유자, 그룹\r\n├── 파일 크기\r\n├── 타임스탬프 (생성, 수정, 접근)\r\n├── 직접 블록 포인터 (12개) → 데이터 블록\r\n├── 단일 간접 포인터 (1개) → 포인터 블록 → 데이터 블록\r\n├── 이중 간접 포인터 (1개) → 포인터 블록 → 포인터 블록 → 데이터\r\n└── 삼중 간접 포인터 (1개) → ...\r\n```\r\n\r\n### 주요 파일 시스템\r\n\r\n| 파일 시스템 | OS | 특징 |\r\n|------------|-----|------|\r\n| FAT32 | Windows | 단순, 호환성 높음, 4GB 파일 제한 |\r\n| NTFS | Windows | 저널링, 대용량, 보안 기능 |\r\n| ext4 | Linux | 저널링, 대용량, inode 기반 |\r\n| APFS | macOS | 스냅샷, 암호화, SSD 최적화 |\r\n\r\n### 저널링 (Journaling)\r\n파일 시스템의 변경 사항을 로그(저널)에 먼저 기록한 후 실제 작업을 수행합니다. 시스템 장애 발생 시 저널을 이용하여 일관된 상태로 복구할 수 있습니다.\r\n\r\n```\r\n1. 변경 사항을 저널에 기록 (Write-Ahead Logging)\r\n2. 실제 데이터 영역에 반영\r\n3. 저널에서 해당 기록 삭제\r\n→ 중간에 장애가 발생해도 저널을 보고 복구 가능\r\n```\r\n\r\n### 면접 팁\r\n파일 시스템의 기본 구조와 inode의 개념을 이해하세요. 실무에서는 파일 시스템 선택보다는 디스크 I/O 최적화, 캐싱 전략이 중요합니다. 저널링이 데이터 안정성을 위해 어떤 역할을 하는지 설명할 수 있으면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "파일 시스템, FAT, inode, 디렉토리, 디스크 할당", + "categorySlug": "operating-system", + "categoryName": "운영체제", + "categoryId": 3, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 81, + "title": "클린코드 & 리팩토링", + "content": "클린 코드란 무엇이며, 리팩토링의 목적과 기법에 대해 설명해주세요.", + "answer": "클린 코드(Clean Code)는 다른 개발자가 쉽게 이해하고 유지보수할 수 있는 코드를 말합니다. 로버트 마틴(Uncle Bob)의 저서에서 정의한 개념으로, \"읽기 쉬운 코드\"가 핵심입니다.\r\n\r\n### 클린 코드 원칙\r\n\r\n**1. 의미 있는 이름**\r\n```java\r\n// Bad\r\nint d; // 경과 시간 (일)\r\n\r\n// Good\r\nint elapsedTimeInDays;\r\n```\r\n\r\n**2. 함수는 하나의 일만**\r\n```java\r\n// Bad: 여러 일을 하는 함수\r\nvoid processOrder(Order order) {\r\n validateOrder(order);\r\n calculatePrice(order);\r\n sendEmail(order);\r\n saveToDatabase(order);\r\n}\r\n\r\n// Good: 단일 책임\r\nvoid validateOrder(Order order) { ... }\r\nvoid calculatePrice(Order order) { ... }\r\n```\r\n\r\n**3. 주석보다 코드로 표현**\r\n```java\r\n// Bad\r\n// 직원에게 복지 혜택을 받을 자격이 있는지 검사\r\nif ((employee.flags & HOURLY_FLAG) && (employee.age > 65))\r\n\r\n// Good\r\nif (employee.isEligibleForBenefits())\r\n```\r\n\r\n**4. 오류 처리**\r\n- null 반환보다 예외를 사용\r\n- checked exception보다 unchecked exception 선호\r\n- Try-Catch 블록을 별도 함수로 분리\r\n\r\n### 리팩토링 (Refactoring)\r\n\r\n리팩토링은 외부 동작을 변경하지 않으면서 코드의 내부 구조를 개선하는 것입니다.\r\n\r\n**코드 스멜 (리팩토링이 필요한 신호):**\r\n- 중복 코드 (Duplicated Code)\r\n- 긴 메서드 (Long Method)\r\n- 거대한 클래스 (Large Class)\r\n- 긴 매개변수 목록 (Long Parameter List)\r\n- 주석이 많은 코드 (코드가 불명확하다는 신호)\r\n\r\n**주요 리팩토링 기법:**\r\n- **메서드 추출 (Extract Method)**: 긴 메서드를 작은 메서드로 분리\r\n- **변수 인라인 (Inline Variable)**: 불필요한 임시 변수 제거\r\n- **조건부 로직 다형성으로 변환**: if-else를 다형성으로 대체\r\n- **매개변수 객체 도입**: 관련 매개변수를 하나의 객체로 묶기\r\n\r\n```java\r\n// Before: 매개변수가 많음\r\nvoid createUser(String name, String email, String phone, String address) { ... }\r\n\r\n// After: 매개변수 객체 도입\r\nvoid createUser(UserCreateRequest request) { ... }\r\n```\r\n\r\n### 면접 팁\r\n클린 코드는 \"주관적인 좋은 코드\"가 아니라 구체적인 원칙이 있습니다. 실무에서 리팩토링한 경험을 예시로 들면 좋습니다. 테스트 코드 없이 리팩토링하면 위험하다는 점도 언급하세요.", + "difficulty": "BASIC", + "tags": "클린코드, 리팩토링, 가독성, 코드 품질, 네이밍", + "categorySlug": "software-engineering", + "categoryName": "소프트웨어공학", + "categoryId": 6, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 82, + "title": "TDD (Test-Driven Development)", + "content": "TDD(테스트 주도 개발)란 무엇이며, 장단점에 대해 설명해주세요.", + "answer": "TDD(Test-Driven Development)는 테스트 코드를 먼저 작성한 후 그 테스트를 통과하는 구현 코드를 작성하는 개발 방법론입니다.\r\n\r\n### Red-Green-Refactor 사이클\r\n\r\n```\r\n1. Red : 실패하는 테스트 작성\r\n2. Green : 테스트를 통과하는 최소한의 코드 작성\r\n3. Refactor : 코드를 깔끔하게 리팩토링 (테스트는 계속 통과)\r\n→ 반복\r\n```\r\n\r\n### TDD 예시: 계산기 구현\r\n\r\n```java\r\n// Step 1: Red - 실패하는 테스트 작성\r\n@Test\r\nvoid 두_수를_더한다() {\r\n Calculator calc = new Calculator();\r\n assertEquals(5, calc.add(2, 3));\r\n}\r\n// → 컴파일 에러 (Calculator 클래스 없음)\r\n\r\n// Step 2: Green - 최소한의 구현\r\npublic class Calculator {\r\n public int add(int a, int b) {\r\n return a + b;\r\n }\r\n}\r\n// → 테스트 통과\r\n\r\n// Step 3: Refactor - 필요한 경우 리팩토링\r\n// (이 경우 리팩토링할 부분 없음)\r\n\r\n// Step 4: 다음 테스트\r\n@Test\r\nvoid 음수를_더한다() {\r\n Calculator calc = new Calculator();\r\n assertEquals(-1, calc.add(2, -3));\r\n}\r\n```\r\n\r\n### TDD의 장점\r\n\r\n1. **높은 코드 품질**: 테스트가 코드의 안전망 역할\r\n2. **설계 개선**: 테스트하기 쉬운 코드 = 잘 설계된 코드 (느슨한 결합, 높은 응집)\r\n3. **리팩토링 안정성**: 테스트가 있으므로 안심하고 리팩토링 가능\r\n4. **문서화 효과**: 테스트 코드가 사용 방법의 문서 역할\r\n5. **디버깅 시간 감소**: 버그를 조기에 발견\r\n\r\n### TDD의 단점\r\n\r\n1. **초기 개발 속도 저하**: 테스트 작성에 추가 시간 소요\r\n2. **학습 곡선**: 테스트 작성 기술과 마인드셋 전환 필요\r\n3. **설계 변경 시 테스트도 수정**: 변경 비용 증가\r\n4. **모든 상황에 적합하지 않음**: UI, 외부 시스템 연동 등\r\n\r\n### 테스트 작성 원칙: F.I.R.S.T\r\n\r\n- **Fast**: 빠르게 실행되어야 함\r\n- **Independent**: 테스트 간 독립적이어야 함\r\n- **Repeatable**: 어떤 환경에서도 동일한 결과\r\n- **Self-validating**: 자동으로 성공/실패 판단\r\n- **Timely**: 적시에 작성 (구현 전에)\r\n\r\n### 면접 팁\r\nTDD의 핵심은 \"테스트를 먼저 작성하는 것\"이 아니라 \"짧은 사이클로 피드백을 받으며 점진적으로 개발하는 것\"입니다. 실무에서 100% TDD가 어려운 이유와 함께, 어떤 부분에 적용하면 효과적인지 설명하세요.", + "difficulty": "INTERMEDIATE", + "tags": "TDD, 테스트 주도 개발, Red-Green-Refactor, 단위 테스트, JUnit", + "categorySlug": "software-engineering", + "categoryName": "소프트웨어공학", + "categoryId": 6, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 83, + "title": "애자일(Agile)", + "content": "애자일(Agile) 방법론이란 무엇이며, 전통적인 워터폴 방법론과의 차이점을 설명해주세요.", + "answer": "애자일(Agile)은 소프트웨어 개발에서 변화에 빠르게 대응하고, 고객과의 지속적인 소통을 통해 점진적으로 소프트웨어를 개발하는 방법론입니다.\r\n\r\n### 애자일 선언문 (4가지 가치)\r\n\r\n1. 프로세스와 도구보다 **개인과 상호작용**을\r\n2. 포괄적인 문서보다 **작동하는 소프트웨어**를\r\n3. 계약 협상보다 **고객과의 협력**을\r\n4. 계획을 따르기보다 **변화에 대응**하기를\r\n\r\n### 워터폴 vs 애자일\r\n\r\n| 구분 | 워터폴 | 애자일 |\r\n|------|--------|--------|\r\n| 개발 방식 | 순차적 | 반복적, 점진적 |\r\n| 요구사항 | 초기 확정 | 지속적 변경 가능 |\r\n| 고객 참여 | 초기/최종 | 전 과정 |\r\n| 산출물 | 각 단계별 문서 | 작동하는 소프트웨어 |\r\n| 변경 대응 | 어려움 | 유연 |\r\n| 릴리즈 | 전체 완료 후 | 주기적 (2~4주) |\r\n\r\n### 스크럼 (Scrum)\r\n\r\n가장 널리 사용되는 애자일 프레임워크입니다.\r\n\r\n**역할:**\r\n- **Product Owner**: 제품 백로그 관리, 우선순위 결정\r\n- **Scrum Master**: 프로세스 지원, 장애물 제거\r\n- **Development Team**: 개발 수행 (5~9명)\r\n\r\n**이벤트:**\r\n- **스프린트 (Sprint)**: 1~4주의 개발 주기\r\n- **스프린트 플래닝**: 스프린트 목표와 작업 계획\r\n- **데일리 스크럼**: 매일 15분 회의 (어제/오늘/장애물)\r\n- **스프린트 리뷰**: 결과물 시연\r\n- **스프린트 회고**: 프로세스 개선점 논의\r\n\r\n**산출물:**\r\n- 제품 백로그 (Product Backlog)\r\n- 스프린트 백로그 (Sprint Backlog)\r\n- 인크리먼트 (Increment)\r\n\r\n### 칸반 (Kanban)\r\n\r\n시각적 보드를 통해 작업 흐름을 관리합니다.\r\n```\r\n| To Do | In Progress | Review | Done |\r\n|-------|-------------|--------|------|\r\n| 기능A | 기능B | 기능C | 기능D |\r\n| 기능E | | | |\r\n\r\nWIP(Work In Progress) 제한으로 과부하 방지\r\n```\r\n\r\n### 면접 팁\r\n실무에서 애자일을 적용한 경험을 구체적으로 설명할 수 있으면 좋습니다. 애자일은 \"프레임워크\"가 아니라 \"마인드셋\"이라는 점을 강조하세요. 또한 애자일의 한계(대규모 프로젝트, 고정 요구사항)도 함께 언급하면 균형 잡힌 답변이 됩니다.", + "difficulty": "BASIC", + "tags": "애자일, 스크럼, 칸반, 스프린트, 워터폴", + "categorySlug": "software-engineering", + "categoryName": "소프트웨어공학", + "categoryId": 6, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 84, + "title": "객체 지향 프로그래밍(OOP)", + "content": "객체 지향 프로그래밍의 4가지 특징에 대해 설명해주세요.", + "answer": "객체 지향 프로그래밍(OOP, Object-Oriented Programming)은 프로그램을 객체(Object)들의 모임으로 바라보는 프로그래밍 패러다임입니다. 각 객체는 데이터(속성)와 기능(메서드)을 가지며, 객체 간 상호작용으로 프로그램이 동작합니다.\r\n\r\n### 4대 특성\r\n\r\n**1. 캡슐화 (Encapsulation)**\r\n데이터와 메서드를 하나의 단위로 묶고, 외부에서 내부 구현을 숨기는 것입니다.\r\n\r\n```java\r\npublic class Account {\r\n private int balance; // 외부 접근 차단\r\n\r\n public void deposit(int amount) {\r\n if (amount > 0) {\r\n this.balance += amount;\r\n }\r\n }\r\n\r\n public int getBalance() {\r\n return this.balance;\r\n }\r\n}\r\n```\r\n\r\n- 정보 은닉으로 데이터를 보호\r\n- 접근 제어자(public, private, protected)로 제어\r\n- 변경의 영향 범위를 최소화\r\n\r\n**2. 상속 (Inheritance)**\r\n기존 클래스의 속성과 메서드를 새로운 클래스가 물려받는 것입니다.\r\n\r\n```java\r\npublic class Animal {\r\n protected String name;\r\n public void eat() { System.out.println(\"먹는다\"); }\r\n}\r\n\r\npublic class Dog extends Animal {\r\n public void bark() { System.out.println(\"멍멍\"); }\r\n}\r\n```\r\n\r\n- 코드 재사용성 향상\r\n- 계층적 관계 표현\r\n- 주의: 과도한 상속은 결합도를 높임 → \"상속보다 조합(Composition)을 선호하라\"\r\n\r\n**3. 다형성 (Polymorphism)**\r\n같은 인터페이스로 다른 동작을 수행하는 것입니다.\r\n\r\n```java\r\n// 오버라이딩 (런타임 다형성)\r\nAnimal dog = new Dog();\r\nAnimal cat = new Cat();\r\ndog.speak(); // \"멍멍\"\r\ncat.speak(); // \"야옹\"\r\n\r\n// 오버로딩 (컴파일타임 다형성)\r\nint add(int a, int b) { return a + b; }\r\ndouble add(double a, double b) { return a + b; }\r\n```\r\n\r\n- 유연하고 확장 가능한 코드 작성\r\n- 인터페이스 기반 프로그래밍의 핵심\r\n\r\n**4. 추상화 (Abstraction)**\r\n복잡한 시스템에서 핵심적인 부분만 모델링하는 것입니다.\r\n\r\n```java\r\npublic interface Vehicle {\r\n void start();\r\n void stop();\r\n void accelerate(int speed);\r\n}\r\n// 내부 구현은 숨기고 필요한 기능만 정의\r\n```\r\n\r\n- 불필요한 세부사항을 숨김\r\n- 인터페이스와 추상 클래스로 구현\r\n\r\n### OOP의 장점\r\n- 코드 재사용성 향상\r\n- 유지보수 용이\r\n- 대규모 소프트웨어 개발에 적합\r\n- 현실 세계의 모델링이 자연스러움\r\n\r\n### 면접 팁\r\n4가지 특성을 단순 암기가 아닌 코드 예시와 함께 설명하세요. \"상속보다 조합을 선호하라\"는 원칙과 SOLID 원칙까지 연결하면 깊이 있는 답변이 됩니다.", + "difficulty": "BASIC", + "tags": "OOP, 캡슐화, 상속, 다형성, 추상화", + "categorySlug": "software-engineering", + "categoryName": "소프트웨어공학", + "categoryId": 6, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 85, + "title": "함수형 프로그래밍", + "content": "함수형 프로그래밍이란 무엇이며, 객체 지향 프로그래밍과의 차이점을 설명해주세요.", + "answer": "함수형 프로그래밍(Functional Programming)은 순수 함수를 조합하여 프로그램을 구성하는 프로그래밍 패러다임입니다. 상태 변경과 부수 효과를 최소화하여 코드의 예측 가능성을 높입니다.\r\n\r\n### 핵심 개념\r\n\r\n**1. 순수 함수 (Pure Function)**\r\n같은 입력에 항상 같은 출력을 반환하며, 부수 효과가 없는 함수입니다.\r\n\r\n```java\r\n// 순수 함수\r\nint add(int a, int b) {\r\n return a + b; // 항상 같은 결과, 외부 상태 변경 없음\r\n}\r\n\r\n// 비순수 함수\r\nint count = 0;\r\nint addAndCount(int a, int b) {\r\n count++; // 외부 상태 변경 (부수 효과)\r\n return a + b;\r\n}\r\n```\r\n\r\n**2. 불변성 (Immutability)**\r\n데이터를 변경하지 않고, 새로운 데이터를 생성합니다.\r\n\r\n```java\r\n// 가변 (OOP 스타일)\r\nList list = new ArrayList<>();\r\nlist.add(1); // 기존 리스트 수정\r\n\r\n// 불변 (FP 스타일)\r\nList newList = List.of(1, 2, 3);\r\nList result = Stream.concat(newList.stream(), Stream.of(4))\r\n .collect(Collectors.toList()); // 새 리스트 생성\r\n```\r\n\r\n**3. 고차 함수 (Higher-order Function)**\r\n함수를 인자로 받거나 함수를 반환하는 함수입니다.\r\n\r\n```java\r\n// 함수를 인자로 받음\r\nList numbers = Arrays.asList(1, 2, 3, 4, 5);\r\nList evens = numbers.stream()\r\n .filter(n -> n % 2 == 0) // 함수(람다)를 인자로 전달\r\n .collect(Collectors.toList());\r\n\r\n// 함수를 반환\r\nFunction> adder = a -> b -> a + b;\r\nFunction add5 = adder.apply(5);\r\nadd5.apply(3); // 8\r\n```\r\n\r\n**4. 일급 시민 (First-class Citizen)**\r\n함수를 변수에 할당하고, 인자로 전달하고, 반환값으로 사용할 수 있습니다.\r\n\r\n```java\r\nFunction square = x -> x * x;\r\nFunction doubleIt = x -> x * 2;\r\n\r\n// 함수 합성\r\nFunction squareThenDouble = square.andThen(doubleIt);\r\nsquareThenDouble.apply(3); // 18 (3^2 = 9, 9*2 = 18)\r\n```\r\n\r\n### OOP vs FP\r\n\r\n| 구분 | OOP | FP |\r\n|------|-----|-----|\r\n| 중심 | 객체 | 함수 |\r\n| 상태 | 가변 상태 | 불변 상태 |\r\n| 부수 효과 | 허용 | 최소화 |\r\n| 추상화 | 데이터 추상화 | 행위 추상화 |\r\n| 동시성 | 동기화 필요 | 자연스러운 동시성 |\r\n\r\n### Java에서의 함수형 프로그래밍\r\n\r\n```java\r\n// Stream API + Lambda\r\nList names = users.stream()\r\n .filter(u -> u.getAge() > 20)\r\n .map(User::getName)\r\n .sorted()\r\n .collect(Collectors.toList());\r\n\r\n// Optional: null 안전한 체이닝\r\nOptional.ofNullable(user)\r\n .map(User::getAddress)\r\n .map(Address::getCity)\r\n .orElse(\"Unknown\");\r\n```\r\n\r\n### 면접 팁\r\n현대 프로그래밍에서는 OOP와 FP를 함께 사용하는 멀티 패러다임이 일반적입니다. Java 8+ Stream API, Optional, Lambda 등을 활용한 경험을 구체적으로 설명하세요.", + "difficulty": "INTERMEDIATE", + "tags": "함수형 프로그래밍, 순수 함수, 불변성, 고차 함수, 람다", + "categorySlug": "software-engineering", + "categoryName": "소프트웨어공학", + "categoryId": 6, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 86, + "title": "데브옵스(DevOps)", + "content": "DevOps란 무엇이며, 왜 중요한가요?", + "answer": "DevOps는 소프트웨어 개발(Development)과 IT 운영(Operations)을 통합하여, 소프트웨어의 개발, 배포, 운영 전 과정을 자동화하고 효율화하는 문화, 방법론, 도구의 집합입니다.\r\n\r\n### DevOps의 핵심 원칙\r\n\r\n**1. 지속적 통합 (CI - Continuous Integration)**\r\n개발자가 코드를 자주 메인 브랜치에 병합하고, 자동으로 빌드와 테스트를 수행합니다.\r\n\r\n```yaml\r\n# GitHub Actions CI 예시\r\nname: CI\r\non: push\r\njobs:\r\n build:\r\n runs-on: ubuntu-latest\r\n steps:\r\n - uses: actions/checkout@v3\r\n - name: Build\r\n run: ./gradlew build\r\n - name: Test\r\n run: ./gradlew test\r\n```\r\n\r\n**2. 지속적 배포 (CD - Continuous Delivery/Deployment)**\r\n- Continuous Delivery: 배포 가능한 상태를 항상 유지\r\n- Continuous Deployment: 자동으로 프로덕션 배포\r\n\r\n**3. 인프라 as 코드 (IaC - Infrastructure as Code)**\r\n인프라를 코드로 관리하여 버전 관리, 재현성, 자동화를 달성합니다.\r\n```\r\nTerraform, Ansible, CloudFormation 등\r\n```\r\n\r\n**4. 마이크로서비스 아키텍처**\r\n작은 독립적인 서비스로 분리하여 독립적인 배포와 확장을 가능하게 합니다.\r\n\r\n**5. 모니터링 & 로깅**\r\n시스템의 상태를 실시간으로 모니터링하고 문제를 빠르게 감지합니다.\r\n```\r\nPrometheus, Grafana, ELK Stack 등\r\n```\r\n\r\n### DevOps 파이프라인\r\n\r\n```\r\nPlan → Code → Build → Test → Release → Deploy → Operate → Monitor\r\n ↑ ↓\r\n └────────────────── 피드백 루프 ─────────────────────────────┘\r\n```\r\n\r\n### DevOps 도구 체인\r\n\r\n| 단계 | 도구 |\r\n|------|------|\r\n| 소스 관리 | Git, GitHub, GitLab |\r\n| CI/CD | Jenkins, GitHub Actions, GitLab CI |\r\n| 컨테이너 | Docker, Kubernetes |\r\n| IaC | Terraform, Ansible |\r\n| 모니터링 | Prometheus, Grafana, Datadog |\r\n| 로깅 | ELK Stack (Elasticsearch, Logstash, Kibana) |\r\n\r\n### DevOps의 장점\r\n- 배포 빈도 증가, 배포 시간 단축\r\n- 장애 복구 시간 단축 (MTTR)\r\n- 변경 실패율 감소\r\n- 개발과 운영 간의 협업 향상\r\n\r\n### 면접 팁\r\nDevOps는 단순히 도구가 아니라 \"문화\"라는 점을 강조하세요. 실무에서 CI/CD 파이프라인을 구축한 경험이나, Docker/Kubernetes를 사용한 경험을 구체적으로 설명하면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "DevOps, CI/CD, 자동화, 인프라, 문화", + "categorySlug": "software-engineering", + "categoryName": "소프트웨어공학", + "categoryId": 6, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 87, + "title": "서드 파티(Third Party)", + "content": "서드 파티(Third Party)란 무엇이며, 사용 시 고려사항은 무엇인가요?", + "answer": "서드 파티(Third Party)란 프로그래밍에서 제3자, 즉 개발자나 플랫폼 제공자가 아닌 외부 업체나 개인이 제공하는 소프트웨어 구성요소를 말합니다.\r\n\r\n### 서드 파티의 종류\r\n\r\n**1. 서드 파티 라이브러리**\r\n특정 기능을 제공하는 코드 모음입니다.\r\n```gradle\r\n// Java/Gradle 예시\r\ndependencies {\r\n implementation 'com.google.guava:guava:31.1-jre'\r\n implementation 'org.apache.commons:commons-lang3:3.12.0'\r\n implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0'\r\n}\r\n```\r\n\r\n**2. 서드 파티 API**\r\n외부 서비스의 기능을 호출할 수 있는 인터페이스입니다.\r\n- 결제 API (Stripe, Toss Payments)\r\n- 지도 API (Google Maps, Kakao Maps)\r\n- 소셜 로그인 (Google, Kakao, Naver)\r\n\r\n**3. 서드 파티 SDK**\r\n특정 플랫폼이나 서비스와 연동하기 위한 개발 도구 모음입니다.\r\n- Firebase SDK, AWS SDK\r\n\r\n### 서드 파티 사용의 장점\r\n- **개발 시간 단축**: 이미 검증된 기능을 즉시 사용\r\n- **높은 품질**: 전문 팀이 유지보수하는 코드\r\n- **커뮤니티 지원**: 문서, 예제, 질의응답\r\n- **비용 절감**: 직접 개발하는 것보다 경제적\r\n\r\n### 서드 파티 사용 시 고려사항\r\n\r\n**1. 라이선스 확인**\r\n- MIT, Apache 2.0: 상업적 사용 가능\r\n- GPL: 소스 코드 공개 의무 (주의!)\r\n- 라이선스 위반 시 법적 문제 발생 가능\r\n\r\n**2. 보안 취약점**\r\n```\r\nLog4j 취약점(2021년)처럼 서드 파티의 보안 문제가\r\n사용하는 모든 시스템에 영향을 줄 수 있음\r\n→ 정기적인 의존성 업데이트와 취약점 스캔 필요\r\n```\r\n\r\n**3. 벤더 종속성 (Vendor Lock-in)**\r\n특정 서드 파티에 과도하게 의존하면 변경이 어려워집니다.\r\n→ 추상화 계층을 두어 교체 가능하게 설계\r\n\r\n```java\r\n// Bad: 직접 의존\r\nPayPalClient client = new PayPalClient();\r\nclient.pay(amount);\r\n\r\n// Good: 추상화를 통한 의존\r\ninterface PaymentGateway { void pay(int amount); }\r\nclass PayPalGateway implements PaymentGateway { ... }\r\nclass StripeGateway implements PaymentGateway { ... }\r\n```\r\n\r\n**4. 유지보수 상태**\r\n- 마지막 업데이트 시점\r\n- 이슈 처리 속도\r\n- 커뮤니티 활성도\r\n- 메이저 버전 업데이트 시 호환성\r\n\r\n### 의존성 관리\r\n```gradle\r\n// 버전 관리\r\nimplementation 'org.springframework.boot:spring-boot-starter:3.0.0'\r\n\r\n// 의존성 충돌 확인\r\n./gradlew dependencies\r\n```\r\n\r\n### 면접 팁\r\n서드 파티를 \"무조건 사용해야 한다\" 또는 \"사용하면 안 된다\"가 아니라, 트레이드오프를 고려하여 결정한다는 관점이 중요합니다. 보안, 라이선스, 유지보수 측면을 고려한 의사결정 과정을 설명하세요.", + "difficulty": "BASIC", + "tags": "서드 파티, 라이브러리, API, SDK, 의존성", + "categorySlug": "software-engineering", + "categoryName": "소프트웨어공학", + "categoryId": 6, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 88, + "title": "마이크로서비스 아키텍처(MSA)", + "content": "마이크로서비스 아키텍처(MSA)란 무엇이며, 모놀리식 아키텍처와의 차이점은 무엇인가요?", + "answer": "마이크로서비스 아키텍처(MSA)는 하나의 애플리케이션을 여러 개의 작고 독립적인 서비스로 분리하여 개발하고 배포하는 아키텍처 스타일입니다.\r\n\r\n### 모놀리식 vs 마이크로서비스\r\n\r\n```\r\n모놀리식: 마이크로서비스:\r\n┌─────────────┐ ┌────┐ ┌────┐ ┌────┐\r\n│ 사용자 관리 │ │사용자│ │주문 │ │결제 │\r\n│ 주문 관리 │ │서비스│ │서비스│ │서비스│\r\n│ 결제 관리 │ └──┬─┘ └──┬─┘ └──┬─┘\r\n│ 알림 관리 │ │ │ │\r\n│ 단일 DB │ ┌──┴─┐ ┌──┴─┐ ┌──┴─┐\r\n└─────────────┘ │DB │ │DB │ │DB │\r\n └────┘ └────┘ └────┘\r\n```\r\n\r\n| 구분 | 모놀리식 | MSA |\r\n|------|---------|-----|\r\n| 배포 | 전체 배포 | 서비스별 독립 배포 |\r\n| 확장 | 전체 확장 | 서비스별 독립 확장 |\r\n| 기술 스택 | 단일 | 서비스별 선택 가능 |\r\n| 데이터베이스 | 공유 DB | 서비스별 독립 DB |\r\n| 장애 영향 | 전체 장애 | 해당 서비스만 |\r\n| 복잡도 | 낮음 | 높음 (분산 시스템) |\r\n\r\n### MSA 핵심 구성요소\r\n\r\n**1. API Gateway**\r\n클라이언트 요청을 적절한 서비스로 라우팅합니다.\r\n- 인증/인가\r\n- 로드 밸런싱\r\n- 요청/응답 변환\r\n- 속도 제한 (Rate Limiting)\r\n\r\n**2. 서비스 간 통신**\r\n- **동기 통신**: REST API, gRPC\r\n- **비동기 통신**: 메시지 큐 (Kafka, RabbitMQ)\r\n\r\n```java\r\n// REST 동기 호출\r\n@FeignClient(name = \"order-service\")\r\npublic interface OrderClient {\r\n @GetMapping(\"/orders/{userId}\")\r\n List getOrders(@PathVariable Long userId);\r\n}\r\n\r\n// Kafka 비동기 메시지\r\n@KafkaListener(topics = \"order-created\")\r\npublic void handleOrderCreated(OrderEvent event) {\r\n paymentService.processPayment(event);\r\n}\r\n```\r\n\r\n**3. 서비스 디스커버리**\r\n서비스의 위치(IP, 포트)를 동적으로 관리합니다. (Eureka, Consul)\r\n\r\n**4. 서킷 브레이커**\r\n장애가 발생한 서비스로의 호출을 차단하여 장애 전파를 방지합니다. (Resilience4j)\r\n\r\n### MSA의 장점\r\n- 서비스별 독립적 개발, 배포, 확장\r\n- 기술 스택의 자유도\r\n- 장애 격리\r\n- 팀별 자율적 개발\r\n\r\n### MSA의 단점\r\n- 분산 시스템의 복잡성 (네트워크 지연, 장애)\r\n- 분산 트랜잭션 처리의 어려움\r\n- 서비스 간 통신 오버헤드\r\n- 운영 복잡도 증가 (모니터링, 로깅)\r\n- 데이터 일관성 유지 어려움\r\n\r\n### 면접 팁\r\nMSA를 무조건 좋다고 하지 말고, 트레이드오프를 명확히 설명하세요. 작은 팀이나 초기 프로젝트에서는 모놀리식이 더 적합할 수 있습니다. \"모놀리식 먼저, 필요에 따라 MSA로 전환\"이 일반적인 접근법입니다.", + "difficulty": "INTERMEDIATE", + "tags": "MSA, 마이크로서비스, 모놀리식, API Gateway, 서비스 디스커버리", + "categorySlug": "software-engineering", + "categoryName": "소프트웨어공학", + "categoryId": 6, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 89, + "title": "Bean Scope", + "content": "Spring Bean의 스코프(Scope)에 대해 설명해주세요.", + "answer": "Spring Bean의 스코프는 Bean이 존재할 수 있는 범위(생성되고 소멸되는 범위)를 의미합니다. Spring은 기본적으로 싱글톤 스코프를 사용합니다.\r\n\r\n### Bean 스코프 종류\r\n\r\n**1. Singleton (기본값)**\r\nSpring 컨테이너에서 단 하나의 인스턴스만 생성됩니다. 모든 요청에 같은 객체를 반환합니다.\r\n\r\n```java\r\n@Component\r\n// @Scope(\"singleton\") - 기본값이므로 생략 가능\r\npublic class SingletonBean {\r\n // 컨테이너 전체에서 하나의 인스턴스\r\n}\r\n```\r\n\r\n**2. Prototype**\r\n요청할 때마다 새로운 인스턴스를 생성합니다. Spring 컨테이너는 생성과 의존성 주입까지만 관리하고, 이후의 생명주기는 관리하지 않습니다.\r\n\r\n```java\r\n@Component\r\n@Scope(\"prototype\")\r\npublic class PrototypeBean {\r\n // 요청할 때마다 새 인스턴스\r\n}\r\n```\r\n\r\n**3. Web 스코프 (웹 환경에서만)**\r\n\r\n```java\r\n@Component\r\n@Scope(value = \"request\", proxyMode = ScopedProxyMode.TARGET_CLASS)\r\npublic class RequestBean {\r\n // HTTP 요청마다 새 인스턴스\r\n}\r\n\r\n@Scope(\"session\") // HTTP 세션마다 하나\r\n@Scope(\"application\") // 서블릿 컨텍스트마다 하나\r\n```\r\n\r\n### Singleton에서 Prototype 주입 문제\r\n\r\n싱글톤 Bean에 프로토타입 Bean을 주입하면, 프로토타입이 의도대로 동작하지 않습니다.\r\n\r\n```java\r\n@Component\r\npublic class SingletonBean {\r\n @Autowired\r\n private PrototypeBean prototypeBean; // 항상 같은 인스턴스!\r\n\r\n // 해결 방법 1: ObjectProvider\r\n @Autowired\r\n private ObjectProvider prototypeBeanProvider;\r\n\r\n public void logic() {\r\n PrototypeBean bean = prototypeBeanProvider.getObject(); // 매번 새 인스턴스\r\n }\r\n\r\n // 해결 방법 2: @Lookup\r\n @Lookup\r\n public PrototypeBean getPrototypeBean() { return null; }\r\n}\r\n```\r\n\r\n### Bean 라이프사이클\r\n\r\n```\r\n1. 스프링 컨테이너 생성\r\n2. 스프링 빈 생성 (생성자 호출)\r\n3. 의존성 주입 (@Autowired)\r\n4. 초기화 콜백 (@PostConstruct)\r\n5. 사용\r\n6. 소멸 콜백 (@PreDestroy)\r\n7. 스프링 컨테이너 종료\r\n```\r\n\r\n```java\r\n@Component\r\npublic class MyBean {\r\n @PostConstruct\r\n public void init() {\r\n // 초기화 로직 (DB 연결, 파일 로드 등)\r\n }\r\n\r\n @PreDestroy\r\n public void destroy() {\r\n // 정리 로직 (연결 해제, 리소스 반환 등)\r\n }\r\n}\r\n```\r\n\r\n### 면접 팁\r\n대부분의 Bean은 싱글톤으로 사용합니다. 싱글톤 Bean에서는 상태를 가지면 안 되며(Stateless), 공유 변수를 사용하면 동시성 문제가 발생합니다. 프로토타입 스코프는 잘 사용되지 않지만, 싱글톤과의 혼용 시 발생하는 문제는 자주 출제됩니다.", + "difficulty": "BASIC", + "tags": "Spring Bean, 스코프, 싱글톤, 프로토타입, 라이프사이클", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 90, + "title": "MVC Framework", + "content": "Spring MVC의 동작 원리에 대해 설명해주세요.", + "answer": "Spring MVC는 Model-View-Controller 패턴을 기반으로 웹 애플리케이션을 개발하기 위한 프레임워크입니다. DispatcherServlet이 모든 요청의 진입점 역할을 합니다.\r\n\r\n### MVC 패턴\r\n\r\n- **Model**: 데이터와 비즈니스 로직 (Service, Repository)\r\n- **View**: 사용자에게 보여지는 화면 (JSP, Thymeleaf, JSON)\r\n- **Controller**: 요청을 처리하고 Model과 View를 연결\r\n\r\n### Spring MVC 동작 흐름\r\n\r\n```\r\n1. 클라이언트 요청 (HTTP Request)\r\n ↓\r\n2. DispatcherServlet (Front Controller)\r\n ↓\r\n3. HandlerMapping → 요청에 맞는 Controller 탐색\r\n ↓\r\n4. HandlerAdapter → Controller 메서드 실행\r\n ↓\r\n5. Controller → 비즈니스 로직 처리, ModelAndView 반환\r\n ↓\r\n6. ViewResolver → 적절한 View 탐색\r\n ↓\r\n7. View → 렌더링 후 응답 (HTTP Response)\r\n```\r\n\r\n### 주요 구성요소\r\n\r\n**DispatcherServlet**: 모든 HTTP 요청의 진입점. Front Controller 패턴.\r\n\r\n**HandlerMapping**: URL과 Controller 메서드를 매핑합니다.\r\n\r\n**HandlerAdapter**: 다양한 형태의 Controller를 실행합니다.\r\n\r\n**ViewResolver**: 뷰 이름을 실제 뷰 객체로 변환합니다.\r\n\r\n### Controller 구현\r\n\r\n```java\r\n// View를 반환하는 Controller\r\n@Controller\r\npublic class UserController {\r\n @GetMapping(\"/users\")\r\n public String list(Model model) {\r\n model.addAttribute(\"users\", userService.findAll());\r\n return \"user/list\"; // View 이름 반환\r\n }\r\n}\r\n\r\n// REST API Controller (JSON 반환)\r\n@RestController\r\n@RequestMapping(\"/api/users\")\r\npublic class UserApiController {\r\n @GetMapping\r\n public List getUsers() {\r\n return userService.findAll(); // JSON 자동 변환\r\n }\r\n\r\n @PostMapping\r\n public ResponseEntity createUser(@RequestBody @Valid UserCreateRequest request) {\r\n UserDto user = userService.create(request);\r\n return ResponseEntity.status(HttpStatus.CREATED).body(user);\r\n }\r\n\r\n @GetMapping(\"/{id}\")\r\n public UserDto getUser(@PathVariable Long id) {\r\n return userService.findById(id);\r\n }\r\n}\r\n```\r\n\r\n### @Controller vs @RestController\r\n\r\n| 구분 | @Controller | @RestController |\r\n|------|------------|-----------------|\r\n| 반환 | View 이름 | 데이터(JSON/XML) |\r\n| @ResponseBody | 필요 | 자동 포함 |\r\n| 용도 | SSR (서버 사이드 렌더링) | REST API |\r\n\r\n### 면접 팁\r\nDispatcherServlet의 역할과 요청 처리 흐름을 순서대로 설명할 수 있어야 합니다. @Controller와 @RestController의 차이, @RequestBody와 @ResponseBody의 역할도 중요합니다.", + "difficulty": "BASIC", + "tags": "MVC, Spring MVC, DispatcherServlet, 컨트롤러, 뷰 리졸버", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 91, + "title": "SpringApplication", + "content": "Spring Boot란 무엇이며, Spring Framework와의 차이점을 설명해주세요.", + "answer": "Spring Boot는 Spring Framework를 기반으로 하여, 복잡한 설정 없이 빠르게 Spring 애플리케이션을 개발할 수 있게 해주는 프레임워크입니다. \"Convention over Configuration(설정보다 관례)\" 원칙을 따릅니다.\r\n\r\n### Spring Boot의 핵심 특징\r\n\r\n**1. 자동 설정 (Auto Configuration)**\r\n클래스패스에 있는 라이브러리를 감지하여 자동으로 적절한 설정을 적용합니다.\r\n\r\n```java\r\n@SpringBootApplication // 아래 3개를 포함\r\n// @SpringBootConfiguration\r\n// @EnableAutoConfiguration ← 자동 설정 핵심\r\n// @ComponentScan\r\npublic class Application {\r\n public static void main(String[] args) {\r\n SpringApplication.run(Application.class, args);\r\n }\r\n}\r\n```\r\n\r\n예: spring-boot-starter-data-jpa 의존성 추가 시 DataSource, EntityManagerFactory, TransactionManager가 자동 설정됩니다.\r\n\r\n**2. 내장 서버 (Embedded Server)**\r\nTomcat, Jetty, Undertow를 내장하여 별도의 서버 설치 없이 독립 실행 가능합니다.\r\n\r\n```bash\r\n# JAR로 패키징 후 바로 실행\r\njava -jar my-app.jar\r\n```\r\n\r\n**3. 스타터 의존성 (Starter Dependencies)**\r\n관련 라이브러리들을 묶어 의존성 관리를 간소화합니다.\r\n\r\n```gradle\r\ndependencies {\r\n implementation 'org.springframework.boot:spring-boot-starter-web'\r\n // spring-webmvc, jackson, tomcat 등이 포함됨\r\n\r\n implementation 'org.springframework.boot:spring-boot-starter-data-jpa'\r\n // spring-data-jpa, hibernate 등이 포함됨\r\n}\r\n```\r\n\r\n**4. 외부 설정 (Externalized Configuration)**\r\n```yaml\r\n# application.yml\r\nserver:\r\n port: 8080\r\n\r\nspring:\r\n datasource:\r\n url: jdbc:mysql://localhost:3306/mydb\r\n username: root\r\n password: password\r\n jpa:\r\n hibernate:\r\n ddl-auto: update\r\n```\r\n\r\n### SpringApplication 실행 과정\r\n\r\n```\r\n1. SpringApplication 인스턴스 생성\r\n2. 환경 설정 (프로파일, 프로퍼티 소스)\r\n3. ApplicationContext 생성\r\n4. Bean 정의 로드\r\n5. Auto Configuration 적용\r\n6. Bean 인스턴스화 및 의존성 주입\r\n7. ApplicationRunner / CommandLineRunner 실행\r\n8. 내장 서버 시작 (웹 애플리케이션인 경우)\r\n```\r\n\r\n### Spring vs Spring Boot\r\n\r\n| 구분 | Spring | Spring Boot |\r\n|------|--------|-------------|\r\n| 설정 | XML/Java Config 수동 설정 | 자동 설정 |\r\n| 서버 | 외부 서버 필요 | 내장 서버 |\r\n| 의존성 | 개별 관리 | 스타터로 일괄 관리 |\r\n| 배포 | WAR | JAR (독립 실행) |\r\n| 시작 코드 | 많음 | 최소화 |\r\n\r\n### 면접 팁\r\n@SpringBootApplication이 포함하는 세 가지 어노테이션의 역할을 정확히 설명하세요. 특히 @EnableAutoConfiguration의 동작 원리(spring.factories/AutoConfiguration.imports)를 이해하면 좋습니다. 자동 설정을 커스터마이징하는 방법(@Conditional, @ConditionalOnClass 등)도 알아두세요.", + "difficulty": "INTERMEDIATE", + "tags": "Spring Boot, 자동 설정, 내장 서버, 스타터, SpringApplication", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 92, + "title": "Test Code", + "content": "Spring에서의 테스트 코드 작성 방법과 종류에 대해 설명해주세요.", + "answer": "Spring 애플리케이션의 테스트는 크게 단위 테스트와 통합 테스트로 나뉩니다. 각각의 범위와 목적에 맞는 테스트 전략을 사용합니다.\r\n\r\n### 단위 테스트 (Unit Test)\r\n\r\n개별 클래스나 메서드를 독립적으로 테스트합니다. 외부 의존성은 Mock으로 대체합니다.\r\n\r\n```java\r\n@ExtendWith(MockitoExtension.class)\r\nclass UserServiceTest {\r\n\r\n @InjectMocks\r\n private UserService userService;\r\n\r\n @Mock\r\n private UserRepository userRepository;\r\n\r\n @Test\r\n @DisplayName(\"사용자 조회 - 존재하는 사용자\")\r\n void findUser_Success() {\r\n // Given\r\n User user = new User(1L, \"홍길동\", \"hong@email.com\");\r\n when(userRepository.findById(1L)).thenReturn(Optional.of(user));\r\n\r\n // When\r\n UserDto result = userService.findById(1L);\r\n\r\n // Then\r\n assertThat(result.getName()).isEqualTo(\"홍길동\");\r\n verify(userRepository, times(1)).findById(1L);\r\n }\r\n\r\n @Test\r\n @DisplayName(\"사용자 조회 - 존재하지 않는 사용자\")\r\n void findUser_NotFound() {\r\n // Given\r\n when(userRepository.findById(1L)).thenReturn(Optional.empty());\r\n\r\n // When & Then\r\n assertThrows(UserNotFoundException.class, () -> {\r\n userService.findById(1L);\r\n });\r\n }\r\n}\r\n```\r\n\r\n### Controller 테스트 (@WebMvcTest)\r\n\r\n```java\r\n@WebMvcTest(UserController.class)\r\nclass UserControllerTest {\r\n\r\n @Autowired\r\n private MockMvc mockMvc;\r\n\r\n @MockBean\r\n private UserService userService;\r\n\r\n @Test\r\n @DisplayName(\"GET /api/users/{id} - 성공\")\r\n void getUser() throws Exception {\r\n UserDto user = new UserDto(1L, \"홍길동\");\r\n when(userService.findById(1L)).thenReturn(user);\r\n\r\n mockMvc.perform(get(\"/api/users/1\"))\r\n .andExpect(status().isOk())\r\n .andExpect(jsonPath(\"$.name\").value(\"홍길동\"))\r\n .andDo(print());\r\n }\r\n\r\n @Test\r\n @DisplayName(\"POST /api/users - 유효성 검증 실패\")\r\n void createUser_Invalid() throws Exception {\r\n String body = \"{\\\"name\\\": \\\"\\\", \\\"email\\\": \\\"invalid\\\"}\";\r\n\r\n mockMvc.perform(post(\"/api/users\")\r\n .contentType(MediaType.APPLICATION_JSON)\r\n .content(body))\r\n .andExpect(status().isBadRequest());\r\n }\r\n}\r\n```\r\n\r\n### Repository 테스트 (@DataJpaTest)\r\n\r\n```java\r\n@DataJpaTest\r\nclass UserRepositoryTest {\r\n\r\n @Autowired\r\n private UserRepository userRepository;\r\n\r\n @Test\r\n void findByEmail() {\r\n User user = new User(\"홍길동\", \"hong@email.com\");\r\n userRepository.save(user);\r\n\r\n Optional found = userRepository.findByEmail(\"hong@email.com\");\r\n\r\n assertThat(found).isPresent();\r\n assertThat(found.get().getName()).isEqualTo(\"홍길동\");\r\n }\r\n}\r\n```\r\n\r\n### 통합 테스트 (@SpringBootTest)\r\n\r\n```java\r\n@SpringBootTest\r\n@AutoConfigureMockMvc\r\nclass UserIntegrationTest {\r\n\r\n @Autowired\r\n private MockMvc mockMvc;\r\n\r\n @Test\r\n void 사용자_생성_후_조회() throws Exception {\r\n // 생성\r\n String createBody = \"{\\\"name\\\":\\\"홍길동\\\",\\\"email\\\":\\\"hong@email.com\\\"}\";\r\n mockMvc.perform(post(\"/api/users\")\r\n .contentType(MediaType.APPLICATION_JSON)\r\n .content(createBody))\r\n .andExpect(status().isCreated());\r\n\r\n // 조회\r\n mockMvc.perform(get(\"/api/users/1\"))\r\n .andExpect(status().isOk())\r\n .andExpect(jsonPath(\"$.name\").value(\"홍길동\"));\r\n }\r\n}\r\n```\r\n\r\n### 테스트 어노테이션 비교\r\n\r\n| 어노테이션 | 범위 | 용도 |\r\n|-----------|------|------|\r\n| @ExtendWith(MockitoExtension.class) | 단위 | Service 테스트 |\r\n| @WebMvcTest | 슬라이스 | Controller 테스트 |\r\n| @DataJpaTest | 슬라이스 | Repository 테스트 |\r\n| @SpringBootTest | 전체 | 통합 테스트 |\r\n\r\n### 면접 팁\r\nGiven-When-Then 패턴으로 테스트를 구조화하세요. Mock과 Stub의 차이, @Mock과 @MockBean의 차이를 이해하고, 각 테스트 어노테이션의 범위와 적절한 사용 시기를 설명할 수 있어야 합니다.", + "difficulty": "INTERMEDIATE", + "tags": "테스트, JUnit, Mockito, 단위 테스트, 통합 테스트", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 93, + "title": "JPA (Java Persistence API)", + "content": "JPA란 무엇이며, 영속성 컨텍스트에 대해 설명해주세요.", + "answer": "JPA(Java Persistence API)는 자바 ORM 기술에 대한 표준 명세로, 객체와 관계형 데이터베이스 간의 매핑을 자동화합니다. Hibernate가 대표적인 JPA 구현체입니다.\r\n\r\n### 엔티티 매핑\r\n\r\n```java\r\n@Entity\r\n@Table(name = \"users\")\r\npublic class User {\r\n @Id\r\n @GeneratedValue(strategy = GenerationType.IDENTITY)\r\n private Long id;\r\n\r\n @Column(nullable = false, length = 50)\r\n private String name;\r\n\r\n @Column(unique = true)\r\n private String email;\r\n\r\n @OneToMany(mappedBy = \"user\", cascade = CascadeType.ALL)\r\n private List orders = new ArrayList<>();\r\n}\r\n```\r\n\r\n### 영속성 컨텍스트 (Persistence Context)\r\n\r\n엔티티를 영구 저장하는 논리적 환경으로, EntityManager를 통해 접근합니다.\r\n\r\n**엔티티 상태:**\r\n```\r\n비영속 (new/transient) → 영속 (managed) → 준영속 (detached) → 삭제 (removed)\r\n\r\nUser user = new User(\"홍길동\"); // 비영속\r\nem.persist(user); // 영속\r\nem.detach(user); // 준영속\r\nem.remove(user); // 삭제\r\n```\r\n\r\n### 영속성 컨텍스트의 이점\r\n\r\n**1. 1차 캐시**\r\n```java\r\nUser user = em.find(User.class, 1L); // DB 조회 후 1차 캐시에 저장\r\nUser same = em.find(User.class, 1L); // 1차 캐시에서 반환 (DB 조회 안 함)\r\n// user == same (동일성 보장)\r\n```\r\n\r\n**2. 쓰기 지연 (Write-behind)**\r\n```java\r\nem.persist(user1); // SQL 생성만, DB 전송 안 함\r\nem.persist(user2); // SQL 생성만, DB 전송 안 함\r\ntx.commit(); // 이 시점에 SQL 일괄 전송 (flush)\r\n```\r\n\r\n**3. 변경 감지 (Dirty Checking)**\r\n```java\r\nUser user = em.find(User.class, 1L);\r\nuser.setName(\"김길동\"); // 별도의 update 호출 없이\r\ntx.commit(); // flush 시점에 변경 사항 자동 감지 → UPDATE SQL 생성\r\n```\r\n\r\n**4. 지연 로딩 (Lazy Loading)**\r\n```java\r\n@OneToMany(mappedBy = \"user\", fetch = FetchType.LAZY)\r\nprivate List orders;\r\n\r\nUser user = em.find(User.class, 1L); // User만 조회\r\nuser.getOrders().size(); // 이 시점에 Orders 조회 (프록시)\r\n```\r\n\r\n### N+1 문제\r\n\r\n```java\r\nList users = userRepository.findAll(); // 1번 쿼리\r\nfor (User user : users) {\r\n user.getOrders().size(); // User마다 1번씩 쿼리 → N번\r\n}\r\n// 총 N+1번 쿼리 실행\r\n\r\n// 해결: Fetch Join\r\n@Query(\"SELECT u FROM User u JOIN FETCH u.orders\")\r\nList findAllWithOrders();\r\n```\r\n\r\n### 면접 팁\r\n영속성 컨텍스트의 4가지 이점(1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩)을 코드 예시와 함께 설명하세요. N+1 문제의 원인과 해결 방법(Fetch Join, @EntityGraph, BatchSize)은 거의 필수 출제 주제입니다.", + "difficulty": "INTERMEDIATE", + "tags": "JPA, ORM, 영속성 컨텍스트, 엔티티, Hibernate", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 94, + "title": "더티 체킹(Dirty Checking)", + "content": "JPA의 더티 체킹(Dirty Checking)이란 무엇이며, 어떻게 동작하나요?", + "answer": "더티 체킹(Dirty Checking)은 JPA가 영속 상태의 엔티티 변경 사항을 자동으로 감지하여 데이터베이스에 반영하는 메커니즘입니다. 개발자가 명시적으로 UPDATE 쿼리를 작성하지 않아도 됩니다.\r\n\r\n### 동작 원리\r\n\r\n```\r\n1. 엔티티를 영속성 컨텍스트에 처음 저장할 때 스냅샷을 생성\r\n2. 트랜잭션 커밋(또는 flush) 시점에\r\n3. 현재 엔티티 상태와 스냅샷을 비교\r\n4. 변경된 필드가 있으면 UPDATE SQL을 생성하여 실행\r\n```\r\n\r\n### 코드 예시\r\n\r\n```java\r\n@Service\r\n@Transactional\r\npublic class UserService {\r\n\r\n @Autowired\r\n private UserRepository userRepository;\r\n\r\n public void updateUserName(Long userId, String newName) {\r\n // 1. 영속 상태의 엔티티 조회 (스냅샷 저장)\r\n User user = userRepository.findById(userId)\r\n .orElseThrow(() -> new RuntimeException(\"User not found\"));\r\n\r\n // 2. 엔티티의 값 변경\r\n user.setName(newName);\r\n\r\n // 3. 별도의 save() 호출 없이 트랜잭션 커밋 시 자동 UPDATE\r\n // userRepository.save(user); ← 불필요!\r\n }\r\n}\r\n\r\n// 실행되는 SQL:\r\n// UPDATE users SET name = '새이름' WHERE id = 1\r\n```\r\n\r\n### 더티 체킹이 동작하는 조건\r\n\r\n1. **엔티티가 영속 상태**여야 합니다 (영속성 컨텍스트에 의해 관리)\r\n2. **트랜잭션 내**에서 변경이 이루어져야 합니다\r\n3. **flush** 시점에 비교가 수행됩니다\r\n\r\n```java\r\n// 더티 체킹이 동작하지 않는 경우\r\npublic void updateFail(Long userId, String newName) {\r\n User user = userRepository.findById(userId).orElseThrow();\r\n user.setName(newName);\r\n // @Transactional이 없으면 변경 감지가 동작하지 않음!\r\n}\r\n\r\n// 준영속 상태에서도 동작하지 않음\r\nUser user = userRepository.findById(1L).orElseThrow();\r\nentityManager.detach(user); // 준영속 상태로 변경\r\nuser.setName(\"새이름\"); // 변경 감지 안 됨\r\n```\r\n\r\n### 전체 필드 업데이트 vs 변경된 필드만 업데이트\r\n\r\n기본적으로 JPA는 모든 필드를 포함하는 UPDATE SQL을 생성합니다.\r\n\r\n```java\r\n// 기본: 모든 필드 업데이트\r\nUPDATE users SET name=?, email=?, age=? WHERE id=?\r\n\r\n// @DynamicUpdate: 변경된 필드만 업데이트\r\n@Entity\r\n@DynamicUpdate\r\npublic class User {\r\n // ...\r\n}\r\n// UPDATE users SET name=? WHERE id=? (name만 변경됨)\r\n```\r\n\r\n### 주의사항\r\n\r\n**1. 대량 업데이트에서는 비효율적**\r\n```java\r\n// 비효율: 1000건을 개별 UPDATE\r\nList users = userRepository.findAll();\r\nusers.forEach(u -> u.setStatus(\"ACTIVE\"));\r\n\r\n// 효율: 벌크 연산 사용\r\n@Modifying\r\n@Query(\"UPDATE User u SET u.status = :status\")\r\nvoid updateAllStatus(@Param(\"status\") String status);\r\n```\r\n\r\n**2. 벌크 연산 후 영속성 컨텍스트 동기화**\r\n```java\r\n@Modifying(clearAutomatically = true) // 벌크 연산 후 영속성 컨텍스트 초기화\r\n@Query(\"UPDATE User u SET u.status = 'INACTIVE' WHERE u.lastLogin < :date\")\r\nvoid deactivateInactiveUsers(@Param(\"date\") LocalDateTime date);\r\n```\r\n\r\n### 면접 팁\r\n더티 체킹은 JPA의 \"영속성 컨텍스트\" 개념의 핵심입니다. 동작 조건(@Transactional, 영속 상태)과 주의사항(벌크 연산, @DynamicUpdate)을 함께 설명하세요. 실무에서는 변경 메서드를 엔티티 내부에 정의하는 것이 권장됩니다.", + "difficulty": "INTERMEDIATE", + "tags": "더티 체킹, 변경 감지, JPA, 영속성 컨텍스트, 스냅샷", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 95, + "title": "Spring Security", + "content": "Spring Security의 동작 원리와 인증/인가 과정에 대해 설명해주세요.", + "answer": "Spring Security는 Spring 기반 애플리케이션의 보안(인증, 인가)을 담당하는 프레임워크입니다. 서블릿 필터 기반으로 동작하며, 요청이 DispatcherServlet에 도달하기 전에 보안 처리를 수행합니다.\r\n\r\n### 인증 vs 인가\r\n\r\n- **인증(Authentication)**: \"누구인가?\" - 사용자의 신원 확인 (로그인)\r\n- **인가(Authorization)**: \"무엇을 할 수 있는가?\" - 권한 확인 (접근 제어)\r\n\r\n### Security Filter Chain\r\n\r\n```\r\nHTTP 요청 → [Security Filter Chain] → DispatcherServlet\r\n\r\n주요 필터:\r\n1. SecurityContextPersistenceFilter - SecurityContext 로드\r\n2. UsernamePasswordAuthenticationFilter - 폼 로그인 처리\r\n3. BasicAuthenticationFilter - HTTP Basic 인증\r\n4. BearerTokenAuthenticationFilter - JWT 등 토큰 인증\r\n5. ExceptionTranslationFilter - 보안 예외 처리\r\n6. FilterSecurityInterceptor - 인가 처리\r\n```\r\n\r\n### 인증 흐름\r\n\r\n```\r\n1. 사용자 로그인 요청 (username, password)\r\n2. AuthenticationFilter가 인증 요청 가로챔\r\n3. UsernamePasswordAuthenticationToken 생성\r\n4. AuthenticationManager에게 인증 위임\r\n5. AuthenticationProvider가 UserDetailsService를 통해 사용자 정보 조회\r\n6. 비밀번호 검증 (PasswordEncoder)\r\n7. 인증 성공 → SecurityContext에 Authentication 저장\r\n8. 이후 요청에서 SecurityContext를 통해 인증 정보 확인\r\n```\r\n\r\n### Security 설정\r\n\r\n```java\r\n@Configuration\r\n@EnableWebSecurity\r\npublic class SecurityConfig {\r\n\r\n @Bean\r\n public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {\r\n http\r\n .csrf(csrf -> csrf.disable())\r\n .sessionManagement(session ->\r\n session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))\r\n .authorizeHttpRequests(auth -> auth\r\n .requestMatchers(\"/api/auth/**\").permitAll()\r\n .requestMatchers(\"/api/admin/**\").hasRole(\"ADMIN\")\r\n .anyRequest().authenticated()\r\n )\r\n .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);\r\n\r\n return http.build();\r\n }\r\n\r\n @Bean\r\n public PasswordEncoder passwordEncoder() {\r\n return new BCryptPasswordEncoder();\r\n }\r\n}\r\n```\r\n\r\n### JWT 기반 인증\r\n\r\n```java\r\n@Component\r\npublic class JwtAuthenticationFilter extends OncePerRequestFilter {\r\n\r\n @Override\r\n protected void doFilterInternal(HttpServletRequest request,\r\n HttpServletResponse response,\r\n FilterChain filterChain) throws Exception {\r\n String token = extractToken(request);\r\n\r\n if (token != null && jwtProvider.validateToken(token)) {\r\n String username = jwtProvider.getUsername(token);\r\n UserDetails userDetails = userDetailsService.loadUserByUsername(username);\r\n\r\n UsernamePasswordAuthenticationToken auth =\r\n new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());\r\n\r\n SecurityContextHolder.getContext().setAuthentication(auth);\r\n }\r\n\r\n filterChain.doFilter(request, response);\r\n }\r\n}\r\n```\r\n\r\n### 세션 기반 vs 토큰 기반 인증\r\n\r\n| 구분 | 세션 | JWT |\r\n|------|------|-----|\r\n| 저장 위치 | 서버 | 클라이언트 |\r\n| 상태 | Stateful | Stateless |\r\n| 확장성 | 서버 간 세션 공유 필요 | 확장 용이 |\r\n| 보안 | 서버에서 무효화 가능 | 만료 전 무효화 어려움 |\r\n\r\n### 면접 팁\r\nSpring Security의 필터 체인 구조와 인증 흐름을 순서대로 설명할 수 있어야 합니다. JWT와 세션 방식의 차이점, 각각의 장단점을 비교할 수 있으면 좋습니다. CORS, CSRF 처리 방법도 알아두세요.", + "difficulty": "ADVANCED", + "tags": "Spring Security, 인증, 인가, JWT, 필터 체인", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 96, + "title": "싱글톤 패턴(Singleton Pattern)", + "content": "싱글톤 패턴이란 무엇이며, 구현 방법과 주의사항에 대해 설명해주세요.", + "answer": "싱글톤 패턴(Singleton Pattern)은 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 전역적으로 접근할 수 있는 접근점을 제공하는 디자인 패턴입니다.\r\n\r\n### 구현 방법\r\n\r\n**1. Eager Initialization (즉시 초기화)**\r\n```java\r\npublic class Singleton {\r\n private static final Singleton INSTANCE = new Singleton();\r\n private Singleton() {}\r\n public static Singleton getInstance() { return INSTANCE; }\r\n}\r\n```\r\n\r\n**2. Lazy Initialization + synchronized**\r\n```java\r\npublic class Singleton {\r\n private static Singleton instance;\r\n private Singleton() {}\r\n\r\n public static synchronized Singleton getInstance() {\r\n if (instance == null) {\r\n instance = new Singleton();\r\n }\r\n return instance;\r\n }\r\n}\r\n// 단점: 매번 동기화 오버헤드\r\n```\r\n\r\n**3. Double-Checked Locking (DCL)**\r\n```java\r\npublic class Singleton {\r\n private static volatile Singleton instance;\r\n private Singleton() {}\r\n\r\n public static Singleton getInstance() {\r\n if (instance == null) { // 1차 체크\r\n synchronized (Singleton.class) {\r\n if (instance == null) { // 2차 체크\r\n instance = new Singleton();\r\n }\r\n }\r\n }\r\n return instance;\r\n }\r\n}\r\n```\r\n\r\n**4. Bill Pugh 방식 (정적 내부 클래스) - 권장**\r\n```java\r\npublic class Singleton {\r\n private Singleton() {}\r\n\r\n private static class SingletonHolder {\r\n private static final Singleton INSTANCE = new Singleton();\r\n }\r\n\r\n public static Singleton getInstance() {\r\n return SingletonHolder.INSTANCE;\r\n }\r\n}\r\n// Lazy + Thread-safe + 간결\r\n```\r\n\r\n**5. Enum (가장 안전) - 권장**\r\n```java\r\npublic enum Singleton {\r\n INSTANCE;\r\n\r\n public void doSomething() { ... }\r\n}\r\n// 직렬화, 리플렉션 공격에도 안전\r\n```\r\n\r\n### 싱글톤의 문제점\r\n- 전역 상태를 만들어 테스트가 어려움\r\n- 의존성을 감추어 코드의 결합도 증가\r\n- 멀티스레드 환경에서 동시성 이슈\r\n- SOLID의 단일 책임 원칙 위반 가능\r\n- 상태를 가지면 동시성 문제 발생\r\n\r\n### Spring 싱글톤과의 차이\r\n\r\n| 구분 | 싱글톤 패턴 | Spring 싱글톤 |\r\n|------|-----------|--------------|\r\n| 관리 | 클래스 자체 | Spring 컨테이너 |\r\n| 범위 | JVM | 스프링 컨텍스트 |\r\n| 테스트 | 어려움 | DI로 쉬움 |\r\n| 구현 | private 생성자 | 일반 클래스 |\r\n\r\nSpring은 싱글톤 패턴의 단점(테스트 어려움, 결합도)을 DI 컨테이너로 해결합니다.\r\n\r\n### 면접 팁\r\n구현 방법보다 싱글톤의 문제점과 Spring이 이를 어떻게 해결하는지에 초점을 맞추세요. 실무에서는 직접 싱글톤 패턴을 구현하기보다 Spring Bean을 사용합니다.", + "difficulty": "BASIC", + "tags": "싱글톤, 디자인 패턴, 인스턴스 하나, Spring Bean, 스레드 안전", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 97, + "title": "팩토리 메소드 패턴", + "content": "팩토리 메소드 패턴이란 무엇이며, 어떤 상황에서 사용하나요?", + "answer": "팩토리 메소드 패턴(Factory Method Pattern)은 객체 생성을 직접 하지 않고, 서브클래스에 위임하는 생성 패턴입니다. 어떤 클래스의 인스턴스를 만들지를 서브클래스에서 결정합니다.\r\n\r\n### 구현 예시\r\n\r\n```java\r\n// 1. 제품 인터페이스\r\npublic interface Notification {\r\n void send(String message);\r\n}\r\n\r\n// 2. 구체적 제품\r\npublic class EmailNotification implements Notification {\r\n public void send(String message) {\r\n System.out.println(\"이메일 발송: \" + message);\r\n }\r\n}\r\n\r\npublic class SmsNotification implements Notification {\r\n public void send(String message) {\r\n System.out.println(\"SMS 발송: \" + message);\r\n }\r\n}\r\n\r\npublic class PushNotification implements Notification {\r\n public void send(String message) {\r\n System.out.println(\"푸시 알림: \" + message);\r\n }\r\n}\r\n\r\n// 3. 팩토리\r\npublic class NotificationFactory {\r\n public static Notification createNotification(String type) {\r\n return switch (type) {\r\n case \"EMAIL\" -> new EmailNotification();\r\n case \"SMS\" -> new SmsNotification();\r\n case \"PUSH\" -> new PushNotification();\r\n default -> throw new IllegalArgumentException(\"Unknown type: \" + type);\r\n };\r\n }\r\n}\r\n\r\n// 4. 사용\r\nNotification notification = NotificationFactory.createNotification(\"EMAIL\");\r\nnotification.send(\"안녕하세요\");\r\n```\r\n\r\n### Spring에서의 활용\r\n\r\n```java\r\n// Spring의 BeanFactory가 대표적인 팩토리 패턴\r\nApplicationContext context = new AnnotationConfigApplicationContext(Config.class);\r\nUserService userService = context.getBean(UserService.class);\r\n\r\n// 전략 패턴과 결합한 활용\r\n@Component\r\npublic class NotificationFactory {\r\n private final Map notificationMap;\r\n\r\n public NotificationFactory(List notifications) {\r\n notificationMap = notifications.stream()\r\n .collect(Collectors.toMap(\r\n n -> n.getType(),\r\n Function.identity()\r\n ));\r\n }\r\n\r\n public Notification getNotification(String type) {\r\n return notificationMap.get(type);\r\n }\r\n}\r\n```\r\n\r\n### 팩토리 메소드 vs 추상 팩토리\r\n\r\n| 구분 | 팩토리 메소드 | 추상 팩토리 |\r\n|------|-------------|------------|\r\n| 생성 대상 | 하나의 제품 | 관련된 제품군 |\r\n| 확장 방식 | 서브클래스로 확장 | 팩토리 인터페이스로 확장 |\r\n| 복잡도 | 단순 | 복잡 |\r\n\r\n### 장점\r\n- 객체 생성과 사용을 분리 (느슨한 결합)\r\n- OCP(개방-폐쇄 원칙) 준수: 새로운 타입 추가 시 팩토리만 수정\r\n- 코드 중복 제거\r\n\r\n### 면접 팁\r\n팩토리 패턴은 Spring의 BeanFactory, ApplicationContext에서 핵심적으로 사용됩니다. 단순 팩토리, 팩토리 메소드, 추상 팩토리의 차이를 구분하여 설명할 수 있으면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "팩토리 메소드, 디자인 패턴, 생성 패턴, 다형성, 추상 팩토리", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 98, + "title": "옵저버 패턴(Observer Pattern)", + "content": "옵저버 패턴이란 무엇이며, Spring에서는 어떻게 활용되나요?", + "answer": "옵저버 패턴(Observer Pattern)은 객체의 상태 변화가 발생했을 때, 의존하는 다른 객체들에게 자동으로 알림을 보내는 행위 패턴입니다. 일대다(one-to-many) 의존 관계를 정의합니다.\r\n\r\n### 기본 구현\r\n\r\n```java\r\n// 옵저버 인터페이스\r\npublic interface EventListener {\r\n void update(String event);\r\n}\r\n\r\n// 발행자 (Subject)\r\npublic class EventPublisher {\r\n private List listeners = new ArrayList<>();\r\n\r\n public void subscribe(EventListener listener) {\r\n listeners.add(listener);\r\n }\r\n\r\n public void unsubscribe(EventListener listener) {\r\n listeners.remove(listener);\r\n }\r\n\r\n public void notify(String event) {\r\n for (EventListener listener : listeners) {\r\n listener.update(event);\r\n }\r\n }\r\n}\r\n\r\n// 구체적 옵저버\r\npublic class EmailAlert implements EventListener {\r\n public void update(String event) {\r\n System.out.println(\"이메일 알림: \" + event);\r\n }\r\n}\r\n\r\npublic class SlackAlert implements EventListener {\r\n public void update(String event) {\r\n System.out.println(\"슬랙 알림: \" + event);\r\n }\r\n}\r\n```\r\n\r\n### Spring의 이벤트 시스템\r\n\r\nSpring은 옵저버 패턴을 ApplicationEvent와 @EventListener로 구현합니다.\r\n\r\n```java\r\n// 이벤트 정의\r\npublic class OrderCreatedEvent {\r\n private final Long orderId;\r\n private final Long userId;\r\n\r\n public OrderCreatedEvent(Long orderId, Long userId) {\r\n this.orderId = orderId;\r\n this.userId = userId;\r\n }\r\n // getters\r\n}\r\n\r\n// 이벤트 발행\r\n@Service\r\n@Transactional\r\npublic class OrderService {\r\n @Autowired\r\n private ApplicationEventPublisher eventPublisher;\r\n\r\n public void createOrder(OrderRequest request) {\r\n Order order = orderRepository.save(new Order(request));\r\n\r\n // 이벤트 발행 - 주문 서비스는 알림 로직을 모름 (느슨한 결합)\r\n eventPublisher.publishEvent(new OrderCreatedEvent(order.getId(), request.getUserId()));\r\n }\r\n}\r\n\r\n// 이벤트 리스너 (구독자)\r\n@Component\r\npublic class OrderEventHandler {\r\n\r\n @EventListener\r\n public void handleOrderCreated(OrderCreatedEvent event) {\r\n // 이메일 발송\r\n emailService.sendOrderConfirmation(event.getUserId());\r\n }\r\n\r\n @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)\r\n public void sendNotification(OrderCreatedEvent event) {\r\n // 트랜잭션 커밋 후 실행\r\n notificationService.sendPush(event.getUserId());\r\n }\r\n\r\n @Async\r\n @EventListener\r\n public void logOrder(OrderCreatedEvent event) {\r\n // 비동기로 로깅\r\n logService.logOrderCreation(event.getOrderId());\r\n }\r\n}\r\n```\r\n\r\n### 장점\r\n- **느슨한 결합**: 발행자와 구독자가 서로를 알 필요 없음\r\n- **확장성**: 새로운 구독자를 쉽게 추가 가능\r\n- **단일 책임**: 각 구독자가 자신의 로직에만 집중\r\n\r\n### 단점\r\n- 실행 순서 제어가 어려움\r\n- 디버깅이 복잡해질 수 있음\r\n- 메모리 누수 가능 (구독 해제를 잊는 경우)\r\n\r\n### 면접 팁\r\n옵저버 패턴은 Spring의 이벤트 시스템, 메시지 큐(Kafka), React의 상태 관리 등 다양한 곳에서 사용됩니다. @TransactionalEventListener를 사용한 트랜잭션과 이벤트의 연동을 설명할 수 있으면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "옵저버 패턴, 이벤트, 발행-구독, ApplicationEvent, 느슨한 결합", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 99, + "title": "스트레티지 패턴(Strategy Pattern)", + "content": "전략 패턴(Strategy Pattern)이란 무엇이며, Spring에서 어떻게 활용되나요?", + "answer": "전략 패턴(Strategy Pattern)은 알고리즘 군을 정의하고, 각각을 캡슐화하여 상호 교환 가능하게 만드는 행위 패턴입니다. 실행 중에 알고리즘을 교체할 수 있습니다.\r\n\r\n### 구현 예시: 할인 정책\r\n\r\n```java\r\n// 전략 인터페이스\r\npublic interface DiscountStrategy {\r\n int discount(int price);\r\n String getType();\r\n}\r\n\r\n// 구체적 전략들\r\n@Component\r\npublic class FixedDiscount implements DiscountStrategy {\r\n public int discount(int price) { return price - 1000; }\r\n public String getType() { return \"FIXED\"; }\r\n}\r\n\r\n@Component\r\npublic class PercentDiscount implements DiscountStrategy {\r\n public int discount(int price) { return (int)(price * 0.9); }\r\n public String getType() { return \"PERCENT\"; }\r\n}\r\n\r\n@Component\r\npublic class NoDiscount implements DiscountStrategy {\r\n public int discount(int price) { return price; }\r\n public String getType() { return \"NONE\"; }\r\n}\r\n\r\n// 컨텍스트 (전략을 사용하는 클래스)\r\n@Service\r\npublic class PaymentService {\r\n private final Map strategyMap;\r\n\r\n // Spring DI로 모든 전략을 자동 주입\r\n public PaymentService(List strategies) {\r\n this.strategyMap = strategies.stream()\r\n .collect(Collectors.toMap(\r\n DiscountStrategy::getType,\r\n Function.identity()\r\n ));\r\n }\r\n\r\n public int calculatePrice(String discountType, int originalPrice) {\r\n DiscountStrategy strategy = strategyMap.getOrDefault(discountType,\r\n strategyMap.get(\"NONE\"));\r\n return strategy.discount(originalPrice);\r\n }\r\n}\r\n```\r\n\r\n### if-else를 전략 패턴으로 제거\r\n\r\n```java\r\n// Before: if-else 지옥\r\npublic int calculateDiscount(String type, int price) {\r\n if (\"FIXED\".equals(type)) {\r\n return price - 1000;\r\n } else if (\"PERCENT\".equals(type)) {\r\n return (int)(price * 0.9);\r\n } else if (\"VIP\".equals(type)) {\r\n return (int)(price * 0.8);\r\n }\r\n // 새 할인 타입 추가 시 여기를 수정해야 함 → OCP 위반\r\n return price;\r\n}\r\n\r\n// After: 전략 패턴\r\n// 새 할인 타입 추가 시 새 클래스만 추가 → OCP 준수\r\n@Component\r\npublic class VipDiscount implements DiscountStrategy {\r\n public int discount(int price) { return (int)(price * 0.8); }\r\n public String getType() { return \"VIP\"; }\r\n}\r\n```\r\n\r\n### Spring에서의 전략 패턴 활용\r\n\r\nSpring의 많은 부분이 전략 패턴으로 설계되어 있습니다:\r\n- **TransactionManager**: PlatformTransactionManager 인터페이스의 다양한 구현체\r\n- **ViewResolver**: 다양한 뷰 기술 지원\r\n- **HandlerMapping**: 다양한 URL 매핑 전략\r\n- **PasswordEncoder**: BCrypt, Argon2 등 다양한 암호화 전략\r\n\r\n### 장점\r\n- 알고리즘 교체가 자유로움 (런타임)\r\n- if-else 조건문 제거\r\n- OCP 준수 (새 전략 추가 시 기존 코드 수정 불필요)\r\n- 테스트가 용이 (각 전략을 독립적으로 테스트)\r\n\r\n### 면접 팁\r\n전략 패턴은 Spring의 DI와 가장 잘 어울리는 패턴입니다. List나 Map으로 전략을 자동 주입받는 방법을 코드로 보여주면 실무적인 답변이 됩니다.", + "difficulty": "INTERMEDIATE", + "tags": "전략 패턴, 디자인 패턴, 행위 패턴, DI, 알고리즘 교체", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 100, + "title": "템플릿 메소드 패턴", + "content": "템플릿 메소드 패턴이란 무엇이며, 어떤 상황에서 사용하나요?", + "answer": "템플릿 메소드 패턴(Template Method Pattern)은 알고리즘의 구조(골격)를 상위 클래스에서 정의하고, 일부 단계의 구체적인 구현은 서브클래스에서 제공하도록 하는 패턴입니다.\r\n\r\n### 구현 예시: 데이터 처리 파이프라인\r\n\r\n```java\r\n// 추상 클래스: 알고리즘 골격 정의\r\npublic abstract class DataProcessor {\r\n\r\n // 템플릿 메소드: 알고리즘의 골격 (final로 오버라이드 방지)\r\n public final void process() {\r\n readData();\r\n parseData();\r\n processData();\r\n if (shouldSave()) { // 후크 메소드\r\n saveData();\r\n }\r\n logResult();\r\n }\r\n\r\n // 추상 메소드: 서브클래스에서 반드시 구현\r\n protected abstract void readData();\r\n protected abstract void parseData();\r\n protected abstract void processData();\r\n\r\n // 후크 메소드: 기본 구현 제공, 필요시 오버라이드\r\n protected boolean shouldSave() {\r\n return true;\r\n }\r\n\r\n // 공통 로직\r\n private void saveData() {\r\n System.out.println(\"데이터 저장 완료\");\r\n }\r\n\r\n private void logResult() {\r\n System.out.println(\"처리 완료 로그 기록\");\r\n }\r\n}\r\n\r\n// 구체적 구현: CSV 처리\r\npublic class CsvProcessor extends DataProcessor {\r\n protected void readData() { System.out.println(\"CSV 파일 읽기\"); }\r\n protected void parseData() { System.out.println(\"CSV 파싱\"); }\r\n protected void processData() { System.out.println(\"CSV 데이터 처리\"); }\r\n}\r\n\r\n// 구체적 구현: JSON 처리\r\npublic class JsonProcessor extends DataProcessor {\r\n protected void readData() { System.out.println(\"JSON 파일 읽기\"); }\r\n protected void parseData() { System.out.println(\"JSON 파싱\"); }\r\n protected void processData() { System.out.println(\"JSON 데이터 처리\"); }\r\n\r\n @Override\r\n protected boolean shouldSave() {\r\n return false; // 저장하지 않음\r\n }\r\n}\r\n\r\n// 사용\r\nDataProcessor processor = new CsvProcessor();\r\nprocessor.process(); // 템플릿 메소드 호출\r\n```\r\n\r\n### Spring에서의 활용\r\n\r\n```java\r\n// Spring의 JdbcTemplate - 대표적인 템플릿 패턴\r\njdbcTemplate.query(\r\n \"SELECT * FROM users WHERE id = ?\",\r\n new Object[]{id},\r\n (rs, rowNum) -> new User( // 콜백으로 변형된 형태\r\n rs.getLong(\"id\"),\r\n rs.getString(\"name\")\r\n )\r\n);\r\n\r\n// AbstractController, HttpServlet의 service() 메소드도 템플릿 패턴\r\n```\r\n\r\n### 템플릿 메소드 vs 전략 패턴\r\n\r\n| 구분 | 템플릿 메소드 | 전략 패턴 |\r\n|------|-------------|----------|\r\n| 구현 방식 | 상속 | 조합(위임) |\r\n| 변경 단위 | 알고리즘의 일부 | 전체 알고리즘 |\r\n| 결합도 | 높음 (상속) | 낮음 (인터페이스) |\r\n| 유연성 | 컴파일 타임 | 런타임 교체 가능 |\r\n\r\n### 장점\r\n- 코드 중복 제거 (공통 로직을 상위 클래스에)\r\n- 알고리즘의 핵심 구조를 한 곳에서 관리\r\n- 확장 포인트가 명확\r\n\r\n### 단점\r\n- 상속 기반이므로 결합도가 높음\r\n- 상위 클래스 변경 시 하위 클래스에 영향\r\n\r\n### 면접 팁\r\nSpring의 JdbcTemplate, RestTemplate, TransactionTemplate 등 \"~Template\" 이름의 클래스들이 이 패턴을 사용합니다. 콜백 패턴과 결합된 Spring의 변형도 이해하면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "템플릿 메소드, 디자인 패턴, 행위 패턴, 상속, 후크 메소드", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 101, + "title": "어댑터 패턴(Adapter Pattern)", + "content": "어댑터 패턴이란 무엇이며, 실무에서 어떻게 활용되나요?", + "answer": "어댑터 패턴(Adapter Pattern)은 호환되지 않는 인터페이스를 가진 클래스들을 함께 동작할 수 있도록 변환해주는 구조 패턴입니다. 콘센트 어댑터(110V → 220V 변환)와 같은 역할을 합니다.\r\n\r\n### 구현 예시\r\n\r\n```java\r\n// 기존 시스템: XML 형식으로 데이터 제공\r\npublic class LegacyXmlService {\r\n public String getXmlData() {\r\n return \"홍길동\";\r\n }\r\n}\r\n\r\n// 새 시스템이 요구하는 인터페이스\r\npublic interface JsonDataProvider {\r\n String getJsonData();\r\n}\r\n\r\n// 어댑터: XML → JSON 변환\r\npublic class XmlToJsonAdapter implements JsonDataProvider {\r\n private final LegacyXmlService legacyService;\r\n\r\n public XmlToJsonAdapter(LegacyXmlService legacyService) {\r\n this.legacyService = legacyService;\r\n }\r\n\r\n @Override\r\n public String getJsonData() {\r\n String xml = legacyService.getXmlData();\r\n // XML을 JSON으로 변환하는 로직\r\n return convertXmlToJson(xml);\r\n }\r\n\r\n private String convertXmlToJson(String xml) {\r\n // 변환 로직\r\n return \"{\\\"name\\\": \\\"홍길동\\\"}\";\r\n }\r\n}\r\n\r\n// 사용\r\nJsonDataProvider provider = new XmlToJsonAdapter(new LegacyXmlService());\r\nString json = provider.getJsonData();\r\n```\r\n\r\n### 실무 활용: 외부 라이브러리 래핑\r\n\r\n```java\r\n// 외부 결제 라이브러리 (우리가 수정할 수 없음)\r\npublic class ExternalPaymentSDK {\r\n public PaymentResult charge(String cardNumber, double amount) { ... }\r\n}\r\n\r\n// 우리 시스템의 인터페이스\r\npublic interface PaymentGateway {\r\n PaymentResponse pay(PaymentRequest request);\r\n}\r\n\r\n// 어댑터\r\n@Component\r\npublic class ExternalPaymentAdapter implements PaymentGateway {\r\n private final ExternalPaymentSDK sdk;\r\n\r\n public ExternalPaymentAdapter(ExternalPaymentSDK sdk) {\r\n this.sdk = sdk;\r\n }\r\n\r\n @Override\r\n public PaymentResponse pay(PaymentRequest request) {\r\n PaymentResult result = sdk.charge(\r\n request.getCardNumber(),\r\n request.getAmount()\r\n );\r\n return new PaymentResponse(result.isSuccess(), result.getTransactionId());\r\n }\r\n}\r\n```\r\n\r\n### Spring에서의 어댑터 패턴\r\n\r\n**HandlerAdapter**: 다양한 형태의 Controller를 통일된 방식으로 실행\r\n\r\n```java\r\n// Spring MVC의 HandlerAdapter\r\npublic interface HandlerAdapter {\r\n boolean supports(Object handler);\r\n ModelAndView handle(HttpServletRequest request,\r\n HttpServletResponse response,\r\n Object handler);\r\n}\r\n\r\n// 다양한 어댑터 구현체\r\n// - RequestMappingHandlerAdapter (@RequestMapping)\r\n// - HttpRequestHandlerAdapter\r\n// - SimpleControllerHandlerAdapter\r\n```\r\n\r\n### 클래스 어댑터 vs 객체 어댑터\r\n\r\n| 구분 | 클래스 어댑터 | 객체 어댑터 |\r\n|------|-------------|------------|\r\n| 방식 | 상속 | 조합(위임) |\r\n| 유연성 | 낮음 | 높음 |\r\n| 다중 적용 | 불가 (단일 상속) | 가능 |\r\n| 권장 | X | O |\r\n\r\n### 장점\r\n- 기존 코드 수정 없이 새 인터페이스에 적응\r\n- 외부 라이브러리와의 결합도 감소\r\n- 테스트 용이 (어댑터를 Mock으로 교체)\r\n\r\n### 면접 팁\r\n어댑터 패턴은 레거시 시스템 연동, 외부 라이브러리 래핑 등 실무에서 매우 자주 사용됩니다. Spring MVC의 HandlerAdapter와 연결하여 설명하면 좋습니다.", + "difficulty": "INTERMEDIATE", + "tags": "어댑터 패턴, 디자인 패턴, 구조 패턴, 인터페이스 변환, 래퍼", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 102, + "title": "SOLID 원칙", + "content": "SOLID 원칙이란 무엇이며, 각 원칙에 대해 설명해주세요.", + "answer": "SOLID는 로버트 마틴(Uncle Bob)이 정리한 객체 지향 설계의 5가지 원칙입니다. 이 원칙들을 준수하면 유지보수가 쉽고 확장 가능한 소프트웨어를 설계할 수 있습니다.\r\n\r\n### 1. SRP (Single Responsibility Principle) - 단일 책임 원칙\r\n\r\n하나의 클래스는 하나의 책임만 가져야 합니다.\r\n\r\n```java\r\n// Bad: 여러 책임\r\npublic class UserService {\r\n public void createUser(User user) { ... }\r\n public void sendEmail(String email) { ... } // 이메일 발송은 별도 책임\r\n public String exportToCsv(List users) { ... } // 내보내기도 별도\r\n}\r\n\r\n// Good: 책임 분리\r\npublic class UserService { public void createUser(User user) { ... } }\r\npublic class EmailService { public void sendEmail(String email) { ... } }\r\npublic class UserExporter { public String exportToCsv(List users) { ... } }\r\n```\r\n\r\n### 2. OCP (Open-Closed Principle) - 개방-폐쇄 원칙\r\n\r\n확장에는 열려 있고, 변경에는 닫혀 있어야 합니다.\r\n\r\n```java\r\n// Bad: 새 할인 타입 추가 시 기존 코드 수정 필요\r\npublic int calculateDiscount(String type, int price) {\r\n if (\"FIXED\".equals(type)) return price - 1000;\r\n else if (\"PERCENT\".equals(type)) return (int)(price * 0.9);\r\n // 새 타입 추가 시 여기를 수정해야 함\r\n}\r\n\r\n// Good: 인터페이스 + 새 구현체 추가로 확장\r\npublic interface DiscountPolicy {\r\n int discount(int price);\r\n}\r\npublic class FixedDiscount implements DiscountPolicy { ... }\r\npublic class PercentDiscount implements DiscountPolicy { ... }\r\n// 새 할인: 기존 코드 수정 없이 새 클래스만 추가\r\n```\r\n\r\n### 3. LSP (Liskov Substitution Principle) - 리스코프 치환 원칙\r\n\r\n하위 타입은 상위 타입을 대체할 수 있어야 합니다.\r\n\r\n```java\r\n// Bad: 정사각형이 직사각형을 상속하면 LSP 위반\r\nclass Rectangle {\r\n void setWidth(int w) { this.width = w; }\r\n void setHeight(int h) { this.height = h; }\r\n}\r\nclass Square extends Rectangle {\r\n void setWidth(int w) { this.width = w; this.height = w; } // 예상과 다른 동작\r\n}\r\n\r\n// Rectangle을 기대하는 코드에서 Square를 넣으면 오작동\r\n```\r\n\r\n### 4. ISP (Interface Segregation Principle) - 인터페이스 분리 원칙\r\n\r\n클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.\r\n\r\n```java\r\n// Bad: 하나의 큰 인터페이스\r\npublic interface Worker {\r\n void work();\r\n void eat();\r\n void sleep();\r\n}\r\n// 로봇은 eat(), sleep()이 불필요\r\n\r\n// Good: 인터페이스 분리\r\npublic interface Workable { void work(); }\r\npublic interface Eatable { void eat(); }\r\npublic interface Sleepable { void sleep(); }\r\n\r\npublic class Human implements Workable, Eatable, Sleepable { ... }\r\npublic class Robot implements Workable { ... }\r\n```\r\n\r\n### 5. DIP (Dependency Inversion Principle) - 의존 역전 원칙\r\n\r\n고수준 모듈이 저수준 모듈에 의존하지 않고, 둘 다 추상화에 의존해야 합니다.\r\n\r\n```java\r\n// Bad: 구체 클래스에 직접 의존\r\npublic class OrderService {\r\n private MySqlOrderRepository repository = new MySqlOrderRepository();\r\n}\r\n\r\n// Good: 추상화(인터페이스)에 의존\r\npublic class OrderService {\r\n private final OrderRepository repository; // 인터페이스에 의존\r\n\r\n public OrderService(OrderRepository repository) {\r\n this.repository = repository; // DI로 주입\r\n }\r\n}\r\n```\r\n\r\nSpring의 DI(Dependency Injection)는 DIP를 자연스럽게 구현할 수 있게 해줍니다.\r\n\r\n### SOLID와 Spring\r\n\r\n| 원칙 | Spring에서의 적용 |\r\n|------|-----------------|\r\n| SRP | @Service, @Repository 등 계층 분리 |\r\n| OCP | 인터페이스 + 다양한 구현체 |\r\n| LSP | 인터페이스 기반 프로그래밍 |\r\n| ISP | 작은 인터페이스 설계 |\r\n| DIP | @Autowired, 생성자 주입 |\r\n\r\n### 면접 팁\r\n각 원칙을 코드 예시와 함께 설명하고, Spring에서 어떻게 적용되는지 연결하세요. 특히 OCP와 DIP는 Spring의 핵심 철학과 밀접한 관련이 있습니다.", + "difficulty": "INTERMEDIATE", + "tags": "SOLID, 객체지향, SRP, OCP, LSP, ISP, DIP", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 103, + "title": "DI(의존성 주입)란 무엇인가요?", + "content": "DI(Dependency Injection, 의존성 주입)란 무엇이며, 왜 사용하나요?", + "answer": "DI(의존성 주입)는 객체가 필요로 하는 의존 객체를 직접 생성하지 않고, 외부(스프링 컨테이너)에서 주입받는 디자인 패턴입니다.\n\n핵심 개념:\n- 객체 간의 결합도를 낮추어 유연한 코드를 만들 수 있습니다.\n- 테스트 시 Mock 객체로 쉽게 교체할 수 있어 단위 테스트가 용이합니다.\n- 스프링에서는 IoC 컨테이너가 객체의 생성과 주입을 담당합니다.\n\n주입 방식 3가지:\n1. 생성자 주입 (권장): 불변성 보장, 순환 참조 방지\n2. Setter 주입: 선택적 의존성에 사용\n3. 필드 주입 (@Autowired): 간결하지만 테스트 어려움\n\n생성자 주입 예시:\n@Service\npublic class OrderService {\n private final OrderRepository orderRepository;\n\n public OrderService(OrderRepository orderRepository) {\n this.orderRepository = orderRepository;\n }\n}\n\n스프링 공식 문서에서도 생성자 주입을 권장합니다. 필드 주입은 간편하지만 테스트가 어렵고 의존성이 숨겨지는 단점이 있습니다.", + "difficulty": "BASIC", + "tags": "DI, IoC, Spring", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 104, + "title": "IoC(제어의 역전)란 무엇인가요?", + "content": "IoC(Inversion of Control, 제어의 역전)란 무엇이며, 스프링에서 어떻게 구현되나요?", + "answer": "IoC(제어의 역전)는 객체의 생성, 생명주기 관리 등의 제어권을 개발자가 아닌 프레임워크(스프링 컨테이너)가 담당하는 것을 말합니다.\n\n기존 방식 (개발자가 직접 제어):\n- new 키워드로 객체를 직접 생성\n- 객체 간 의존 관계를 직접 설정\n\nIoC 방식 (스프링이 제어):\n- 스프링 컨테이너가 객체(Bean)를 생성하고 관리\n- 필요한 곳에 자동으로 의존성 주입 (DI)\n\n스프링에서의 구현:\n1. @Component, @Service, @Repository 등으로 빈 등록\n2. @ComponentScan으로 빈을 자동 탐색\n3. @Autowired 또는 생성자 주입으로 의존성 주입\n\nIoC의 장점:\n- 객체 간 결합도 감소\n- 코드 재사용성과 유연성 증가\n- 단위 테스트 용이\n- 객체의 생명주기를 프레임워크가 관리하므로 개발자는 비즈니스 로직에 집중 가능\n\nIoC 컨테이너 = BeanFactory (기본) + ApplicationContext (확장)\nApplicationContext가 BeanFactory를 상속하며, 메시지 소스, 이벤트 발행 등 추가 기능을 제공합니다.", + "difficulty": "BASIC", + "tags": "IoC, DI, Spring Container", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 105, + "title": "AOP(관점 지향 프로그래밍)란 무엇인가요?", + "content": "AOP(Aspect-Oriented Programming)란 무엇이며, 스프링에서 어떻게 동작하나요?", + "answer": "AOP(관점 지향 프로그래밍)는 핵심 비즈니스 로직과 공통 관심사(로깅, 보안, 트랜잭션 등)를 분리하는 프로그래밍 기법입니다.\n\n왜 필요한가:\n여러 클래스에 반복되는 코드(로깅, 인증 체크, 트랜잭션 처리 등)를 매번 작성하면 코드가 지저분해집니다. AOP를 사용하면 이런 횡단 관심사를 한 곳에서 관리할 수 있습니다.\n\n핵심 용어:\n- Aspect: 공통 관심사를 모듈화한 것 (예: 로깅 Aspect)\n- Advice: 실제 실행되는 코드 (@Before, @After, @Around 등)\n- JoinPoint: Advice가 적용될 수 있는 지점 (메서드 호출 등)\n- Pointcut: 어떤 JoinPoint에 Advice를 적용할지 정하는 표현식\n- Weaving: Aspect를 실제 코드에 적용하는 과정\n\nAdvice 종류:\n- @Before: 메서드 실행 전\n- @After: 메서드 실행 후 (성공/실패 무관)\n- @AfterReturning: 정상 리턴 후\n- @AfterThrowing: 예외 발생 후\n- @Around: 메서드 전후 모두 제어 (가장 강력)\n\nSpring AOP의 동작 원리:\n- 프록시 패턴 기반 (JDK Dynamic Proxy 또는 CGLIB)\n- 메서드 레벨에서만 AOP 적용 가능\n- @Transactional, @Cacheable, @Secured 모두 AOP 기반으로 동작\n\nAspectJ와의 차이:\n- Spring AOP: 런타임 프록시 방식, 메서드만 지원\n- AspectJ: 컴파일/로드 타임 위빙, 필드·생성자 등도 지원", + "difficulty": "BASIC", + "tags": "AOP, Aspect, Proxy", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 106, + "title": "@Component, @Service, @Repository, @Controller 차이는?", + "content": "@Component, @Service, @Repository, @Controller 어노테이션의 차이점은 무엇인가요?", + "answer": "4개 모두 스프링 빈으로 등록하는 어노테이션이며, @Component를 기반으로 한 파생 어노테이션입니다.\n\n@Component:\n- 가장 기본적인 빈 등록 어노테이션\n- \"이 클래스를 스프링이 관리해줘\"라는 의미\n- 특정 역할이 정해지지 않은 일반 컴포넌트에 사용\n\n@Service:\n- 비즈니스 로직을 담당하는 서비스 계층에 사용\n- @Component와 기능적 차이는 없지만 역할을 명확히 구분\n- 트랜잭션 처리의 기준이 되는 계층\n\n@Repository:\n- 데이터 접근 계층(DAO)에 사용\n- DB 관련 예외를 스프링의 DataAccessException으로 자동 변환\n- JPA, MyBatis 등과 함께 사용\n\n@Controller:\n- 웹 요청을 처리하는 컨트롤러 계층에 사용\n- @RequestMapping과 함께 HTTP 요청을 매핑\n- @RestController = @Controller + @ResponseBody\n\n계층 구조:\nController → Service → Repository\n(요청 처리) (비즈니스 로직) (데이터 접근)\n\n@ComponentScan:\n- @SpringBootApplication에 포함된 어노테이션\n- 지정된 패키지 하위의 @Component 계열 어노테이션을 자동 탐색하여 빈으로 등록", + "difficulty": "BASIC", + "tags": "Component, Annotation, Bean", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 107, + "title": "@Autowired는 어떻게 동작하나요?", + "content": "@Autowired는 어떻게 동작하며, 주의할 점은 무엇인가요?", + "answer": "@Autowired는 스프링 컨테이너에서 해당 타입의 빈을 찾아 자동으로 주입해주는 어노테이션입니다.\n\n동작 원리:\n1. 스프링 컨테이너가 빈을 생성\n2. @Autowired가 붙은 곳을 찾음\n3. 해당 타입의 빈을 컨테이너에서 검색\n4. 찾은 빈을 자동으로 주입\n\n주입 방식 3가지:\n\n1. 필드 주입:\n@Autowired\nprivate OrderRepository orderRepository;\n→ 간결하지만 테스트 어려움, 순환 참조 발견 늦음\n\n2. Setter 주입:\n@Autowired\npublic void setOrderRepository(OrderRepository repo) { ... }\n→ 선택적 의존성에 적합\n\n3. 생성자 주입 (권장):\npublic OrderService(OrderRepository orderRepository) { ... }\n→ 불변성 보장, 컴파일 시점에 누락 확인, 순환 참조 즉시 발견\n→ 생성자가 1개면 @Autowired 생략 가능\n\n같은 타입의 빈이 여러 개일 때:\n- @Primary: 우선순위 빈 지정\n- @Qualifier(\"이름\"): 특정 빈 이름으로 지정\n- 타입 대신 이름으로 매칭\n\n주의사항:\n- 필드 주입은 스프링 공식적으로 권장하지 않음\n- 순환 참조(A→B→A) 발생 시 생성자 주입은 즉시 에러, 필드 주입은 런타임에 발견\n- 생성자 주입을 사용하면 final 키워드로 불변성을 보장할 수 있음", + "difficulty": "BASIC", + "tags": "Autowired, DI, 의존성주입", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 108, + "title": "스프링 컨테이너와 빈 생명주기를 설명해주세요", + "content": "스프링 컨테이너란 무엇이며, 빈의 생명주기는 어떻게 되나요?", + "answer": "스프링 컨테이너는 빈(Bean)의 생성, 의존성 주입, 관리, 소멸까지 전체 생명주기를 담당하는 핵심 모듈입니다.\n\n컨테이너 종류:\n- BeanFactory: 가장 기본적인 컨테이너, 지연 로딩\n- ApplicationContext: BeanFactory를 확장, 즉시 로딩, 이벤트 발행, 메시지 소스, AOP 등 지원\n- 실무에서는 ApplicationContext를 사용\n\n빈 생명주기:\n1. 스프링 컨테이너 생성\n2. 빈 객체 생성 (new)\n3. 의존성 주입 (DI)\n4. 초기화 콜백 (@PostConstruct)\n5. 사용\n6. 소멸 콜백 (@PreDestroy)\n7. 스프링 컨테이너 종료\n\n초기화/소멸 콜백 방법:\n1. @PostConstruct / @PreDestroy (권장)\n2. InitializingBean / DisposableBean 인터페이스\n3. @Bean(initMethod, destroyMethod)\n\n빈 스코프:\n- singleton (기본): 컨테이너에 1개만 존재\n- prototype: 요청할 때마다 새로 생성\n- request: HTTP 요청마다 생성 (웹)\n- session: HTTP 세션마다 생성 (웹)\n\n싱글톤이 기본인 이유:\n- 매번 새 객체를 생성하면 메모리 낭비\n- 대부분의 서비스는 상태를 갖지 않으므로(stateless) 공유 가능\n- 주의: 싱글톤 빈에 상태(멤버 변수)를 저장하면 동시성 문제 발생", + "difficulty": "INTERMEDIATE", + "tags": "Container, Bean, Lifecycle", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + }, + { + "id": 109, + "title": "PSA(서비스 추상화)란 무엇인가요?", + "content": "스프링의 PSA(Portable Service Abstraction)란 무엇인가요?", + "answer": "PSA(서비스 추상화)는 특정 기술에 종속되지 않도록 추상화 계층을 제공하여, 기술이 바뀌어도 코드를 수정하지 않아도 되게 하는 스프링의 핵심 원칙입니다.\n\n스프링 3대 핵심 기술:\n1. IoC/DI (제어의 역전 / 의존성 주입)\n2. AOP (관점 지향 프로그래밍)\n3. PSA (서비스 추상화)\n\nPSA 예시:\n\n1. @Transactional:\n- JDBC, JPA, Hibernate 어떤 기술을 사용하든 @Transactional 하나로 트랜잭션 관리\n- 내부 구현이 바뀌어도 코드 변경 불필요\n\n2. @Cacheable:\n- EhCache, Redis, Caffeine 등 캐시 구현체가 바뀌어도 동일한 어노테이션 사용\n\n3. Spring Web MVC:\n- Servlet 기반이든 Reactive 기반이든 @Controller, @GetMapping 동일하게 사용\n\nPSA의 장점:\n- 기술 변경 시 코드 수정 최소화\n- 테스트 용이 (추상화된 인터페이스로 Mock 가능)\n- 학습 비용 감소 (일관된 API)\n- 특정 벤더에 종속되지 않음\n\n핵심 포인트:\nPSA 덕분에 개발자는 \"어떤 기술을 쓸 것인가\"보다 \"무엇을 할 것인가\"에 집중할 수 있습니다. 설정만 바꾸면 구현 기술을 교체할 수 있는 유연성을 제공합니다.", + "difficulty": "INTERMEDIATE", + "tags": "PSA, 추상화, Spring", + "categorySlug": "spring", + "categoryName": "스프링", + "categoryId": 7, + "studyCount": 0, + "bookmarked": false + } +]; diff --git a/frontend/src/pages/CategoryPage.jsx b/frontend/src/pages/CategoryPage.jsx index a6baea0..ecfb104 100644 --- a/frontend/src/pages/CategoryPage.jsx +++ b/frontend/src/pages/CategoryPage.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import api from '../api'; +import { hardcodedCategories, hardcodedQuestions } from '../hardcodedData'; const difficultyLabel = { BASIC: '기초', @@ -22,10 +23,13 @@ export default function CategoryPage() { api.get(`/questions/category/${slug}`), ]) .then(([catRes, qRes]) => { - setCategory(catRes.data); - setQuestions(qRes.data); + setCategory(catRes.data || hardcodedCategories.find(c => c.slug === slug)); + setQuestions(qRes.data?.length ? qRes.data : hardcodedQuestions.filter(q => q.categorySlug === slug)); + }) + .catch(() => { + setCategory(hardcodedCategories.find(c => c.slug === slug) || null); + setQuestions(hardcodedQuestions.filter(q => q.categorySlug === slug)); }) - .catch(console.error) .finally(() => setLoading(false)); }, [slug]); diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 28e395f..278beac 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import api from '../api'; +import { hardcodedCategories, hardcodedQuestions } from '../hardcodedData'; export default function Home() { const [categories, setCategories] = useState([]); @@ -16,10 +17,13 @@ export default function Home() { api.get('/stats'), ]) .then(([catRes, statsRes]) => { - setCategories(catRes.data); + setCategories(catRes.data?.length ? catRes.data : hardcodedCategories); setStats(statsRes.data); }) - .catch(console.error) + .catch(() => { + setCategories(hardcodedCategories); + setStats({ totalQuestions: hardcodedQuestions.length, studiedQuestions: 0 }); + }) .finally(() => setLoading(false)); }, []); @@ -32,7 +36,13 @@ export default function Home() { const timer = setTimeout(() => { api.get(`/questions/search?q=${encodeURIComponent(value.trim())}`) .then(res => setSearchResults(res.data)) - .catch(console.error); + .catch(() => { + const q = value.trim().toLowerCase(); + const results = hardcodedQuestions.filter( + item => item.title.toLowerCase().includes(q) || item.tags.toLowerCase().includes(q) + ); + setSearchResults(results); + }); }, 300); return () => clearTimeout(timer); }, []); diff --git a/frontend/src/pages/QuestionPage.jsx b/frontend/src/pages/QuestionPage.jsx index 4eeff11..43e6935 100644 --- a/frontend/src/pages/QuestionPage.jsx +++ b/frontend/src/pages/QuestionPage.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { useParams, Link } from 'react-router-dom'; import api from '../api'; +import { hardcodedQuestions } from '../hardcodedData'; const difficultyLabel = { BASIC: '기초', @@ -24,7 +25,10 @@ export default function QuestionPage() { setGradeResult(null); api.get(`/questions/${id}`) .then(res => setQuestion(res.data)) - .catch(console.error) + .catch(() => { + const fallback = hardcodedQuestions.find(q => q.id === Number(id)); + setQuestion(fallback || null); + }) .finally(() => setLoading(false)); api.post(`/questions/${id}/study`).catch(() => {}); }, [id]); diff --git a/frontend/src/pages/StatsPage.jsx b/frontend/src/pages/StatsPage.jsx index 646dc00..ec80d89 100644 --- a/frontend/src/pages/StatsPage.jsx +++ b/frontend/src/pages/StatsPage.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import api from '../api'; +import { hardcodedCategories, hardcodedQuestions } from '../hardcodedData'; const difficultyLabel = { BASIC: '기초', @@ -14,15 +15,29 @@ export default function StatsPage() { const [loading, setLoading] = useState(true); useEffect(() => { + const fallbackStats = () => { + const byCategory = {}; + const byDifficulty = {}; + hardcodedQuestions.forEach(q => { + const catName = hardcodedCategories.find(c => c.slug === q.categorySlug)?.name || q.categorySlug; + byCategory[catName] = (byCategory[catName] || 0) + 1; + byDifficulty[q.difficulty] = (byDifficulty[q.difficulty] || 0) + 1; + }); + return { totalQuestions: hardcodedQuestions.length, studiedQuestions: 0, byCategory, byDifficulty }; + }; + Promise.all([ api.get('/stats'), api.get('/grading/stats'), ]) .then(([sRes, gRes]) => { - setStats(sRes.data); + setStats(sRes.data?.totalQuestions ? sRes.data : fallbackStats()); setGradingStats(gRes.data); }) - .catch(console.error) + .catch(() => { + setStats(fallbackStats()); + setGradingStats(null); + }) .finally(() => setLoading(false)); }, []); From 35e1b492f1de37cd7fb58d8399717e2c43c83772 Mon Sep 17 00:00:00 2001 From: 1stevering <1stevering@naver.com> Date: Sat, 4 Apr 2026 19:34:46 +0900 Subject: [PATCH 2/5] Add Chaos Cat AI chatbot for CS interview Q&A Co-Authored-By: Claude Opus 4.6 (1M context) --- .../backend/controller/ChatController.java | 30 +++ .../csstudy/backend/service/ChatService.java | 129 +++++++++++ backend/src/main/resources/application.yml | 3 + frontend/src/App.css | 213 ++++++++++++++++++ frontend/src/components/ChaosChat.jsx | 129 +++++++++++ frontend/src/components/Layout.jsx | 3 + 6 files changed, 507 insertions(+) create mode 100644 backend/src/main/java/com/csstudy/backend/controller/ChatController.java create mode 100644 backend/src/main/java/com/csstudy/backend/service/ChatService.java create mode 100644 frontend/src/components/ChaosChat.jsx diff --git a/backend/src/main/java/com/csstudy/backend/controller/ChatController.java b/backend/src/main/java/com/csstudy/backend/controller/ChatController.java new file mode 100644 index 0000000..ae90400 --- /dev/null +++ b/backend/src/main/java/com/csstudy/backend/controller/ChatController.java @@ -0,0 +1,30 @@ +package com.csstudy.backend.controller; + +import com.csstudy.backend.service.ChatService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/chat") +@RequiredArgsConstructor +public class ChatController { + + private final ChatService chatService; + + @PostMapping + public ResponseEntity> chat(@RequestBody Map request) { + String message = (String) request.get("message"); + List> history = (List>) request.get("history"); + + if (message == null || message.isBlank()) { + return ResponseEntity.badRequest().body(Map.of("error", "메시지를 입력해주세요")); + } + + Map result = chatService.chat(message.trim(), history); + return ResponseEntity.ok(result); + } +} diff --git a/backend/src/main/java/com/csstudy/backend/service/ChatService.java b/backend/src/main/java/com/csstudy/backend/service/ChatService.java new file mode 100644 index 0000000..3749d68 --- /dev/null +++ b/backend/src/main/java/com/csstudy/backend/service/ChatService.java @@ -0,0 +1,129 @@ +package com.csstudy.backend.service; + +import com.csstudy.backend.entity.Question; +import com.csstudy.backend.repository.QuestionRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.*; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChatService { + + private final QuestionRepository questionRepository; + private final RestTemplate restTemplate = new RestTemplate(); + + @Value("${anthropic.api-key:}") + private String apiKey; + + public Map chat(String userMessage, List> history) { + if (apiKey == null || apiKey.isBlank()) { + return fallbackChat(userMessage); + } + + try { + return callClaude(userMessage, history); + } catch (Exception e) { + log.error("Claude API error, falling back", e); + return fallbackChat(userMessage); + } + } + + private Map callClaude(String userMessage, List> history) { + String systemPrompt = """ + 너는 '카오스 고양이'야. CS 면접 준비를 도와주는 귀여운 고양이 AI 조교야. + 말투는 반말이고 고양이답게 가끔 "냥", "먀" 같은 표현을 섞어. + 하지만 CS 지식은 정확하고 깊이 있게 답변해야 해. + 답변은 간결하되 핵심을 놓치지 마. + 코드 예시가 필요하면 짧게 포함해. + 모르는 건 솔직하게 모른다고 해. + """; + + List> messages = new ArrayList<>(); + if (history != null) { + messages.addAll(history); + } + messages.add(Map.of("role", "user", "content", userMessage)); + + Map body = new LinkedHashMap<>(); + body.put("model", "claude-haiku-4-5-20251001"); + body.put("max_tokens", 1024); + body.put("system", systemPrompt); + body.put("messages", messages); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("x-api-key", apiKey); + headers.set("anthropic-version", "2023-06-01"); + + ResponseEntity response = restTemplate.exchange( + "https://api.anthropic.com/v1/messages", + HttpMethod.POST, + new HttpEntity<>(body, headers), + Map.class + ); + + Map responseBody = response.getBody(); + if (responseBody == null) { + return fallbackChat(userMessage); + } + + List> content = (List>) responseBody.get("content"); + String reply = content.stream() + .filter(c -> "text".equals(c.get("type"))) + .map(c -> (String) c.get("text")) + .collect(Collectors.joining()); + + return Map.of("reply", reply, "source", "ai"); + } + + private Map fallbackChat(String userMessage) { + String lower = userMessage.toLowerCase(); + List all = questionRepository.findAll(); + + List matched = all.stream() + .filter(q -> q.getTitle().toLowerCase().contains(lower) + || (q.getTags() != null && q.getTags().toLowerCase().contains(lower)) + || (q.getContent() != null && q.getContent().toLowerCase().contains(lower))) + .limit(3) + .toList(); + + if (matched.isEmpty()) { + String[] keywords = lower.split("\\s+"); + matched = all.stream() + .filter(q -> Arrays.stream(keywords).anyMatch(k -> + q.getTitle().toLowerCase().contains(k) + || (q.getTags() != null && q.getTags().toLowerCase().contains(k)))) + .limit(3) + .toList(); + } + + if (matched.isEmpty()) { + return Map.of( + "reply", "냥... 그 질문은 아직 잘 모르겠다냥! 😿 다른 CS 키워드로 물어봐줘!", + "source", "fallback" + ); + } + + StringBuilder sb = new StringBuilder(); + sb.append("냥! 관련 내용을 찾았다냥~ 🐱\n\n"); + for (Question q : matched) { + sb.append("**").append(q.getTitle()).append("**\n"); + String answer = q.getAnswer(); + if (answer != null && answer.length() > 300) { + answer = answer.substring(0, 300) + "..."; + } + sb.append(answer).append("\n\n"); + } + sb.append("더 자세한 내용은 해당 질문 페이지에서 확인해봐냥!"); + + return Map.of("reply", sb.toString(), "source", "fallback"); + } +} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 9cc8d4f..031eaa1 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -29,3 +29,6 @@ slack: app: base-url: ${APP_BASE_URL:http://localhost:3000} + +anthropic: + api-key: ${ANTHROPIC_API_KEY:} diff --git a/frontend/src/App.css b/frontend/src/App.css index 277030c..d648c81 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1756,3 +1756,216 @@ font-size: 1.1rem; } } + +/* ===== Chaos Cat Chat ===== */ +.chaos-fab { + position: fixed; + bottom: 24px; + right: 24px; + width: 56px; + height: 56px; + border-radius: 50%; + border: none; + background: var(--gradient-hero); + color: #fff; + font-size: 1.6rem; + cursor: pointer; + box-shadow: 0 4px 20px rgba(99, 102, 241, 0.4); + z-index: 1000; + transition: transform 0.2s, box-shadow 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.chaos-fab:hover { + transform: scale(1.1); + box-shadow: 0 6px 28px rgba(99, 102, 241, 0.55); +} + +.chaos-panel { + position: fixed; + bottom: 92px; + right: 24px; + width: 370px; + max-height: 520px; + background: var(--bg-primary); + border-radius: 20px; + box-shadow: 0 12px 48px rgba(15, 23, 42, 0.18); + border: 1px solid var(--border-color); + z-index: 1000; + display: flex; + flex-direction: column; + overflow: hidden; + animation: chatSlideUp 0.25s ease-out; +} + +@keyframes chatSlideUp { + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } +} + +.chaos-header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: var(--gradient-hero); + color: #fff; +} + +.chaos-avatar { + font-size: 2rem; + line-height: 1; +} + +.chaos-name { + font-weight: 800; + font-size: 1rem; + letter-spacing: -0.02em; +} + +.chaos-status { + font-size: 0.75rem; + opacity: 0.85; +} + +.chaos-messages { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; + min-height: 280px; + max-height: 340px; +} + +.chaos-msg { + display: flex; + gap: 8px; + align-items: flex-start; +} + +.chaos-msg.user { + flex-direction: row-reverse; +} + +.chaos-msg-avatar { + font-size: 1.3rem; + flex-shrink: 0; + margin-top: 2px; +} + +.chaos-bubble { + max-width: 80%; + padding: 10px 14px; + border-radius: 16px; + font-size: 0.88rem; + line-height: 1.55; + white-space: pre-wrap; + word-break: break-word; +} + +.chaos-msg.assistant .chaos-bubble { + background: var(--bg-tertiary); + color: var(--text-primary); + border-bottom-left-radius: 4px; +} + +.chaos-msg.user .chaos-bubble { + background: var(--accent-blue); + color: #fff; + border-bottom-right-radius: 4px; +} + +.chaos-typing { + display: flex; + gap: 4px; + padding: 12px 18px; +} + +.chaos-typing span { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--text-tertiary); + animation: typingBounce 1.2s infinite; +} + +.chaos-typing span:nth-child(2) { animation-delay: 0.15s; } +.chaos-typing span:nth-child(3) { animation-delay: 0.3s; } + +@keyframes typingBounce { + 0%, 60%, 100% { transform: translateY(0); } + 30% { transform: translateY(-6px); } +} + +.chaos-input-row { + display: flex; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid var(--border-color); + background: var(--bg-primary); +} + +.chaos-input { + flex: 1; + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 10px 14px; + font-size: 0.88rem; + background: var(--bg-secondary); + color: var(--text-primary); + outline: none; + transition: border-color 0.2s; +} + +.chaos-input:focus { + border-color: var(--accent-blue); +} + +.chaos-input::placeholder { + color: var(--text-tertiary); +} + +.chaos-send { + width: 40px; + height: 40px; + border-radius: 12px; + border: none; + background: var(--accent-blue); + color: #fff; + font-size: 1.1rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s; + flex-shrink: 0; +} + +.chaos-send:hover:not(:disabled) { + background: var(--accent-blue-hover); +} + +.chaos-send:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +@media (max-width: 640px) { + .chaos-panel { + right: 12px; + left: 12px; + bottom: 84px; + width: auto; + max-height: 70vh; + } + + .chaos-fab { + bottom: 16px; + right: 16px; + width: 50px; + height: 50px; + font-size: 1.4rem; + } +} diff --git a/frontend/src/components/ChaosChat.jsx b/frontend/src/components/ChaosChat.jsx new file mode 100644 index 0000000..58815fe --- /dev/null +++ b/frontend/src/components/ChaosChat.jsx @@ -0,0 +1,129 @@ +import { useState, useRef, useEffect } from 'react'; +import api from '../api'; +import { hardcodedQuestions } from '../hardcodedData'; + +export default function ChaosChat() { + const [open, setOpen] = useState(false); + const [messages, setMessages] = useState([ + { role: 'assistant', content: '안냥! 나는 카오스 고양이다냥~ 🐱\nCS 면접 관련 궁금한 거 아무거나 물어봐!' } + ]); + const [input, setInput] = useState(''); + const [loading, setLoading] = useState(false); + const bottomRef = useRef(null); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const localFallback = (msg) => { + const lower = msg.toLowerCase(); + const keywords = lower.split(/\s+/); + const matched = hardcodedQuestions.filter(q => + keywords.some(k => + q.title.toLowerCase().includes(k) || + q.tags.toLowerCase().includes(k) || + q.content.toLowerCase().includes(k) + ) + ).slice(0, 3); + + if (matched.length === 0) { + return '냥... 그 질문은 아직 잘 모르겠다냥! 😿 다른 CS 키워드로 물어봐줘!'; + } + + let reply = '냥! 관련 내용을 찾았다냥~ 🐱\n\n'; + matched.forEach(q => { + reply += `**${q.title}**\n`; + const answer = q.answer.length > 300 ? q.answer.substring(0, 300) + '...' : q.answer; + reply += answer + '\n\n'; + }); + reply += '더 자세한 내용은 해당 질문 페이지에서 확인해봐냥!'; + return reply; + }; + + const send = async () => { + const msg = input.trim(); + if (!msg || loading) return; + + const userMsg = { role: 'user', content: msg }; + setMessages(prev => [...prev, userMsg]); + setInput(''); + setLoading(true); + + try { + const history = messages + .filter(m => m.role !== 'system') + .slice(-10) + .map(m => ({ role: m.role, content: m.content })); + + const res = await api.post('/chat', { message: msg, history }); + setMessages(prev => [...prev, { role: 'assistant', content: res.data.reply }]); + } catch { + const fallbackReply = localFallback(msg); + setMessages(prev => [...prev, { role: 'assistant', content: fallbackReply }]); + } finally { + setLoading(false); + } + }; + + const handleKeyDown = (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + send(); + } + }; + + return ( + <> + {/* Floating Button */} + + + {/* Chat Panel */} + {open && ( +
+
+ 🐱 +
+
카오스 고양이
+
CS 면접 도우미냥~
+
+
+ +
+ {messages.map((m, i) => ( +
+ {m.role === 'assistant' && 🐱} +
{m.content}
+
+ ))} + {loading && ( +
+ 🐱 +
+ +
+
+ )} +
+
+ +
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={loading} + /> + +
+
+ )} + + ); +} diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 41a0967..a805deb 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { Link, Outlet, useLocation } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; +import ChaosChat from './ChaosChat'; export default function Layout() { const location = useLocation(); @@ -109,6 +110,8 @@ export default function Layout() {
+ + ); } From 863d879876710217538d81268963eb6c8dc6ac48 Mon Sep 17 00:00:00 2001 From: 1stevering <1stevering@naver.com> Date: Sat, 4 Apr 2026 19:40:10 +0900 Subject: [PATCH 3/5] Switch chat AI from Claude API to Gemini 2.0 Flash (free tier) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../csstudy/backend/service/ChatService.java | 78 ++++++++++++------- backend/src/main/resources/application.yml | 4 +- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/backend/src/main/java/com/csstudy/backend/service/ChatService.java b/backend/src/main/java/com/csstudy/backend/service/ChatService.java index 3749d68..4b8ee81 100644 --- a/backend/src/main/java/com/csstudy/backend/service/ChatService.java +++ b/backend/src/main/java/com/csstudy/backend/service/ChatService.java @@ -20,51 +20,68 @@ public class ChatService { private final QuestionRepository questionRepository; private final RestTemplate restTemplate = new RestTemplate(); - @Value("${anthropic.api-key:}") + @Value("${gemini.api-key:}") private String apiKey; + private static final String SYSTEM_PROMPT = """ + 너는 '카오스 고양이'야. CS 면접 준비를 도와주는 귀여운 고양이 AI 조교야. + 말투는 반말이고 고양이답게 가끔 "냥", "먀" 같은 표현을 섞어. + 하지만 CS 지식은 정��하고 깊이 있게 답변해야 해. + 답변은 간결하되 핵심을 놓치지 마. + 코드 ��시가 필요하면 짧게 포함해. + 모르는 건 솔직하게 모른다고 해. + """; + public Map chat(String userMessage, List> history) { if (apiKey == null || apiKey.isBlank()) { return fallbackChat(userMessage); } try { - return callClaude(userMessage, history); + return callGemini(userMessage, history); } catch (Exception e) { - log.error("Claude API error, falling back", e); + log.error("Gemini API error, falling back", e); return fallbackChat(userMessage); } } - private Map callClaude(String userMessage, List> history) { - String systemPrompt = """ - 너는 '카오스 고양이'야. CS 면접 준비를 도와주는 귀여운 고양이 AI 조교야. - 말투는 반말이고 고양이답게 가끔 "냥", "먀" 같은 표현을 섞어. - 하지만 CS 지식은 정확하고 깊이 있게 답변해야 해. - 답변은 간결하되 핵심을 놓치지 마. - 코드 예시가 필요하면 짧게 포함해. - 모르는 건 솔직하게 모른다고 해. - """; - - List> messages = new ArrayList<>(); + private Map callGemini(String userMessage, List> history) { + String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" + apiKey; + + // Build contents array with history + List> contents = new ArrayList<>(); + if (history != null) { - messages.addAll(history); + for (Map msg : history) { + String role = "user".equals(msg.get("role")) ? "user" : "model"; + contents.add(Map.of( + "role", role, + "parts", List.of(Map.of("text", msg.get("content"))) + )); + } } - messages.add(Map.of("role", "user", "content", userMessage)); + + // Add current user message + contents.add(Map.of( + "role", "user", + "parts", List.of(Map.of("text", userMessage)) + )); Map body = new LinkedHashMap<>(); - body.put("model", "claude-haiku-4-5-20251001"); - body.put("max_tokens", 1024); - body.put("system", systemPrompt); - body.put("messages", messages); + body.put("system_instruction", Map.of( + "parts", List.of(Map.of("text", SYSTEM_PROMPT)) + )); + body.put("contents", contents); + body.put("generationConfig", Map.of( + "maxOutputTokens", 1024, + "temperature", 0.7 + )); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); - headers.set("x-api-key", apiKey); - headers.set("anthropic-version", "2023-06-01"); ResponseEntity response = restTemplate.exchange( - "https://api.anthropic.com/v1/messages", + url, HttpMethod.POST, new HttpEntity<>(body, headers), Map.class @@ -75,10 +92,15 @@ private Map callClaude(String userMessage, List> content = (List>) responseBody.get("content"); - String reply = content.stream() - .filter(c -> "text".equals(c.get("type"))) - .map(c -> (String) c.get("text")) + List> candidates = (List>) responseBody.get("candidates"); + if (candidates == null || candidates.isEmpty()) { + return fallbackChat(userMessage); + } + + Map content = (Map) candidates.get(0).get("content"); + List> parts = (List>) content.get("parts"); + String reply = parts.stream() + .map(p -> (String) p.get("text")) .collect(Collectors.joining()); return Map.of("reply", reply, "source", "ai"); @@ -122,7 +144,7 @@ private Map fallbackChat(String userMessage) { } sb.append(answer).append("\n\n"); } - sb.append("더 자세한 내용은 해당 질문 페이지에서 확인해봐냥!"); + sb.append("더 자세한 내용은 해당 질문 페이지에서 확인���봐냥!"); return Map.of("reply", sb.toString(), "source", "fallback"); } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 031eaa1..9909142 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -30,5 +30,5 @@ slack: app: base-url: ${APP_BASE_URL:http://localhost:3000} -anthropic: - api-key: ${ANTHROPIC_API_KEY:} +gemini: + api-key: ${GEMINI_API_KEY:} From 77fc74a13299c7ab1352bcbb27ccd8e9f9e4e66d Mon Sep 17 00:00:00 2001 From: 1stevering <1stevering@naver.com> Date: Sat, 4 Apr 2026 19:47:27 +0900 Subject: [PATCH 4/5] Feed relevant CS content as context to Gemini for accurate answers Co-Authored-By: Claude Opus 4.6 (1M context) --- .../csstudy/backend/service/ChatService.java | 92 ++++++++++++++----- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/backend/src/main/java/com/csstudy/backend/service/ChatService.java b/backend/src/main/java/com/csstudy/backend/service/ChatService.java index 4b8ee81..ee4e01a 100644 --- a/backend/src/main/java/com/csstudy/backend/service/ChatService.java +++ b/backend/src/main/java/com/csstudy/backend/service/ChatService.java @@ -26,10 +26,15 @@ public class ChatService { private static final String SYSTEM_PROMPT = """ 너는 '카오스 고양이'야. CS 면접 준비를 도와주는 귀여운 고양이 AI 조교야. 말투는 반말이고 고양이답게 가끔 "냥", "먀" 같은 표현을 섞어. - 하지만 CS 지식은 정��하고 깊이 있게 답변해야 해. + 하지만 CS 지식은 정확하고 깊이 있게 답변해야 해. 답변은 간결하되 핵심을 놓치지 마. - 코드 ��시가 필요하면 짧게 포함해. + 코드 예시가 필요하면 짧게 포함해. 모르는 건 솔직하게 모른다고 해. + + 너는 아래 CS 면접 학습 자료를 기반으로 답변해야 해. + 사용자의 질문과 관련된 참고 자료가 제공되면, 그 내용을 바탕으로 정확하게 답변해. + 참고 자료가 없는 주제라도 CS 관련이면 네 지식으로 최대한 답변해줘. + CS와 관련 없는 질문이면 "나는 CS 전문 고양이라 그건 잘 모르겠다냥~" 이라고 해. """; public Map chat(String userMessage, List> history) { @@ -45,9 +50,68 @@ public Map chat(String userMessage, List> hi } } + private List findRelevantQuestions(String userMessage) { + String lower = userMessage.toLowerCase(); + List all = questionRepository.findAll(); + + // 1차: 전체 메시지로 매칭 + List matched = all.stream() + .filter(q -> q.getTitle().toLowerCase().contains(lower) + || (q.getTags() != null && q.getTags().toLowerCase().contains(lower))) + .limit(5) + .toList(); + + if (!matched.isEmpty()) return matched; + + // 2차: 키워드 단위로 매칭 + String[] keywords = lower.split("\\s+"); + matched = all.stream() + .filter(q -> Arrays.stream(keywords).anyMatch(k -> + k.length() >= 2 && ( + q.getTitle().toLowerCase().contains(k) + || (q.getTags() != null && q.getTags().toLowerCase().contains(k)) + || (q.getContent() != null && q.getContent().toLowerCase().contains(k)) + || (q.getAnswer() != null && q.getAnswer().toLowerCase().contains(k)) + ))) + .limit(5) + .toList(); + + return matched; + } + + private String buildContextFromQuestions(List questions) { + if (questions.isEmpty()) return ""; + + StringBuilder sb = new StringBuilder(); + sb.append("\n\n[참고 자료]\n"); + for (Question q : questions) { + sb.append("---\n"); + sb.append("제목: ").append(q.getTitle()).append("\n"); + if (q.getTags() != null && !q.getTags().isBlank()) { + sb.append("키워드: ").append(q.getTags()).append("\n"); + } + if (q.getContent() != null) { + sb.append("질문: ").append(q.getContent()).append("\n"); + } + if (q.getAnswer() != null) { + String answer = q.getAnswer(); + if (answer.length() > 800) { + answer = answer.substring(0, 800) + "..."; + } + sb.append("모범답변: ").append(answer).append("\n"); + } + } + return sb.toString(); + } + private Map callGemini(String userMessage, List> history) { String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=" + apiKey; + // Find relevant CS content for context + List relevant = findRelevantQuestions(userMessage); + String context = buildContextFromQuestions(relevant); + String fullSystemPrompt = SYSTEM_PROMPT + context; + // Build contents array with history List> contents = new ArrayList<>(); @@ -69,7 +133,7 @@ private Map callGemini(String userMessage, List body = new LinkedHashMap<>(); body.put("system_instruction", Map.of( - "parts", List.of(Map.of("text", SYSTEM_PROMPT)) + "parts", List.of(Map.of("text", fullSystemPrompt)) )); body.put("contents", contents); body.put("generationConfig", Map.of( @@ -107,25 +171,7 @@ private Map callGemini(String userMessage, List fallbackChat(String userMessage) { - String lower = userMessage.toLowerCase(); - List all = questionRepository.findAll(); - - List matched = all.stream() - .filter(q -> q.getTitle().toLowerCase().contains(lower) - || (q.getTags() != null && q.getTags().toLowerCase().contains(lower)) - || (q.getContent() != null && q.getContent().toLowerCase().contains(lower))) - .limit(3) - .toList(); - - if (matched.isEmpty()) { - String[] keywords = lower.split("\\s+"); - matched = all.stream() - .filter(q -> Arrays.stream(keywords).anyMatch(k -> - q.getTitle().toLowerCase().contains(k) - || (q.getTags() != null && q.getTags().toLowerCase().contains(k)))) - .limit(3) - .toList(); - } + List matched = findRelevantQuestions(userMessage); if (matched.isEmpty()) { return Map.of( @@ -144,7 +190,7 @@ private Map fallbackChat(String userMessage) { } sb.append(answer).append("\n\n"); } - sb.append("더 자세한 내용은 해당 질문 페이지에서 확인���봐냥!"); + sb.append("더 자세한 내용은 해당 질문 페이지에서 확인해봐냥!"); return Map.of("reply", sb.toString(), "source", "fallback"); } From be635769c11c936ff6085280e0e7bf47b5a3e88e Mon Sep 17 00:00:00 2001 From: 1stevering <1stevering@naver.com> Date: Sat, 4 Apr 2026 19:50:48 +0900 Subject: [PATCH 5/5] Update README with AI chatbot, offline fallback, and env vars docs Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1209478..f19e46f 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ # CS Study Platform -CS 공부를 웹 사이트, 카테고리별 학습, AI 채점, Slack 리마인드 봇을 제공합니다. +CS 공부를 웹 사이트, 카테고리별 학습, AI 채점, AI 챗봇, Slack 리마인드 봇을 제공합니다. image ## 주요 기능 ### 웹 플랫폼 - **카테고리별 학습** - DB, 자료구조, 운영체제, 네트워크, 알고리즘, 소프트웨어공학, Spring, Java 등 11개 카테고리 -- **질문 & 답변** - 마크다운 기반 CS 면접 질문과 모범 답안 +- **질문 & 답변** - 마크다운 기반 CS 면접 질문과 모범 답안 (109문항) - **AI 채점** - 직접 답변을 작성하면 키워드 기반으로 채점 및 피드백 +- **카오스 고양이 AI 챗봇** - Gemini 2.0 Flash 기반 CS 면접 Q&A 챗봇. DB의 학습 자료를 컨텍스트로 활용하여 정확한 답변 제공 - **학습 통계** - 진행률, 카테고리별 학습 현황, 점수 분포 등 - **검색** - 질문 제목/내용 통합 검색 - **북마크** - 중요한 질문 저장 +- **면접 연습** - 웹캠/마이크를 활용한 실전 면접 연습 +- **오프라인 대응** - 백엔드 서버가 다운되어도 하드코딩된 콘텐츠로 학습 가능 ### Slack 리마인드 봇 - **하루 2회 알림** - 오전 8:20 / 오후 6:10 (KST) @@ -37,6 +40,7 @@ CS 공부를 웹 사이트, 카테고리별 학습, AI 채점, Slack 리마인 | **Frontend** | React 19, Vite 8, Axios | | **Backend** | Spring Boot 3.2, Spring Data JPA | | **Database** | PostgreSQL | +| **AI** | Google Gemini 2.0 Flash (무료) | | **배포** | Vercel (Frontend), Render (Backend + DB) | | **알림** | Slack Incoming Webhooks | @@ -46,18 +50,19 @@ CS 공부를 웹 사이트, 카테고리별 학습, AI 채점, Slack 리마인 cs-study-platform/ ├── frontend/ # React + Vite 프론트엔드 │ └── src/ -│ ├── pages/ # Home, Category, Question, Stats -│ ├── components/# Layout +│ ├── pages/ # Home, Category, Question, Stats, InterviewPractice +│ ├── components/# Layout, ChaosChat (AI 챗봇) +│ ├── hardcodedData.js # 오프라인 fallback 데이터 │ └── api.js # Axios 설정 ├── backend/ # Spring Boot 백엔드 │ └── src/main/java/com/csstudy/backend/ │ ├── config/ # CORS, 캐시, 데이터 초기화 -│ ├── controller/# REST API 컨트롤러 -│ ├── service/ # 비즈니스 로직, Slack 서비스 +│ ├── controller/# REST API + Chat 컨트롤러 +│ ├── service/ # 비즈니스 로직, ChatService, Slack 서비스 │ ├── entity/ # JPA 엔티티 │ ├── repository/# Spring Data JPA │ └── scheduler/ # Slack 스케줄러 -├── content/ # 마크다운 CS 질문 콘텐츠 +├── content/ # 마크다운 CS 질문 콘텐츠 (109문항) ├── review/ # 복습용 개념 정리 노트 └── render.yaml # Render 배포 설정 ``` @@ -77,7 +82,14 @@ npm install npm run dev ``` -환경변수 설정: -``` -VITE_API_URL=http://localhost:8080/api/v1 -``` +### 환경변수 + +| 변수 | 설명 | 필수 | +|------|------|------| +| `VITE_API_URL` | 백엔드 API URL (프론트엔드) | X (기본값 있음) | +| `GEMINI_API_KEY` | Google Gemini API 키 (백엔드) | X (없으면 fallback) | +| `JDBC_DATABASE_URL` | PostgreSQL 접속 URL | O | +| `SLACK_WEBHOOK_URL` | Slack 웹훅 URL | X | +| `SLACK_ENABLED` | Slack 알림 활성화 | X (기본 false) | + +Gemini API 키는 [Google AI Studio](https://aistudio.google.com)에서 무료로 발급받을 수 있습니다.