데이터베이스 기초
🎯 핵심 질문
왜 Excel 쿼리는 10초가 걸리는데, 쇼핑몰 검색은 0.01초밖에 안 걸릴까? 데이터가 "수천 건"에서 "10억 건"으로, "1인 사용"에서 "수천만 명의 동시 접속"으로 변하면 Excel로는 부족합니다. 데이터베이스는 바로 이 문제를 해결하기 위해 탄생했습니다 — 대량의 데이터와 높은 동시성을 처리하는 "슈퍼 Excel"입니다. 이 장에서는 데이터베이스의 핵심 원리를 처음부터 알기 쉽게 설명합니다.
1. 데이터베이스 도입 동기
1.1 작은 서점에서 쇼핑몰까지: 데이터 규모의 변화
작은 서점을 운영한다고 상상해 보세요. 하루에 몇 권의 책을 팔고, 노트에 기록합니다:
2024-01-15: 홍길동이 《백년의 고독》을 구매, 59위안
2024-01-16: 이순신이 《삶》을 구매, 39위안이때는 노트로 충분합니다. 하지만 서점이 "아마존"이 되어 하루에 백만 건의 주문이 쏟아지면 문제가 생깁니다:
- 데이터량: 수십 줄이 아니라 수억 줄
- 동시 접속: 한 명이 조회하는 것이 아니라 수천만 명이 동시에 접속
- 데이터 연관: 주문은 사용자, 상품, 재고, 물류와 연결... 복잡한 관계를 효율적으로 관리해야 함
- 데이터 안전: 정전으로 모든 주문을 잃어서는 안 됨
📓 Excel/노트
- 개인이나 소규모 팀에 적합
- 데이터량: 수천~수만 행
- 1인 사용, 순차 접근
- 수동 검색, 속도가 느림
🗄️ 데이터베이스
- 엔터프라이즈급 애플리케이션에 적합
- 데이터량: 수억 건 이상
- 수천만 명이 동시에 온라인 접속
- 밀리초 단위의 쿼리 속도
이것이 "데이터베이스"가 해결하는 문제입니다: 대량의 데이터를 효율적으로 저장하고, 빠르게 조회하며, 안전하게 관리하려면 어떻게 해야 하는가?
1.2 실제 실패 사례: 왜 Excel로 사용자 데이터를 저장하면 안 되는가
"내 프로젝트는 사용자가 몇 만 명뿐이라 Excel로 충분하지 않나요?"라고 말할 수 있습니다. 실제 이야기를 들려드리겠습니다.
김 대리의 창업 실패기
김 대리가 소셜 앱을 창업했는데, 초기에는 사용자가 많지 않아 Excel로 사용자 정보(이름, 전화번호, 가입 시간 등)를 저장했습니다. 매일 Excel를 내보내서 사용자 증가를 통계내고, 모든 게 정상이었습니다.
사용자가 10만 명을 돌파하자 문제가 나타나기 시작했습니다:
- Excel를 여는 데 5분 대기
- "서울 사용자"를 필터링하는 데 한참 멈춤
- 한 번은 Excel 파일이 손상되어 수천 명의 사용자 데이터가 영구적으로 유실
가장 치명적인 것은 "특정 사용자의 모든 주문 보기" 기능을 구현하려 했는데 — 사용자 정보와 주문이 서로 다른 Excel 시트에 있어, 수동으로 복사-붙여넣기만 가능했고 매번 30분이 걸렸습니다.
선배에게 조언을 구하자, 선배는 한 번 보고 웃으며 말했습니다. "네가 필요한 건 Excel가 아니라 데이터베이스야."
데이터베이스로 전환한 후 모든 것이 바뀌었습니다:
- "서울 사용자" 조회는 0.01초밖에 안 걸림
- "관계"를 통해 사용자와 주문이 자동으로 연결되어, SQL 문 하나로 해결
- 데이터가 자동 백업되어 파일 손상 걱정 끝
김 대리는 이道理를 깨달았습니다: 데이터가 적을 때는 아무거나 써도 되지만, 데이터가 커지면 Excel은 재앙입니다.
💡 핵심 교훈
데이터베이스는 "더 복잡한 Excel"가 아니라 완전히 다른 설계 철학입니다:
- Excel: 소규모 데이터, 1인 사용을 위해 설계
- 데이터베이스: 대규모 데이터, 높은 동시성, 복잡한 연관을 위해 설계
적절한 도구를 선택하면 시스템 성능을 수천 배에서 수만 배까지 향상시킬 수 있습니다.
2. 핵심 개념: 테이블, 행, 열, 기본키
🤔 이 개념들이 데이터베이스와 무슨 관련이 있나요?
테이블, 행, 열, 기본키는 데이터베이스의 "블록"입니다.
집을 짓는다고 상상해 보세요:
- 테이블 = 하나의 방(한 종류의 데이터를 저장)
- 행 = 방 안의 상자 하나(하나의 완전한 기록)
- 열 = 상자의 라벨(이름, 나이 등)
- 기본키 = 상자의 고유 번호(절대 중복되지 않음)
이 기초 개념을 이해해야 데이터가 어떻게 구성되어 있는지 알 수 있습니다.
데이터베이스를 깊이 배우기 전에 먼저 이 핵심 개념들을 확실히 이해해야 합니다. 이해를 돕기 위해 도서관에 비유해 보겠습니다.
2.1 도서관 비유로 데이터베이스 구조 이해하기
도서관에 들어간다고 상상해 보세요. 그 안의 조직은 데이터베이스와 놀라울 정도로 비슷합니다:
| 개념 | 📚 도서관 비유 | 실제 역할 | 구체적 예시 |
|---|---|---|---|
| 데이터베이스(Database) | 도서관 전체 | 모든 데이터를 저장하는 컨테이너 | 이커머스 웹사이트의 데이터베이스 |
| 테이블(Table) | 하나의 책장 | 같은 종류의 데이터 집합 | 사용자 테이블, 상품 테이블, 주문 테이블 |
| 열(Column) | 책등의 라벨 | 데이터의 속성(필드) | 이름, 나이, 전화번호 |
| 행(Row) | 책장의 각 책 | 하나의 구체적인 데이터 기록 | "홍길동, 25세, 서울" |
| 기본키(Primary Key) | 각 책의 ISBN 번호 | 각 행을 유일하게 식별하는 ID | user_id = 1001 |
실제 예시 보기: 사용자 테이블(users)
| user_id (기본키) | name | age | city | |
|---|---|---|---|---|
| 1001 | 홍길동 | 25 | 서울 | hong@example.com |
| 1002 | 이순신 | 30 | 부산 | lee@example.com |
| 1003 | 강감찬 | 28 | 서울 | kang@example.com |
- 테이블:
users(모든 사용자 데이터 저장) - 열:
user_id,name,age,city,email(각 사용자의 속성) - 행: 각 행은 한 명의 사용자(예: "홍길동, 25세, 서울")
- 기본키:
user_id(1001, 1002, 1003, 절대 중복되지 않음)
2.2 기본키(Primary Key): 데이터의 "고유 식별자"
📖 기본키란?
기본키는 테이블에서 각 행의 고유 식별자로, 주민등록번호와 같습니다.
핵심 특징:
- 유일성: 절대 중복되지 않음(같은 주민등록번호를 가진 두 사람은 없음)
- 비어 있지 않음: 반드시 값이 있어야 함("주민등록번호가 없는" 사람은 있을 수 없음)
- 불변성: 한 번 설정되면 수정되지 않음(주민등록번호는 변하지 않음)
일반적 방법:
- 자동 증가 정수 사용: 1, 2, 3, 4...
- UUID(전 세계적으로 유일한 식별자) 사용:
550e8400-e29b-41d4-a716-446655440000
왜 기본키가 필요할까요? 기본키가 없는 세계를 상상해 보세요:
시나리오: "홍길동"의 나이를 수정하고 싶은데, 테이블에 "홍길동"이 3명 있습니다. 시스템은 어느 쪽을 수정해야 할까요?
-- 기본키가 없으면, "홍길동"이라는 이름을 가진 모든 사람이 동시에 수정됨!
UPDATE users SET age = 26 WHERE name = '홍길동';
-- 기본키가 있으면, 정확히 수정 가능
UPDATE users SET age = 26 WHERE user_id = 1001;기본키의 황금 법칙: 모든 테이블에는 기본키가 있어야 하며, 기본키는 절대 수정하지 마세요.
2.3 외래키(Foreign Key): 테이블을 연결하는 다리
이것이 데이터베이스가 Excel보다 강력한 핵심 — 테이블 간에 관계를 설정할 수 있습니다.
📖 외래키란?
외래키는 다른 테이블의 기본키를 가리키는 열로, 테이블 간의 연관을 설정하는 데 사용됩니다.
간단히 이해하기:
- 기본키 = 나의 주민등록번호
- 외래키 = 내가 참조하는 다른 사람의 주민등록번호
예시: 주문 테이블의 user_id가 바로 외래키로, 사용자 테이블의 기본키를 가리킵니다.
실제 예시를 살펴보세요:
사용자 테이블(users):
| user_id (기본키) | name | phone |
|---|---|---|
| 1001 | 홍길동 | 010xxxx |
| 1002 | 이순신 | 011xxxx |
주문 테이블(orders):
| order_id (기본키) | product_name | price | user_id (외래키) |
|---|---|---|---|
| 5001 | iPhone 15 | 5999 | 1001 |
| 5002 | MacBook | 14999 | 1001 |
| 5003 | AirPods | 1999 | 1002 |
핵심 이해:
- 주문 테이블의
user_id = 1001은 사용자 테이블의user_id = 1001(홍길동)을 가리킴 - "주문 5001은 누가 샀나"를 조회할 때, 데이터베이스가 자동으로 사용자 테이블에서
user_id = 1001인 사용자를 찾음
장점:
- 데이터 중복 없음: 홍길동이 100개의 상품을 구매해도 그의 정보는 사용자 테이블에 한 번만 저장
- 유지보수 용이: 홍길동이 전화번호를 바꾸면 사용자 테이블만 수정하면 되고, 모든 주문이 자동으로 새 번호와 연결
- 유연한 쿼리: "각 사용자의 총 소비액은 얼마인가" 같은 복잡한 질문에 쉽게 답할 수 있음
主键(Primary Key):用户表的 user_id 是主键,唯一标识每个用户。
外键(Foreign Key):订单表的 user_id 是外键,指向用户表的主键。
关联查询:通过外键,数据库可以快速找到"订单 001 是用户 101 买的",然后去用户表查到"用户 101 是张三"。
3. 데이터베이스와 대화하는 접근법: SQL 입문과 실전
데이터베이스를 마우스로 "클릭"해서 직접 조작할 수는 없습니다(GUI 도구가 있지만, 본질적으로도 명령어로 변환하는 것입니다). 데이터베이스에 작업을 지시하려면 특별한 언어가 필요합니다.
이 언어가 바로 SQL(Structured Query Language, 구조적 질의어)입니다.
좋은 소식은 SQL이 자연 영어와 매우 비슷해서 읽으면 말하는 것 같다는 것입니다.
3.1 SQL의 핵심 조작: CRUD
대부분의 경우 네 가지 조작만 마스터하면 됩니다. 업계에서 CRUD라고 부릅니다:
| 조작 | 영어 | SQL 키워드 | 이해하기 |
|---|---|---|---|
| Create | 생성 | INSERT | 새로운 데이터 추가 |
| Read | 읽기 | SELECT | 데이터 조회 |
| Update | 수정 | UPDATE | 데이터 갱신 |
| Delete | 삭제 | DELETE | 데이터 삭제 |
📊 표에서 알 수 있는 것
이 네 가지 조작은 데이터 처리의 모든 시나리오를 커버합니다:
- Create: 사용자가 가입할 때, 새 사용자 기록 삽입
- Read: 사용자가 로그인할 때, 사용자명과 비밀번호 조회
- Update: 사용자가 프로필을 수정할 때, 테이블의 데이터 갱신
- Delete: 사용자가 계정을 탈퇴할 때, 사용자 데이터 삭제
이 네 가지만 기억하면 일상 SQL 조작의 80%를 마스터하는 것입니다.
3.2 데이터 조회(SELECT): 데이터베이스에서 가장 많이 사용하는 조작
조회는 데이터베이스에서 가장 중요한 기능이자, 성능 최적화의 핵심입니다.
예시 1: 서울에 있는 모든 사용자 찾기
SELECT name, age FROM users WHERE city = '서울';단어별 이해:
SELECT name, age: name과 age 두 열을 선택FROM users: users 테이블에서WHERE city = '서울': city가 "서울"인 조건에서
반환 결과:
| name | age |
|---|---|
| 홍길동 | 25 |
| 강감찬 | 28 |
예시 2: 가격이 5000에서 15000 사이인 상품 찾기
SELECT name, price FROM products
WHERE price BETWEEN 5000 AND 15000;예시 3: 퍼지 검색(이름에 "홍"이 포함된 사용자 찾기)
SELECT name FROM users WHERE name LIKE '%홍%';⚠️ 성능 함정: LIKE 사용
LIKE '%홍%'은 전체 테이블 스캔을 유발하여, 데이터가 많을 때 매우 느립니다.
최적화 제안:
- ❌
LIKE '%홍%'사용하지 마세요(앞뒤 모두 %) - ✅
LIKE '홍%'은 사용 가능(뒤에만 %)
LIKE '홍%'은 인덱스를 활용할 수 있지만, LIKE '%홍%'은 인덱스를 사용할 수 없기 때문입니다.
3.3 데이터 삽입(INSERT): 새로운 기록 추가
예시: 새 사용자 추가
INSERT INTO users (user_id, name, age, city, email)
VALUES (1004, '박지성', 35, '광주', 'park@example.com');단어별 이해:
INSERT INTO users: users 테이블에 삽입(user_id, name, age, city, email): 삽입할 열 지정VALUES (1004, '박지성', ...): 해당 값
배치 삽입(더 효율적):
INSERT INTO users (name, age, city) VALUES
('철수', 25, '서울'),
('영희', 28, '부산'),
('민수', 30, '광주');3.4 데이터 수정(UPDATE): 기록 갱신
예시: 서울에 있는 모든 사용자의 나이를 1씩 증가
UPDATE users SET age = age + 1 WHERE city = '서울';❌ 매우 위험: WHERE를 잊지 마세요!
WHERE 절을 빼먹으면 모든 행이 수정됩니다!
-- 위험! 모든 사용자의 나이가 26으로 변경됨
UPDATE users SET age = 26;
-- 올바름: user_id = 1001인 사용자만 수정
UPDATE users SET age = 26 WHERE user_id = 1001;실제 교훈: 2012년, 한 유명 기업의 엔지니어가 WHERE를 잊어서 프로덕션 환경의 수백만 건의 사용자 데이터가 잘못 수정되었고, 시스템이 4시간 동안 마비되어 막대한 손실이 발생했습니다.
3.5 데이터 삭제(DELETE): 기록 삭제
예시: user_id = 1004인 사용자 삭제
DELETE FROM users WHERE user_id = 1004;❌ 이중 위험: DELETE에는 더더욱 WHERE가 필요!
-- 위험! 테이블의 모든 데이터가 삭제됨!
DELETE FROM users;
-- 올바름: 지정된 행만 삭제
DELETE FROM users WHERE user_id = 1004;모범 사례:
- 삭제하기 전에 SELECT로 먼저 데이터 확인
- 중요 시스템에서는 "소프트 삭제" 사용(
is_deleted필드를 추가하여 삭제 표시) - 프로덕션 환경에서 조작 전 데이터 백업
3.6 다중 테이블 쿼리(JOIN): 데이터베이스의 마법의 순간
앞서 설명한 "외래키"가 기억나시나요? SQL의 가장 강력한 점은 연관된 여러 테이블을 한 번에 조회할 수 있다는 것입니다.
시나리오: "홍길동이 구매한 모든 상품" 조회
세 개의 테이블이 있다고 가정합니다:
사용자 테이블(users):
| user_id | name |
|---|---|
| 1001 | 홍길동 |
상품 테이블(products):
| product_id | name | price |
|---|---|---|
| 201 | iPhone 15 | 5999 |
| 202 | MacBook | 14999 |
주문 테이블(orders):
| order_id | user_id | product_id | quantity |
|---|---|---|---|
| 5001 | 1001 | 201 | 1 |
| 5002 | 1001 | 202 | 2 |
SQL 쿼리:
SELECT u.name, p.name AS product_name, p.price, o.quantity
FROM orders o
JOIN users u ON o.user_id = u.user_id
JOIN products p ON o.product_id = p.product_id
WHERE u.name = '홍길동';반환 결과:
| name | product_name | price | quantity |
|---|---|---|---|
| 홍길동 | iPhone 15 | 5999 | 1 |
| 홍길동 | MacBook | 14999 | 2 |
JOIN의 과정 이해:
FROM orders o: 주문 테이블에서 시작JOIN users u ON o.user_id = u.user_id: user_id로 사용자 테이블 연결JOIN products p ON o.product_id = p.product_id: product_id로 상품 테이블 연결WHERE u.name = '홍길동': 홍길동의 주문만 필터링
SELECT name, age FROM users WHERE age > 25;4. 데이터베이스 성능 최적화 원리: 인덱스 원리 공개
이것은 데이터베이스에서 가장 신비로운 부분이자, 면접에서 가장 많이 나오는 질문입니다.
Excel에서 "성이 홍인 사람 모두"를 찾으려면 Excel은 첫 행부터 마지막 행까지 스캔해야 합니다. 이것이 바로 전체 테이블 스캔입니다 — 데이터가 많을수록 느려집니다.
하지만 데이터베이스에서는 10억 행의 데이터가 있어도 조회에 몇 밀리초밖에 걸리지 않습니다.
비결은 바로 인덱스(Index)입니다.
4.1 직관적 이해: 사전의 시사점
목차 없는 1000페이지짜리 책에서 단어를 찾아야 한다고 상상해 보세요. 어떻게 해야 할까요?
한 페이지 한 페이지 넘기는 수밖에 없습니다 — 이것이 전체 테이블 스캔으로, 평균 500페이지를 넘겨야 합니다.
하지만 이 책에 음절 인덱스가 있다면?
"데이터베이스"라는 단어를 찾을 때:
- 인덱스를 펼쳐 "데"로 시작하는 영역을 찾음
- "데" 영역 안에서 "이"를 찾음
- 인덱스가 알려줍니다: 256페이지에 있습니다
3번만 넘기면 찾을 수 있습니다! 이것이 인덱스 검색입니다.
데이터베이스의 인덱스는 책의 목차와 같습니다:
- 인덱스 없음: 행 단위 스캔(10억 행 = 수분)
- 인덱스 있음: 직접 이동(10억 행 = 3번의 디스크 I/O = 몇 밀리초)
4.2 전체 테이블 스캔 vs 인덱스 검색: 속도 비교 비교
사용자 테이블에 1000만 건의 기록이 있다고 가정합니다.
시나리오: user_id = 5,555,555인 사용자 찾기
| 방식 | 과정 | 검사해야 할 행 수 | 소요 시간 추정 |
|---|---|---|---|
| 전체 테이블 스캔 | 첫 행부터 시작하여 한 행씩 확인 | 평균 500만 행 | 5-30초 |
| 인덱스 검색 | 인덱스 트리를 검색하여 대상 위치로 직접 이동 | 3-4번의 비교 | 0.003초 |
속도 차이: 수천 배!
💡 핵심 교훈
인덱스는 은총알이 아닙니다. 대가가 있습니다:
- 공간 차지: 인덱스에 추가 저장 공간 필요
- 쓰기 속도 저하: 매번 INSERT/UPDATE/DELETE 시 인덱스도 갱신해야 함
언제 인덱스를 만들어야 하나?
- 자주 쿼리에 사용되는 열(WHERE, JOIN의 조건)
- 데이터량이 큰 경우(수천 건 이하는 불필요)
언제 인덱스를 만들지 말아야 하나?
- 거의 쿼리하지 않는 열
- 잦은 업데이트가 발생하는 열
- 데이터량이 적은 테이블
4.3 기반 데이터 구조: B+ 트리
실제 인덱스는 단순한 "알파벳 목록"이 아니라 정교하게 설계된 데이터 구조인 B+ 트리(B+ Tree)입니다.
📖 B+ 트리란?
B+ 트리는 "키가 크고 폭이 넓은" 트리 형태의 데이터 구조입니다:
- 키가 큼(낮음): 루트에서 잎까지 보통 3-4단계
- 폭이 넓음: 각 노드가 수백 개의 키값을 저장할 수 있음
왜 "키가 크고 폭이 넓어야" 하나?
데이터는 디스크에 저장되며, 디스크를 읽을 때마다(I/O) 매우 느립니다(메모리보다 수천 배 느림). B+ 트리의 설계 목표는 디스크 I/O 횟수를 최소화하는 것입니다.
- 3-4단계 높이 = 최대 3-4번의 디스크 읽기
- 각 단계에 대량의 데이터 저장 = 트리가 너무 높아지지 않음을 보장
실제 예시:
B+ 트리의 각 노드가 1000개의 키값을 저장할 수 있다고 가정:
- 루트 노드: 1000개의 키값 → 1000개의 자식 노드를 가리킴
- 중간 노드: 각각 1000개의 키값 저장 → 1000개의 잎 노드를 가리킴
- 잎 노드: 각각 1000건의 실제 데이터 저장
총 데이터량 = 1000 × 1000 × 1000 = 10억 건의 데이터
트리의 높이 = 3단계
이것은 10억 건의 데이터에서 아무거나 하나를 찾을 때 단 3번의 디스크 I/O만 필요하다는 뜻입니다!
이것이 데이터베이스 쿼리가 엄청나게 빠른 비밀입니다.
👆 点击"开始查找"看全表扫描有多慢
👆 点击"开始查找"看索引有多快
5. 트랜잭션: 데이터를 잃지 않고, 엉키지 않게 접근법
명절 기차표 예매 상황을 상상해 보세요:
- 시간 T1: 사용자 A가 조회, "G1234 열차 잔여 좌석 1석" 발견
- 시간 T2: 사용자 B도 조회, 역시 "잔여 좌석 1석" 발견
- 시간 T3: 사용자 A가 "구매" 클릭, 시스템이 재고 차감, 표를 A에게 판매
- 시간 T4: 사용자 B가 "구매" 클릭 — 보호 메커니즘이 없다면, 시스템이 다시 재고를 차감하여 같은 좌석을 B에게도 판매!
이것이 전형적인 동시성 충돌 문제입니다.
5.1 트랜잭션(Transaction)이 개요
트랜잭션은 데이터베이스의 한 그룹 조작으로, 이 조작들은 모두 성공하거나 모두 실패하며, "절반만 실행된" 상태가 되지 않습니다.
🤖 일상의 예시
은행 이체가 전형적인 트랜잭션입니다:
- 계좌 A에서 100위안 차감
- 계좌 B에 100위안 추가
1단계가 성공했는데 2단계가 실패하면(예: 정전) 어떻게 될까요?
- 트랜잭션 없음: 계좌 A의 돈은 사라지고 계좌 B는 돈을 받지 못해, 돈이 허공에서 증발
- 트랜잭션 있음: 시스템이 2단계 실패를 감지하고 자동으로 1단계를 롤백, 두 계좌 모두 원래 상태로 복원
이것이 트랜잭션의 원자성입니다: 모두 하거나 모두 하지 않거나.
5.2 트랜잭션의 네 가지 특성(ACID)
트랜잭션에는 네 가지 특성이 있으며, 약자로 ACID라고 합니다:
| 특성 | 영어 | 의미 | 은행 이체의 예시 |
|---|---|---|---|
| Atomicity | 원자성 | 모두 하거나 모두 하지 않거나 | 차감과 입금이 동시에 성공해야 함, 차감만 하고 입금하지 않을 수 없음 |
| Consistency | 일관성 | 데이터가 항상 합법적 상태를 유지 | 이체 전후 두 계좌의 총액은 변하지 않아야 함 |
| Isolation | 격리성 | 여러 트랜잭션이 서로 영향을 미치지 않음 | A가 이체하는 동안 B가 보는 것은 "이체 전" 또는 "이체 후"의 잔액이어야 하며, 중간 상태를 볼 수 없음 |
| Durability | 지속성 | 한 번 커밋되면 데이터가 영구 저장 | 이체 성공 후 정전이 나도 계좌 잔액은 되돌아가지 않음 |
📊 표에서 알 수 있는 것
이 네 가지 특성이 데이터의 안전성을 보장합니다:
- 원자성: "절반만 실행" 방지(돈은 차감됐는데 입금 안 됨)
- 일관성: 비합리적 데이터 방지(이체 후 총액이 변함)
- 격리성: 동시성 충돌 방지(두 사람이 동시에 같은 데이터를 수정)
- 지속성: 데이터 유실 방지(커밋 후 정전이 나도 영향 없음)
이러한 보장이 없으면 은행 시스템은 전혀 운영될 수 없습니다.
5.3 트랜잭션의 격리 수준: 안전성과 성능의 균형
이론적으로 트랜잭션은 완전히 격리되어야 합니다. 하지만 완전 격리 = 성능 극도로 저하(대량의 잠금이 필요하여 다른 트랜잭션이 대기만 해야 함).
따라서 데이터베이스는 네 가지 격리 수준을 제공합니다:
| 격리 수준 | 더티 리드 | 논리퍼블 리드 | 팬텀 리드 | 성능 | 적용 시나리오 |
|---|---|---|---|---|---|
| 커밋되지 않은 읽기 | 가능 | 가능 | 가능 | 가장 빠름 | 거의 사용 안 함(데이터가 틀릴 수 있음) |
| 커밋된 읽기 | 불가능 | 가능 | 가능 | 비교적 빠름 | 일반 비즈니스(Oracle 기본값) |
| 반복 가능한 읽기 | 불가능 | 불가능 | 가능 | 보통 | 은행 이체(MySQL 기본값) |
| 직렬화 | 불가능 | 불가능 | 불가능 | 가장 느림 | 극도로 엄격한 시나리오(거의 사용 안 함) |
📖 세 가지 "읽기"란 무엇인가?
- 더티 리드(Dirty Read): 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽음(롤백될 수 있어 부정확)
- 논리퍼블 리드(Non-repeatable Read): 같은 트랜잭션에서 같은 데이터를 두 번 읽었는데 결과가 다름(다른 트랜잭션이 수정함)
- 팬텀 리드(Phantom Read): 같은 트랜잭션에서 두 번 쿼리했는데 결과 집합의 행 수가 다름(다른 트랜잭션이 데이터를 삽입/삭제함)
비유적 예시(은행 잔액 조회):
- 더티 리드: 잔액이 1000위안이라고 조회했지만, 상대방 트랜잭션이 롤백되어 실제로는 100위안뿐
- 논리퍼블 리드: 첫 조회 시 잔액 1000위안, 두 번째 조회 시 800위안으로 변함(차감됨)
- 팬텀 리드: 첫 조회 시 5건의 거래, 두 번째 조회 시 6건(새 거래 추가됨)
场景:用户 A 和 B 同时看到还剩 1 张票,同时点击购买。
没有事务:A 扣库存,B 也扣库存,同一张票卖给了两个人!
有事务(隔离性):A 的操作加锁,B 必须等待。A 买完后,库存变为 0,B 看到的是"已售罄"。
6. 성능 최적화: 쿼리를 1000배 빠르게 만드는 실전 팁
이제 인덱스, 트랜잭션 같은 핵심 개념을 이해했습니다. 하지만 실제 프로젝트에서는 다양한 성능 문제에 직면할 수 있습니다.
이 절에서는 바로 적용할 수 있는 최적화 전략을 제공합니다.
6.1 인덱스 사용 시 주의사항 가이드
⚠️ 일반적 오류: 인덱스가 무효화되는 함정
인덱스를 만들었는데도 쿼리가 여전히 느린 경우가 많습니다 — 인덱스가 무효화되었기 때문입니다.
인덱스 무효화의 일반적 원인:
- 인덱스 열에 함수 사용
- 암시적 타입 변환
- LIKE 쿼리가 %로 시작
- OR 조건(일부 상황)
- 복합 인덱스가 최좌측 접두사 원칙을 충족하지 않음
함정 1: 인덱스 열에 함수 사용
-- ❌ 오류: 인덱스 열에 함수를 사용하면 인덱스를 사용할 수 없음
SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- ✅ 올바름: 범위 쿼리로 변경하면 인덱스 사용 가능
SELECT * FROM users
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';함정 2: 암시적 타입 변환
-- user_id가 int 타입이라고 가정
-- ❌ 오류: 문자열을 전달하면 암시적 변환이 발생하여 인덱스를 사용할 수 없음
SELECT * FROM users WHERE user_id = '123';
-- ✅ 올바름: 해당 타입을 전달
SELECT * FROM users WHERE user_id = 123;함정 3: LIKE가 %로 시작
-- ❌ 오류: %로 시작하면 인덱스를 사용할 수 없음
SELECT * FROM users WHERE name LIKE '%홍길동%';
-- ✅ 올바름: 고정 접두사로 시작하면 인덱스 사용 가능
SELECT * FROM users WHERE name LIKE '홍길동%';
-- ✅ 또는 전문 인덱스 사용(텍스트 검색에 적합)
SELECT * FROM users WHERE MATCH(name) AGAINST('홍길동');6.2 SQL 최적화 실전 템플릿
템플릿 1: 페이지네이션 최적화(깊은 페이지 문제)
문제와 해결 방법 보기
-- ❌ 문제: OFFSET이 크면 쿼리가 점점 느려짐
SELECT * FROM orders
ORDER BY created_at DESC
LIMIT 10 OFFSET 1000000;
-- ✅ 최적화 방안 1: 이전 쿼리의 타임스탬프를 커서로 사용
SELECT * FROM orders
WHERE created_at < '2024-01-15 12:00:00'
ORDER BY created_at DESC
LIMIT 10;
-- ✅ 최적화 방안 2: 기본키 범위 쿼리 사용
SELECT * FROM orders
WHERE order_id > 1000000
ORDER BY order_id
LIMIT 10;템플릿 2: 배치 삽입 최적화
-- ❌ 비효율: 여러 번의 단건 삽입(네트워크 왕복이 여러 번)
INSERT INTO users (name, age) VALUES ('홍길동', 25);
INSERT INTO users (name, age) VALUES ('이순신', 30);
INSERT INTO users (name, age) VALUES ('강감찬', 28);
-- ✅ 효율: 단일 SQL 배치 삽입(네트워크 왕복 한 번만)
INSERT INTO users (name, age) VALUES
('홍길동', 25),
('이순신', 30),
('강감찬', 28);템플릿 3: SELECT * 피하기
-- ❌ 비효율: 모든 열을 반환(필요 없는 대형 필드 포함)
SELECT * FROM users WHERE user_id = 1;
-- ✅ 효율: 필요한 열만 반환
SELECT user_id, name, email FROM users WHERE user_id = 1;6.3 높은 동시성 시나리오 대응 전략
| 시나리오 | 문제 | 해결 방안 |
|---|---|---|
| 핫 데이터 | 특정 행이 빈번하게 읽기/쓰기되어 잠금 경쟁 발생 | 캐시(Redis) 사용 + 읽기/쓰기 분리 |
| 플래시 세일 | 순간적인 높은 동시성 재고 차감 | 낙관적 잠금 + 재고 사전 로딩 + 메시지 큐로 피크 분산 |
| 슬로우 쿼리 | 복잡한 쿼리가 데이터베이스를 마비시킴 | 인덱스 최적화 + 쿼리 분할 + 읽기/쓰기 분리 |
| 커넥션 고갈 | 너무 많은 동시 요청으로 커넥션 풀이 고갈됨 | 커넥션 풀 최적화 + 트래픽 제한 + 서비스 강등 |
💡 핵심 교훈
성능 최적화의 기본 원칙:
- 먼저 측정하고, 나중에 최적화:
EXPLAIN으로 쿼리 계획을 분석하여 진짜 병목을 찾기 - 인덱스 우선: 성능 문제의 80%는 인덱스 최적화로 해결 가능
- 데이터베이스 부하 감소: 캐시를 쓸 수 있으면 캐시를, 비동기로 할 수 있으면 비동기로
- 분할 정복: 큰 테이블은 작은 테이블로, 큰 쿼리는 작은 쿼리로 분할
7. 요약과 학습 로드맵
표로 데이터베이스의 핵심 개념을 되돌아보겠습니다:
| 개념 | 한마디 설명 | 해결하는 문제 | 핵심 포인트 |
|---|---|---|---|
| 테이블, 행, 열 | 데이터의 구성 방식 | 구조화된 데이터를 어떻게 저장할 것인가 | 테이블 = Excel 워크시트, 행 = 기록, 열 = 필드 |
| 기본키 | 각 행의 유일한 식별자 | 특정 행을 정확히 찾는 방법 | 유일, 비어 있지 않음, 불변 |
| 외래키 | 테이블을 연결하는 다리 | 서로 다른 테이블의 데이터를 연관시키는 방법 | 다른 테이블의 기본키를 가리킴 |
| SQL | 데이터베이스와 대화하는 언어 | 데이터를 어떻게 추가/조회/수정/삭제할 것인가 | SELECT, INSERT, UPDATE, DELETE |
| 인덱스 | 쿼리를 가속하는 데이터 구조 | 데이터를 빠르게 찾는 방법 | B+ 트리, 디스크 I/O 감소 |
| 트랜잭션 | 데이터 안전을 보장하는 메커니즘 | 동시성 충돌과 데이터 유실을 방지하는 방법 | ACID: 원자성, 일관성, 격리성, 지속성 |
마지막으로
데이터베이스는 매우 깊고 넓은 주제이며, 이 글은 입문일 뿐입니다. 더 깊이 학습하고 싶다면 다음 경로를 권장합니다:
다음 단계 학습:
- 실습: MySQL이나 PostgreSQL을 설치하고, 테이블을 만들고, 데이터를 삽입하고, SQL 쿼리를 작성
- ORM 프레임워크: 코드에서 데이터베이스를 사용하는 방법 학습(예: SQLAlchemy, Prisma, TypeORM)
- 인덱스 최적화: 복합 인덱스, 커버링 인덱스, 인덱스 푸시다운 등 고급 주제 심층 연구
- 트랜잭션 원리: MVCC(다중 버전 동시성 제어), 잠금 메커니즘, 격리 수준 구현 이해
- 분산 데이터베이스: 샤딩, 읽기/쓰기 분리, 마스터-슬레이브 복제 등 아키텍처 학습
기억하세요: 이론 + 실습 = 진정한 마스터리.