데이터베이스 기초 (인덱스 / 트랜잭션 / 쿼리 최적화)
🎯 핵심 질문
왜 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(다중 버전 동시성 제어), 잠금 메커니즘, 격리 수준 구현 이해
- 분산 데이터베이스: 샤딩, 읽기/쓰기 분리, 마스터-슬레이브 복제 등 아키텍처 학습
기억하세요: 이론 + 실습 = 진정한 마스터리.