이 문서는 의문을 가지고 있던 지점을 AI 에이전트와 대화하며 정리한 메모입니다. InnoDB 인덱스와 페이지 구조, 락과 MVCC, 세션과 커넥션 풀까지 MySQL 핵심 개념을 한 번에 정리합니다.
1. 랜덤 액세스(Random Access)#
1-1. 의미#
랜덤 액세스란 저장된 데이터의 위치와 상관없이 원하는 위치의 데이터에 직접 접근할 수 있는 방식을 의미합니다.
예를 들어 배열에서 a[100]에 바로 접근하거나, DB에서 인덱스를 통해 특정 키 값을 빠르게 찾는 것이 랜덤 액세스의 대표적 예시입니다.
1-2. 데이터베이스에서 왜 중요한가#
DBMS는 대량의 데이터를 저장하고 검색합니다. 이때 모든 데이터를 처음부터 끝까지 순차적으로 읽는다면 비효율적입니다. 그래서 DBMS는 인덱스를 사용해 **“어디쯤에 원하는 값이 있을지”**를 빠르게 찾고, 그 위치로 바로 이동합니다.
즉, 인덱스는 랜덤 액세스를 효율적으로 가능하게 해주는 핵심 도구입니다.
2. InnoDB에서 기본 키(PK)가 물리적 저장 위치를 결정한다는 뜻#
2-1. 클러스터형 인덱스(Clustered Index)#
InnoDB에서는 테이블 데이터가 기본 키 순서대로 저장됩니다. 즉, 기본 키 자체가 단순 식별자일 뿐만 아니라 클러스터드 인덱스 리프 페이지에서의 배치 기준이 됩니다.
이것을 흔히 다음처럼 표현합니다.
- InnoDB 테이블 = 기본 키 기반의 클러스터형 인덱스
- 리프 페이지 = 실제 행(row) 데이터 저장
2-2. 무슨 뜻인가#
예를 들어 PK가 다음과 같다고 해봅시다.
id = 1, 2, 3, 4, 5InnoDB는 이 데이터를 대체로 id 순으로 정렬된 상태로 페이지에 저장합니다.
여기서 말하는 “물리적 저장 위치"는 파일 안의 오프셋이 영구히 고정된다는 뜻보다는, B-tree 리프 페이지가 PK 순서로 구성된다는 뜻에 가깝습니다.
즉:
- PK가 작으면 앞쪽 페이지
- PK가 크면 뒤쪽 페이지
에 배치되는 경향이 있습니다.
2-3. 왜 중요한가#
기본 키 기준으로 데이터가 정렬되어 있으므로:
- PK 조회가 매우 빠름
- PK 범위 조회가 매우 유리함
- 보조 인덱스가 PK를 참조하므로 PK 크기가 전체 인덱스 크기에 영향 줌
3. “자주 변하는 값을 PK로 두면 성능이 나빠진다"의 의미#
3-1. 이유#
InnoDB는 데이터를 PK 순서로 저장하므로, PK 값이 바뀌면 그 행의 저장 위치가 달라질 수 있습니다.
즉, UPDATE pk = ...는 사실상 내부적으로:
- 기존 레코드 삭제
- 새 PK 위치에 다시 삽입
에 가까운 비용을 유발할 수 있습니다.
3-2. 부작용#
- 페이지 재배치 비용 발생
- 페이지 분할(page split) 가능성 증가
- 보조 인덱스도 함께 정비 필요
- 디스크 I/O 증가
- 락 경쟁 가능성 증가
3-3. 실무 관점#
그래서 PK는 보통 다음 특성을 갖는 값이 좋습니다.
- 짧고
- 고정적이며
- 절대 바뀌지 않고
- 가능하면 단조 증가(auto increment 등)
4. 기본 키가 실제로 바뀌는 경우의 예시#
보통 PK는 잘 안 바뀌는 것이 맞습니다. 하지만 잘못된 모델링을 하면 PK 변경이 필요해질 수 있습니다.
예시#
4-1. 자연키를 PK로 둔 경우#
예:
- 이메일 주소
- 주민번호
- 사번
- 사용자명(username)
이런 값은 “변하지 않을 것 같지만” 실제 서비스에서는 바뀔 수 있습니다.
예:
- 이메일 변경
- 사용자명 변경 정책 허용
- 외부 시스템 통합으로 ID 규칙 변경
4-2. 시스템 통합#
A 시스템과 B 시스템을 합치는데 PK 체계가 충돌하면 PK 재정비가 필요해질 수 있습니다.
4-3. 업무 규칙 변경#
예전에는 상품코드를 PK로 사용했는데, 상품코드 규칙 자체가 바뀌는 경우입니다.
5. B+트리 탐색 복잡도와 디스크 접근#
5-1. 탐색 복잡도#
B+트리는 높이를 h라고 할 때 탐색 비용이 대략 O(h)입니다.
5-2. 왜 디스크 접근 횟수와 연결되는가#
B+트리의 각 노드는 보통 한 페이지(page) 단위로 저장됩니다. 따라서 루트 → 내부 노드 → 리프 노드로 내려갈 때 노드 하나를 읽을 때마다 페이지 하나를 읽는 셈이 됩니다.
즉:
- 트리 높이 = 최악의 경우 읽어야 하는 페이지 수
- 페이지가 버퍼 풀에 없다면 = 디스크 I/O 발생
5-3. 한 번에 리프까지 못 가는 이유#
한 페이지 안에는 전체 트리가 들어있지 않습니다. 한 페이지에는 일부 키와 자식 페이지 주소만 들어있습니다.
그래서 검색 과정은:
- 루트 페이지 읽기
- 그 안에서 어느 자식 페이지로 갈지 결정
- 자식 페이지 읽기
- 다시 다음 자식 결정
- 리프 페이지 도달
형태가 됩니다.
6. MySQL에서 “접근"은 누가 하는가#
MySQL에서 디스크 접근은 사용자가 직접 하는 것이 아니라 스토리지 엔진과 OS가 협력하여 수행합니다.
흐름을 단순화하면 다음과 같습니다.
- 클라이언트가 SQL 전송
- MySQL 서버가 파싱/최적화
- 스토리지 엔진(InnoDB 등)이 필요한 페이지 요청
- 버퍼 풀에 없으면 OS를 통해 디스크에서 읽음
- 페이지를 메모리에 적재 후 사용
즉, 실제 SQL은 MySQL이 처리하지만 물리적 읽기/쓰기 요청은 결국 OS와 디스크 서브시스템이 담당합니다.
7. B+트리 페이지 안에는 무엇이 들어 있나#
7-1. 내부 노드#
내부 노드에는 대체로 다음이 있습니다.
- 여러 개의 키 값
- 각 키 범위에 대응하는 자식 페이지 포인터
예를 들어:
- key < 10 이면 page A
- 10 <= key < 20 이면 page B
- key >= 20 이면 page C
7-2. 리프 노드#
리프 노드에는 실제 검색 대상이 있습니다.
- InnoDB의 클러스터드 인덱스 리프: 실제 row 데이터
- 보조 인덱스 리프: 인덱스 키 + PK 값
7-3. 페이지 간 이동#
현재 페이지에서 키 값을 비교한 뒤, 그 결과에 맞는 다음 페이지 번호(page id) 를 찾아 해당 페이지를 읽습니다.
즉, “다른 페이지로 간다"는 말은 결국 페이지 번호를 가지고 그 페이지를 메모리/디스크에서 읽는 것입니다.
8. 버퍼 관리 정책: STEAL / NO-STEAL, FORCE / NO-FORCE#
트랜잭션 중 수정된 페이지(더티 페이지)를 언제 디스크에 쓸지 기준으로 나누는 대표적인 정책입니다.
다만 이 용어들은 데이터베이스 교과서에서 자주 쓰는 분류이고, MySQL 공식 문서가 InnoDB를 이 네 용어로 직접 규정하는 것은 아닙니다. InnoDB의 버퍼 풀 flush, redo, checkpoint 동작을 이해하기 위한 개념적 프레임으로 보는 편이 안전합니다.
8-1. STEAL#
커밋 전이라도 더티 페이지를 디스크에 쓸 수 있음
의미#
버퍼 공간이 부족하면, 아직 커밋되지 않은 트랜잭션이 수정한 페이지도 디스크에 내보낼 수 있습니다.
장점#
- 버퍼 관리 유연
- 메모리 사용 효율 좋음
단점#
- 커밋 전 데이터가 디스크에 기록될 수 있으므로, 장애 시 UNDO 필요
8-2. NO-STEAL#
커밋 전 더티 페이지를 디스크에 쓰지 않음
장점#
- 장애 시 UNDO 부담이 줄어듦
단점#
- 버퍼 압박이 큼
- 메모리 관리가 어렵고 성능상 불리할 수 있음
8-3. FORCE#
커밋 시 해당 트랜잭션의 수정 내용을 디스크 페이지까지 강제로 기록
장점#
- 장애 후 REDO 필요성 감소
단점#
- 커밋마다 디스크 쓰기 부담이 큼
- 성능 저하 가능성 큼
8-4. NO-FORCE#
커밋 시 로그만 보장하고, 데이터 페이지는 나중에 써도 됨
장점#
- 커밋이 빠름
- 현대적인 WAL 계열 DBMS에서 흔한 방향
단점#
- 장애 시 REDO 필요
9. REDO와 UNDO#
9-1. REDO#
이미 커밋된 변경을 다시 적용해서 복구하는 것
예:
- 커밋은 되었는데 데이터 페이지 반영 전에 장애 발생
- 로그를 보고 변경사항을 다시 적용
즉, 내구성(Durability) 확보를 위한 복구
9-2. UNDO#
커밋되지 않은 변경을 되돌리는 것
예:
- 트랜잭션이 중간에 실패
- 이미 일부 변경이 반영되었으면 원래 상태로 복원해야 함
즉, 원자성(Atomicity) 확보를 위한 복구
9-3. 차이 정리#
- REDO: “해야 했던 일을 다시 함”
- UNDO: “하면 안 되는 일을 취소함”
10. InnoDB의 UNDO 로그와 REDO 로그의 성격#
10-1. UNDO 로그#
InnoDB의 UNDO는 논리적 성격이 강합니다.
특히:
- UPDATE 이전 값 복원
- DELETE 취소
- INSERT 취소(삽입된 행 제거)
등의 목적으로 사용됩니다.
MVCC에서도 과거 버전을 재구성하는 데 사용됩니다. 다만 INSERT의 UNDO는 MVCC용 과거 버전 제공에는 직접 사용되지 않고, 주로 롤백 시 “삽입된 행을 제거하기 위한 정보"로 쓰입니다.
10-2. REDO 로그#
REDO는 일반적으로 low-level 변경 복구 정보의 성격이 강합니다.
즉, SQL 문장을 다시 실행하는 것이라기보다, 데이터 파일에 아직 반영되지 못한 변경을 복구 시점에 다시 적용할 수 있도록 기록합니다.
11. “INSERT 전에는 데이터가 없는데 UNDO 로그가 왜 필요한가?”#
매우 중요한 포인트입니다.
11-1. 핵심#
INSERT 전에는 “이전 값"이 없으므로, UPDATE처럼 before image를 저장하지는 않습니다. 하지만 롤백을 하려면 **“이 행을 삭제해야 한다”**는 정보는 필요합니다.
즉, INSERT의 UNDO는 보통:
- 이전 값을 복원하는 용도라기보다
- “이 INSERT를 취소하려면 어떤 레코드를 제거해야 하는가"를 나타내는 정보
입니다.
11-2. MVCC와의 관계#
INSERT된 새 행은 원래 과거 버전이 없기 때문에, 다른 트랜잭션의 일관된 읽기에서는 아예 안 보이면 그만입니다.
그래서 INSERT UNDO는 MVCC에서 과거 버전을 만드는 역할보다는, 주로 롤백 / 복구용 의미가 큽니다.
12. 물리적 로그 vs 논리적 로그#
12-1. 물리적 로그#
“페이지의 어디가 어떻게 바뀌었는지"를 기록
장점#
- 재적용이 명확
- REDO에 적합
단점#
- 로그량이 커질 수 있음
- 페이지 구조 의존적
- 동시성/유연성 측면에서 불리할 수 있음
12-2. 논리적 로그#
“무슨 연산을 했는지"를 기록
예:
- A의 잔액에서 100 차감
- B의 잔액에 100 증가
장점#
- 더 추상적
- 일부 상황에서 동시성과 유연성에 유리
단점#
- 재실행 시 문맥이 필요할 수 있음
13. REPEATABLE READ, READ COMMITTED, MVCC에 대한 이해#
13-1. REPEATABLE READ의 핵심#
InnoDB의 REPEATABLE READ 에서 일반 SELECT(consistent read) 는 보통 트랜잭션 안의 첫 consistent read가 만든 읽기 스냅샷(read view) 을 기준으로 조회합니다.
그래서 같은 트랜잭션 내에서는 나중에 다른 트랜잭션이 커밋한 데이터라도 처음 스냅샷에 없었다면 보이지 않을 수 있습니다.
즉:
- 트랜잭션2가 시작된 뒤
- 트랜잭션1이 커밋했더라도
- 트랜잭션2의 일반 SELECT는 여전히 예전 스냅샷 기준으로 읽음
이것이 사용자가 물어본 “트랜잭션2는 본인 트랜잭션 ID 이전의 값만 읽는 것으로 아는데, 왜 트랜잭션1이 기록한 데이터는 못 읽지?” 에 대한 핵심 이유입니다.
정확한 이해#
중요한 것은 단순히 “트랜잭션 ID가 더 작으냐"가 아니라, 트랜잭션2가 생성한 Read View에서 그 트랜잭션이 가시한지 여부입니다.
또한 SELECT ... FOR UPDATE, SELECT ... FOR SHARE, UPDATE, DELETE 같은 잠금 경로는 항상 같은 규칙으로 snapshot을 읽는 것이 아니라 최신 상태와 락 규칙을 함께 사용하므로, 일반 SELECT 와 구분해서 이해해야 합니다.
13-2. READ COMMITTED의 핵심#
각 SELECT 문장이 실행될 때마다 새 Read View를 만듭니다.
그래서 일반적으로:
- 한 번 조회했을 때 안 보였던 값이
- 나중에 다른 트랜잭션 커밋 후 다시 조회하면
- 보일 수 있습니다.
다만 실제 실험에서는 다음을 주의해야 합니다.
- 첫 번째 SELECT와 두 번째 SELECT가 정말 각각 독립된 consistent read였는가
- 잠금 읽기(
SELECT ... FOR UPDATE)였는가 - 애플리케이션/프레임워크가 트랜잭션 경계를 어떻게 잡았는가
즉, 이론상 READ COMMITTED는 “문장 단위 최신 커밋 반영"이지만, 실험 상황의 쿼리 종류와 트랜잭션 흐름에 따라 관찰 결과 해석을 신중히 해야 합니다.
14. 락(lock)의 종류와 레코드 락#
14-1. 레코드 락(Record Lock)#
인덱스 레코드 하나를 잠그는 락입니다.
즉 “행 전체"를 추상적으로 잠그는 것이 아니라, 실제로는 인덱스 엔트리 기준으로 락이 걸린다고 이해해야 합니다.
14-2. 언제 사용되나#
UPDATEDELETESELECT ... FOR UPDATESELECT ... FOR SHARE(LOCK IN SHARE MODE와 동등한 구문)
등에서 사용됩니다.
15. INSERT 시 락은 어떻게 동작하나#
사용자가 특히 많이 궁금해한 부분입니다.
15-1. “없는 데이터에 어떻게 배타 락을 거나?”#
정확히는 없는 행 자체를 잠그는 것이 아니라, 그 행이 들어갈 인덱스 구간 / 위치 와 삽입될 레코드에 대해 적절한 락 메커니즘을 사용합니다.
관련 개념:
- insert intention lock
- gap lock
- next-key lock
- record lock
15-2. Insert Intention Lock#
삽입하려는 인덱스 구간에 대해 “나는 여기에 insert하려고 한다"는 의도를 표시하는 락입니다.
여러 트랜잭션이 같은 gap에 insert하려 해도, 실제 동일 위치/동일 키 충돌이 아니면 어느 정도 병행 가능성을 가집니다.
15-3. Gap Lock#
존재하는 인덱스 레코드 사이의 “간격"을 잠급니다.
주된 목적:
- 팬텀 읽기 방지
- 범위 내 새로운 레코드 삽입 방지
15-4. Next-Key Lock#
Record Lock + Gap Lock
즉:
- 어떤 인덱스 레코드와
- 그 앞의 gap
을 함께 잠급니다.
16. InnoDB는 PK 인덱스에만 락을 거는가?#
아닙니다. InnoDB는 인덱스 기반으로 락을 관리하며, 이것은 PK(클러스터드 인덱스)뿐 아니라 보조 인덱스(secondary index) 에도 적용됩니다.
즉:
- 어떤 인덱스를 통해 탐색하느냐
- 어떤 조건으로 조회/수정하느냐
에 따라 락이 걸리는 인덱스가 달라질 수 있습니다.
다만 InnoDB에서 실제 레코드는 PK 인덱스 리프에 있으므로, 보조 인덱스를 통해 접근하더라도 최종적으로 PK 쪽 확인이 동반될 수 있습니다.
17. Unique 인덱스 vs 일반 세컨더리 인덱스#
17-1. 흔한 설명#
“유니크 인덱스는 1건만 읽으면 되지만, 유니크하지 않은 인덱스는 한 건 더 읽어야 해서 느리다”
이 설명은 완전히 틀렸다고 보긴 어렵지만, 지나치게 단순화된 설명입니다.
17-2. 정확히는#
유니크 인덱스는 조건이 일치하면 최대 1건만 존재함이 보장되므로, 옵티마이저는 탐색 결과를 빠르게 확정할 수 있습니다.
반면 일반 인덱스는 같은 키가 여러 개일 수 있어
- 추가 레코드 존재 가능성 확인
- 여러 엔트리 순회
등이 필요할 수 있습니다.
하지만 성능 차이는 대개 아주 미세하며, 실제 병목은 보통 다음이 더 큽니다.
- 커버링 인덱스 여부
- 랜덤 I/O 여부
- 페이지 캐시 히트율
- 반환 컬럼 수
- 읽어야 하는 실제 row 수
18. 특정 이메일 존재 여부 확인 쿼리 비교#
대상 테이블:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE
);비교 대상:
SELECT * FROM users WHERE email = :user_email;
SELECT email FROM users WHERE email = :user_email;
SELECT count(*) FROM users WHERE email = :user_email;18-1. SELECT *#
가장 비효율적일 가능성이 큽니다.
이유#
email인덱스로 위치는 찾더라도*때문에 전체 row를 읽어야 함- 즉, 인덱스만으로 끝나지 않음
- 보통 커버링 인덱스 아님
실행계획 관점:
- 인덱스 탐색 후
- 실제 clustered index row 접근 필요
18-2. SELECT email#
이 쿼리는 email 인덱스만으로 결과를 반환할 수 있습니다.
장점#
- 커버링 인덱스 가능
- 테이블 row 전체를 안 읽어도 될 수 있음
- 네트워크 반환 데이터도 적음
18-3. SELECT count(*)#
존재 여부만 보고 싶다면 동작 자체는 맞습니다.
다만 이 예시는 email 이 UNIQUE 라서 차이가 작을 수 있어도, 존재 여부만 필요하다면 보통 EXISTS 나 SELECT 1 ... LIMIT 1 처럼 “있느냐"를 바로 표현하는 형태를 더 많이 권장합니다.
18-4. 존재 여부만 필요하다면 가장 추천되는 형태#
SELECT EXISTS(
SELECT 1
FROM users
WHERE email = :user_email
) AS email_exists;이유#
- 존재 확인이 목적임이 명확
- 첫 행을 찾으면 더 볼 필요가 없음
- 읽는 사람에게도 의도가 분명함
19. SELECT *가 비효율적인 이유를 옵티마이저와 실행계획 관점에서 보면#
19-1. 커버링 인덱스 불가#
SELECT *는 모든 컬럼이 필요하므로
email 유니크 인덱스만으로는 쿼리를 끝낼 수 없습니다.
즉:
email인덱스에서 조건 일치 엔트리 찾기- 그 엔트리가 가리키는 실제 row를 다시 읽기
19-2. 결과 전송량 증가#
존재 여부만 필요한데도:
- id
- username
모두 반환하면 불필요한 I/O와 네트워크 비용이 생깁니다.
19-3. 실무 관점 결론#
“필요한 컬럼만 조회하라” 는 원칙이 매우 중요합니다.
20. Unique 제약 조건과 인덱스#
20-1. UNIQUE를 걸면 인덱스가 생기나?#
일반적으로 그렇습니다. MySQL은 UNIQUE 제약 조건을 위해 유니크 인덱스를 생성합니다.
20-2. 해당 컬럼으로 WHERE 검색 시 빨라지나?#
네, 보통 빨라집니다.
예:
SELECT * FROM users WHERE email = 'a@b.com';여기서 email에 UNIQUE 인덱스가 있으면,
MySQL은 인덱스를 사용해 빠르게 탐색할 수 있습니다.
20-3. UNIQUE인데도 NULL은?#
MySQL에서는 UNIQUE 인덱스라도 NULL 가능한 컬럼이면 여러 NULL 값을 허용합니다.
즉, “UNIQUE 이니까 NULL 도 하나만 된다"라고 이해하면 틀립니다.
21. 조인 문법 두 가지#
SELECT * FROM employees e JOIN salaries s ON e.emp_no = s.emp_no;
SELECT * FROM employees e, salaries s WHERE e.emp_no = s.emp_no;차이#
기능적으로는 같은 inner join 결과를 냅니다.
권장 방식#
첫 번째처럼 JOIN ... ON을 사용하는 것이 좋습니다.
이유#
- 가독성 좋음
- 조인 조건이 명확
- 복잡한 쿼리에서 실수 줄임
22. Undo 로그와 Undo 페이지의 차이#
Undo 로그#
트랜잭션 변경을 되돌리기 위한 논리적 정보
Undo 페이지#
그 Undo 로그들이 실제 저장되는 물리적 페이지 단위
즉:
- Undo 로그 = 내용
- Undo 페이지 = 그 내용을 담는 저장 공간
23. 트랜잭션은 DB ACID를 보장할 뿐, 자바 코드의 원자성을 보장하지 않는다#
이 문장은 핵심적으로 맞습니다.
의미#
DB 트랜잭션은 데이터베이스 내부 변경에 대해 ACID를 보장하지만, 애플리케이션 코드 전체가 자동으로 원자화되는 것은 아닙니다.
Spring의 @Transactional 도 기본적으로 현재 실행 스레드에 바인딩된 트랜잭션 경계를 다루는 것이지, 메서드 안에서 새로 만든 스레드까지 자동으로 전파되는 모델은 아닙니다.
예:
- DB 조회
- 자바 조건 분기
- 없으면 생성
- 저장
이 전체가 애플리케이션 레벨 경쟁 조건(race condition)에 노출될 수 있습니다.
synchronized로 막는 것이 왜 한계가 있나#
- 애플리케이션 인스턴스가 여러 대면 JVM 내부 lock은 무력
- DB 레벨 경쟁 상태는 DB 제약/락으로 해결해야 함
- 대표적 해결책:
- UNIQUE 제약
- 적절한 트랜잭션과 락
- upsert 패턴
24. 미디어 복구(Media Recovery)#
미디어 복구는 디스크 손상, 파일 손상, 저장 매체 장애 등으로 인해 손실된 데이터를 백업과 로그를 이용해 복원하는 작업입니다.
즉:
- 단순 트랜잭션 롤백/크래시 복구보다 더 큰 장애 범위
- 백업 + 아카이브 로그 + redo 적용 등의 절차가 중요
25. PK 크기가 커질수록 인덱스 크기가 커지는 이유#
InnoDB에서 보조 인덱스의 리프 엔트리에는 단순히 “보조 인덱스 값"만 있는 것이 아니라 해당 row를 찾기 위한 PK 값도 함께 저장됩니다.
예:
- PK =
INT(4 bytes) - 보조 인덱스 하나당 각 엔트리가 PK 4 bytes를 포함
그런데 PK가:
- 긴 문자열
- 복합 키
- UUID 문자열 형태
라면 모든 보조 인덱스 엔트리가 그만큼 더 커집니다.
영향#
- 인덱스 크기 증가
- 페이지당 저장 가능한 엔트리 수 감소
- B+트리 높이 증가 가능성
- 캐시 효율 하락
따라서 PK는 짧을수록 유리합니다.
26. MySQL 스레드와 OS 커널 스레드의 관계#
MySQL 서버는 내부적으로:
- 클라이언트 연결마다 연결되는 요청 처리용 스레드
- 백그라운드 작업용 스레드
를 사용합니다.
기본 connection thread model에서는 클라이언트 연결마다 전용 스레드가 붙고, 이 스레드들은 운영체제가 스케줄링하는 실행 단위 위에서 동작합니다.
동기화가 왜 필요한가#
여러 스레드가 동시에:
- 버퍼 풀
- 락 테이블
- 인덱스 구조
- 로그 버퍼
같은 공유 자원에 접근하면 충돌이 발생할 수 있습니다.
그래서:
- mutex
- semaphore
- latch
- rw-lock
같은 동기화 도구가 사용됩니다.
즉, 운영체제는 스레드 실행과 기본 동기화 원시 기능을 제공하고, MySQL은 이를 이용해 DB 내부 공유 자원을 보호합니다.
27. B-tree 인덱스의 “고유번호"와 실제 키 값#
대화 중 이 부분은 표현상 혼동이 있었습니다. 실무/교재에서 일반적으로 중요한 것은 다음입니다.
- 실제 키 값: 인덱스를 구성하는 컬럼 값
- 행 식별 정보:
- InnoDB 보조 인덱스라면 PK 값
- MyISAM이라면 데이터 파일 offset 같은 위치 정보
즉, 보통 학습에서 중요한 구분은 “인덱스 키” vs “그 키가 가리키는 실제 레코드 식별자” 입니다.
28. MyISAM vs InnoDB#
28-1. MyISAM의 특징#
- 트랜잭션 미지원
- 테이블 락 사용
- read-mostly 워크로드에 주로 사용
- 데이터와 인덱스를 별도 저장
28-2. MyISAM의 테이블 락#
MyISAM은 row lock이 아니라 table lock 기반입니다.
장점#
- 구현 단순
- 오버헤드 적음
단점#
- 동시 쓰기에 취약
- 쓰기 작업이 많으면 병목
28-3. 빠른 읽기 연산#
트랜잭션과 MVCC가 없고 테이블 락 기반이기 때문에, 역사적으로는 읽기 비중이 매우 큰 환경에서 쓰이곤 했습니다.
28-4. InnoDB는 느린가?#
단순 읽기 구조만 보면 MyISAM이 유리해 보일 수 있지만, 실제 현대 서비스 환경에서는 InnoDB의 장점이 훨씬 큽니다.
특히:
- PK 조회는 InnoDB 클러스터드 인덱스가 매우 빠름
- 트랜잭션 지원
- row-level locking
- crash recovery
- MVCC
그래서 일반 서비스에서는 거의 InnoDB가 기본 선택입니다.
29. Spring에서 MyISAM을 쓰면 트랜잭션을 못 쓰는가?#
DB 엔진 수준에서는 사실상 그렇습니다.
Spring의 @Transactional은 프레임워크 레벨에서 트랜잭션 경계를 잡아 주지만,
실제 DB가 트랜잭션을 지원하지 않으면 ACID 보장을 받을 수 없습니다.
즉:
- Spring이 어노테이션을 붙여도
- MyISAM 변경은 COMMIT/ROLLBACK 기반의 진짜 트랜잭션으로 보호되지 않음
- 따라서 롤백을 호출해도 MyISAM 변경은 되돌려지지 않을 수 있음
따라서 트랜잭션이 필요한 애플리케이션이라면 InnoDB가 필요합니다.
30. wait_timeout의 global 과 session 차이#
global#
새로 생성되는 세션의 기본값
session#
현재 연결된 특정 세션에만 적용되는 값
즉:
SET GLOBAL wait_timeout = ...→ 새 연결부터 영향SET SESSION wait_timeout = ...→ 현재 연결에만 영향
MySQL은 연결 스레드가 시작될 때 session wait_timeout 값을 global wait_timeout 또는 interactive_timeout 에서 초기화합니다.
31. MySQL의 세션(Session)은 무엇인가#
세션은 스키마 단위가 아니라, 클라이언트와 서버 간의 하나의 연결(connection) 을 의미합니다.
즉:
- 웹 서버의 DB 커넥션 하나 = DB 세션 하나
이 세션은 다음을 가집니다.
- 세션 변수
- 트랜잭션 상태
- 현재 default schema
- temporary table 등
32. Spring 서버에 100명이 요청을 보내면 세션은 하나인가?#
아닙니다. 보통은 DB 커넥션 풀(HikariCP 등)을 사용합니다.
즉:
- 요청 100개가 들어와도
- DB 세션은 커넥션 풀 크기만큼 운영될 수 있음
- 각 요청은 필요 시 풀에서 커넥션 하나를 빌려 사용
따라서 “Spring 서버와 DB 사이에 단 하나의 세션만 있다"는 것은 틀립니다.
33. In-memory DBMS vs 큰 페이지 캐시를 가진 디스크 기반 DBMS#
사용자가 제기한 질문은 매우 본질적입니다.
“페이지 전체를 메모리에 캐시한다면 디스크 기반 DBMS도 인메모리 DBMS와 똑같지 않나?”
겉보기에는 비슷해 보일 수 있지만, 구조적으로 다릅니다.
33-1. 단순 페이지 캐시와 인메모리 DB의 차이#
디스크 기반 DBMS는 기본적으로:
- 디스크 페이지 구조
- 버퍼 매니저
- 페이지 flush
- WAL 기반 recovery
- 페이지 포맷 유지
를 전제로 설계됩니다.
반면 인메모리 DBMS는 애초에:
- 포인터 기반 자료구조
- 캐시 친화적 레이아웃
- 디스크 페이지 형식 최소화
- 직렬화/역직렬화 최소화
등을 목표로 설계됩니다.
33-2. 직렬화 오버헤드 관점#
디스크 기반 DBMS는 데이터를 디스크에 영속화하기 위해 페이지 포맷, 로그 포맷, flush 규칙 등을 관리해야 합니다.
즉 메모리에 올라와 있어도:
- 디스크에 맞는 구조 유지
- 페이지 dirty 관리
- 체크포인트
- 직렬화 형태 고려
같은 오버헤드가 있습니다.
인메모리 DBMS는 이런 전통적 디스크 페이지 중심 제약이 훨씬 약합니다.
33-3. 데이터 레이아웃 유지 오버헤드#
디스크 기반 DBMS는 보통 페이지 단위 정합성을 유지해야 합니다.
예:
- 슬롯 디렉터리
- 페이지 헤더
- free space 관리
- page split
- page compaction
이런 레이아웃 관리 비용이 큽니다.
반면 인메모리 DBMS는 디스크 페이지 레이아웃에 덜 얽매여 CPU cache 친화적인 구조를 선택할 자유가 더 큽니다.
즉:
- “전부 메모리에 올려둔다"는 사실만 같을 뿐
- 내부 자료구조와 실행 엔진 최적화가 다릅니다.
34. 선택도(Selectivity) / 기수성(Cardinality)#
인덱스 키 값의 중복이 많아질수록:
- 기수성(cardinality) 은 낮아지고
- 선택도(selectivity) 도 낮아집니다
왜 중요하나#
선택도가 낮으면 특정 값으로 검색해도 많은 row가 나올 가능성이 크므로, 옵티마이저는 인덱스 사용의 이점을 낮게 평가할 수 있습니다.
예:
- 성별 컬럼 (
M,F) 인덱스는 선택도가 매우 낮음 - 이메일 컬럼은 거의 유일 → 선택도 매우 높음
35. 데이터베이스는 데이터를 어떻게 읽어오나#
전체 흐름을 정리하면 다음과 같습니다.
35-1. 파싱#
SQL 문법 분석
35-2. 최적화#
옵티마이저가 실행 계획 선택
35-3. 실행#
- 인덱스 탐색 or 풀스캔
- 필요한 페이지를 버퍼 풀에서 찾음
- 없으면 디스크에서 읽음
35-4. 결과 생성#
필터, 조인, 정렬, 집계 수행
35-5. 반환#
클라이언트에 결과 전송
핵심은 결국:
DBMS는 “페이지” 단위로 데이터를 읽고, 버퍼 풀/인덱스/옵티마이저를 이용해 디스크 I/O를 최소화한다.
36. 마지막으로 다시 정리하는 핵심 개념들#
36-1. InnoDB에서 중요한 것#
- PK = 클러스터드 인덱스
- 보조 인덱스 리프에는 PK가 들어감
- 트랜잭션/MVCC/UNDO/REDO 지원
- 락은 인덱스 기반으로 동작
- REPEATABLE READ에서 스냅샷 읽기 중요
36-2. 쿼리 최적화 관점 핵심#
SELECT *지양- 필요한 컬럼만 조회
- 존재 여부는
EXISTS고려 - 커버링 인덱스 여부 중요
- PK는 짧고 안정적일수록 좋음
36-3. 애플리케이션 관점 핵심#
- DB 트랜잭션이 자바 코드 전체의 원자성을 보장하는 것은 아님
- 멀티 인스턴스 환경에서는 JVM
synchronized만으로 충분하지 않음 - 데이터 무결성은 DB 제약조건과 트랜잭션 설계로 확보해야 함
37. 학습 포인트 요약 체크리스트#
아래 항목을 스스로 설명할 수 있으면 이번 대화의 핵심을 잘 이해한 것입니다.
- 랜덤 액세스가 무엇인지 설명할 수 있다.
- InnoDB에서 PK가 왜 물리적 저장 순서에 영향을 주는지 설명할 수 있다.
- PK가 커지면 왜 보조 인덱스도 커지는지 설명할 수 있다.
- B+트리 높이와 디스크 I/O 횟수의 관계를 설명할 수 있다.
- STEAL / NO-STEAL, FORCE / NO-FORCE를 교과서 개념으로 구분할 수 있다.
- REDO와 UNDO의 차이를 설명할 수 있다.
- INSERT의 UNDO가 왜 필요한지 설명할 수 있다.
- REPEATABLE READ에서 왜 다른 트랜잭션의 커밋을 못 볼 수 있는지 설명할 수 있다.
- 레코드 락, 갭 락, 넥스트 키 락을 구분할 수 있다.
-
SELECT *,SELECT email,COUNT(*),EXISTS중 존재 여부 확인에 무엇이 적절한지 설명할 수 있다. - MyISAM과 InnoDB 차이를 설명할 수 있다.
- Spring의 요청과 DB 세션(커넥션 풀)의 관계를 설명할 수 있다.
- 인메모리 DB와 디스크 기반 DB+큰 캐시의 차이를 설명할 수 있다.
38. 마무리#
이번 대화는 단순 문법보다 더 중요한, DBMS가 내부적으로 어떻게 동작하는지 를 이해하는 방향으로 진행되었습니다. 백엔드 개발자에게 특히 중요한 관점은 다음 세 가지입니다.
- 성능: 인덱스, 페이지, 랜덤 I/O, 커버링 인덱스
- 정합성: 트랜잭션, 락, MVCC, UNIQUE 제약
- 설계: PK 선택, 쿼리 형태, 애플리케이션-DB 역할 분리
이 세 가지를 계속 연결해서 공부하면 MySQL을 훨씬 깊이 이해할 수 있습니다.
참고 자료#
- MySQL 8.4 Reference Manual, Clustered and Secondary Indexes https://dev.mysql.com/doc/refman/8.4/en/innodb-index-types.html
- MySQL 8.4 Reference Manual, The Physical Structure of an InnoDB Index https://dev.mysql.com/doc/refman/8.4/en/innodb-physical-structure.html
- MySQL 8.4 Reference Manual, File Space Management https://dev.mysql.com/doc/refman/8.4/en/innodb-file-space.html
- MySQL 8.4 Reference Manual, Buffer Pool https://dev.mysql.com/doc/refman/8.4/en/innodb-buffer-pool.html
- MySQL 8.4 Reference Manual, Transaction Isolation Levels https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html
- MySQL 8.4 Reference Manual, Consistent Nonlocking Reads https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html
- MySQL 8.4 Reference Manual, Locking Reads https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html
- MySQL 8.4 Reference Manual, Locks Set by Different SQL Statements in InnoDB https://dev.mysql.com/doc/refman/8.4/en/innodb-locks-set.html
- MySQL 8.4 Reference Manual, Undo Logs https://dev.mysql.com/doc/refman/8.4/en/innodb-undo-logs.html
- MySQL 8.4 Reference Manual, Redo Log https://dev.mysql.com/doc/refman/8.4/en/innodb-redo-log.html
- MySQL 8.4 Reference Manual, Server System Variables (
wait_timeout) https://dev.mysql.com/doc/refman/8.4/en/server-system-variables.html - MySQL 8.4 Reference Manual, The MyISAM Storage Engine https://dev.mysql.com/doc/refman/8.4/en/myisam-storage-engine.html
- MySQL 8.4 Reference Manual, Connection Interfaces https://dev.mysql.com/doc/refman/8.4/en/connection-interfaces.html
- MySQL 8.4 Reference Manual, CREATE INDEX Statement https://dev.mysql.com/doc/refman/8.4/en/create-index.html
- Spring Framework Javadoc,
@Transactionalhttps://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/Transactional.html
