문서 읽는 데 58분 · D1

D-1: 면접에 나오는 데이터베이스

목차 27
전체 12강 중 10강 · CS 기초지식
난이도 · 중급

ℹ️컴퓨터 구조·운영체제·네트워크 — 언어 밑에 깔린 원리와 기술 면접 CS 질문 대비. 코딩에 어느 정도 익숙해진 뒤가 좋아요.

안녕하세요, 홍순구 튜터입니다. CS 기초 열 번째 시간, 오늘부터 D 카테고리 — 면접 기초로 들어갑니다.

지난 시간 마지막에 우리는 "URL을 치면 일어나는 일"의 전체 여정을 한 줄기로 꿰었죠. 그 여정의 한가운데에 서버 처리 단계가 있었고, 거기서 제가 한 줄 슬쩍 흘렸던 게 있습니다 — "서버는 요청을 받아 라우팅하고, 앱 로직을 돌리고, 필요하면 데이터베이스를 뒤진다"고요. 오늘은 그 박스를 엽니다. 서버가 뒤지는 그 데이터베이스가 정확히 무엇이고, 그 안에서 무슨 일이 벌어지는가를 파헤칩니다.

이 주제가 왜 중요하냐면, 우리가 이 과목 맨 처음에 던졌던 질문 "이 API가 왜 느려요?" 의 가장 흔한 답이 바로 여기 있기 때문이에요. 느린 API의 진짜 범인은 대개 네트워크가 아니라 데이터베이스 조회입니다. "인덱스를 안 걸어서", "트랜잭션이 서로 기다려서", "조인이 잘못돼서" — 면접관이 이런 꼬리를 물 때 원리로 답하는 사람과 막히는 사람이 갈립니다.

한 가지 먼저 선을 그어둘게요. 데이터베이스는 그 자체로 한 과목(database)을 꽉 채우는 큰 주제입니다. SQL을 직접 짜고, 정규화를 1단계부터 단계별로 분해하고, 실행 계획을 튜닝하는 깊이는 그 과목의 몫이에요. 오늘 우리가 다루는 건 "면접관이 물으면 원리로 답할 수 있다"까지 — ACID가 뭔지, 격리 수준이 왜 있는지, 인덱스가 왜 빠른지를 한 호흡에 설명하는 선까지입니다. SQL 문법은 거의 쓰지 않고 개념으로만 갑니다.

오늘 우리가 걸을 길은 이렇습니다.

텍스트
   오늘의 여정 — 면접에 나오는 데이터베이스

   ① 데이터베이스란         파일 대신 DBMS 를 쓰는 이유·관계형 모델
   ② 트랜잭션과 ACID        쪼개지지 않는 한 묶음의 일 (면접 1순위)
   ③ 세 가지 이상현상       동시에 돌면 생기는 사고
   ④ 트랜잭션 격리 수준     어디까지 막을 것인가 (면접 빈출)
   ⑤ 락과 MVCC             격리를 실제로 구현하는 두 방식
   ⑥ 인덱스                수억 행에서 한 줄을 즉시 찾는 법 (면접 빈출)
   ⑦ SQL vs NoSQL + CAP    정합성과 확장성의 갈림길 (면접 빈출)

①에서 "데이터베이스가 대체 뭔가"의 토대를 잡고, ②~⑤에서 데이터가 깨지지 않도록 지키는 장치(트랜잭션·격리·락)를 봅니다. ⑥에서 그 데이터를 빠르게 찾는 법(인덱스)을, ⑦에서 "그래서 어떤 데이터베이스를 고를 것인가"를 다룹니다.

💡 오늘 수업의 핵심 — "데이터베이스는 트랜잭션·ACID로 데이터가 깨지지 않게 지키고, 격리 수준으로 동시 실행의 사고를 막으며, 인덱스(B+트리)로 수억 행에서 한 줄을 빠르게 찾는다. SQL이냐 NoSQL이냐는 정합성과 확장성의 트레이드오프로 고른다" 🎯

🎯 학습 목표

  • 트랜잭션·ACID가 왜 필요한지를 계좌 이체 예로 설명하고, 원자성·일관성·고립성·지속성 네 글자를 한 호흡에 풀어냅니다.
  • 세 가지 이상현상(dirty/non-repeatable/phantom read)이 왜 생기는지 이해하고, 격리 수준 4단계가 각각 무엇을 막는지 표로 답합니다.
  • 인덱스가 왜 빠른지를 B+트리로 설명하고 그 트레이드오프를 짚으며, SQL과 NoSQL을 정합성·확장성으로 비교해 "언제 무엇"을 고를지 판단합니다.

Step 1: "데이터베이스란 — 파일이 아니라 DBMS를 쓰는 이유"

데이터베이스(database)는 글자 그대로 "데이터를 모아둔 곳"입니다. 그런데 여기서 면접관이 자주 던지는 첫 질문이 있어요 — "그냥 파일에 저장하면 안 되나요? 왜 굳이 데이터베이스를 쓰죠?" 이 질문에 답하는 게 오늘의 출발점입니다.

여러분이 좋아요 수를 그냥 텍스트 파일에 저장한다고 해봅시다. 평소엔 문제없어 보여요. 그런데 이런 일이 벌어집니다.

  • 동시에 여러 명이 좋아요를 누르면? 두 요청이 같은 파일을 동시에 고쳐 쓰다가 하나가 덮여 사라집니다.
  • 데이터가 많아지면? 1억 줄짜리 파일에서 특정 사용자를 찾으려면 처음부터 끝까지 다 읽어야 해요.
  • 쓰는 도중에 전원이 나가면? 반쯤 쓰다 만 파일이 깨진 채로 남습니다.

이 세 가지 — 동시 접근·빠른 검색·고장 복구 — 를 직접 코드로 다 해결하려면 평생을 써도 모자랍니다. 그래서 사람들이 이 문제를 전문으로 풀어주는 소프트웨어를 만들었는데, 그게 바로 DBMS(DataBase Management System, 데이터베이스 관리 시스템)예요. 우리가 흔히 "데이터베이스 쓴다"고 할 때, 사실은 이 DBMS를 쓰는 겁니다. MySQL·PostgreSQL·Oracle 같은 이름들이 전부 DBMS예요.

용어를 한 번에 정리하고 갈게요.

  • DB(데이터베이스): 데이터가 저장된 그 자체.
  • DBMS: 그 데이터를 관리해주는 소프트웨어(MySQL·PostgreSQL 등).
  • RDBMS(Relational DBMS, 관계형 DBMS): 데이터를 표(table) 형태로 다루는 DBMS. 우리가 면접에서 "데이터베이스" 하면 보통 이걸 떠올립니다.

관계형이라는 말의 핵심은 데이터를 표로 정리한다는 거예요. 표에는 (row, 가로 한 줄 = 한 건의 데이터)과 (column, 세로 한 줄 = 항목)이 있습니다.

텍스트
   users 테이블 — 행과 열, 그리고 키

   ┌─────┬───────────┬───────────┐
   │ id  │ username  │ followers │
   ├─────┼───────────┼───────────┤
   │  1  │ hong      │   1240    │
   │  2  │ kim       │   8500    │
   └─────┴───────────┴───────────┘
     세로 한 줄 = 열(컬럼) : id · username · followers
     가로 한 줄 = 행(레코드) : 한 사용자 한 건
     id = 기본키(PK) : 이 행을 유일하게 가리키는 값

여기서 두 가지 열쇠 개념이 나옵니다. 기본키(Primary Key, PK)는 각 행을 유일하게 구별하는 값이에요. 위 표에서 id가 그렇죠 — id=1이면 그건 무조건 한 사람입니다. 그리고 외래키(Foreign Key, FK)는 다른 표의 기본키를 가리키는 값입니다. 예를 들어 게시물 표에 author_id라는 열이 있어서 usersid를 가리키면, "이 게시물을 누가 썼나"를 연결할 수 있어요. 표와 표가 이렇게 키로 연결되는 게 "관계형"의 관계입니다.

표에 질문을 던지는 언어가 SQL(Structured Query Language)이에요. "팔로워가 1000명 넘는 사용자를 보여줘" 같은 질문을 SQL로 적으면 DBMS가 답을 찾아줍니다. SQL을 직접 짜는 법은 database 과목에서 깊게 다루니, 여기선 "표에 묻는 언어가 있다" 정도만 알아두면 됩니다.

마지막으로 정규화(normalization)를 한 입만 짚을게요. 한 표에 모든 걸 욱여넣으면 같은 데이터가 여기저기 중복돼요. 사용자 이름이 게시물마다 통째로 복사돼 있으면, 이름을 바꿀 때 수백 군데를 다 고쳐야 하고 하나라도 놓치면 데이터가 어긋납니다. 그래서 중복을 줄이려고 표를 적절히 나누는 작업이 정규화예요. 사용자 정보는 users 표에 한 번만 두고, 게시물은 author_id로 그 표를 가리키게 하는 식이죠. 1단계·2단계·3단계로 나누는 구체적인 절차는 database 과목의 몫이고, 여기선 "중복을 줄이려 표를 쪼갠다"는 직관만 가져갑니다.

🎯 면접에서는 이렇게 답하세요

"데이터베이스(DB)는 데이터를 저장한 것 자체이고, 그걸 관리해주는 소프트웨어가 DBMS입니다. 파일에 직접 저장하지 않고 DBMS를 쓰는 이유는 동시 접근 제어, 빠른 검색, 고장 복구, 무결성 보장을 전문으로 해결해주기 때문이에요. 그중 데이터를 표(행·열)로 다루고 표끼리 키로 연결하는 게 관계형 DBMS(RDBMS)이고, 표를 질의하는 언어가 SQL입니다. 각 행을 유일하게 구별하는 게 기본키, 다른 표를 가리키는 게 외래키예요."

🙋 학생 질문 — "튜터님, 엑셀도 표인데 데이터베이스랑 뭐가 다른가요?"

아주 좋은 질문이에요. 겉모습은 둘 다 행과 열의 표라 비슷해 보이죠. 결정적인 차이는 "여러 사람이 동시에, 안전하게, 빠르게" 다룰 수 있느냐예요.

엑셀은 기본적으로 한 사람이 한 파일을 여는 도구입니다. 수백 명이 동시에 같은 셀을 고치거나, 수억 행에서 조건에 맞는 행을 0.01초 만에 찾거나, 고치는 도중 전원이 나가도 데이터가 안 깨지게 보장하는 — 이런 일에는 맞지 않아요. DBMS는 바로 그걸 위해 트랜잭션(다음 Step), 인덱스(Step 6), 동시성 제어(Step 5)를 갖췄습니다. 즉 엑셀은 "사람이 눈으로 보고 다루는 표"이고, 데이터베이스는 "프로그램이 동시에·대량으로·안전하게 다루는 표"라고 보면 됩니다.


Step 2: "트랜잭션과 ACID — 쪼개지지 않는 한 묶음의 일"

이번 Step의 주제는 데이터베이스 면접에서 가장 자주 나오는 단어, 트랜잭션과 ACID입니다.

트랜잭션(transaction)은 쪼개면 안 되는 한 묶음의 작업이에요. 가장 유명한 예가 계좌 이체입니다. A가 B에게 1000원을 보낸다고 해봅시다. 이건 사실 두 단계예요.

  1. A 계좌에서 1000원을 뺀다(출금).
  2. B 계좌에 1000원을 더한다(입금).

문제는, 1번만 되고 2번이 실패하면? A에서 돈은 빠졌는데 B에 안 들어갔으니 1000원이 허공으로 증발합니다. 반대로 2번만 되면 돈이 복사돼버려요. 그래서 이 둘은 반드시 함께 성공하거나, 함께 실패해야 합니다. 이 "전부 아니면 전무"(all-or-nothing)의 한 묶음이 트랜잭션이에요.

텍스트
   계좌 이체 트랜잭션 — 둘 다 되거나, 둘 다 안 되거나

   BEGIN  (트랜잭션 시작)
     │
     ├─ ① A 계좌에서 1000원 출금
     │
     ├─ ② B 계좌에 1000원 입금
     │
     ▼
   ┌─ 둘 다 성공      ─▶ COMMIT   : 변경을 확정한다
   └─ 하나라도 실패   ─▶ ROLLBACK : 전부 없던 일로 되돌린다

여기서 두 단어가 나왔죠. 커밋(commit)은 "여기까지 다 성공했으니 확정"이고, 롤백(rollback)은 "문제 생겼으니 처음으로 되돌리기"입니다. 이체 도중 2번에서 오류가 나면 롤백이 1번 출금까지 깨끗이 되돌려, 마치 아무 일도 없던 것처럼 만들어줘요.

그럼 이 트랜잭션이 제대로 동작하기 위해 보장해야 하는 네 가지 성질이 바로 ACID입니다. 면접 1순위 단어라 하나씩 또렷이 짚을게요.

  • A — 원자성(Atomicity): 트랜잭션은 더 쪼갤 수 없는 원자처럼, 다 되거나 다 안 되거나 둘 중 하나. 위의 이체가 정확히 이거예요. 중간 상태("출금만 됨")는 허용되지 않습니다.
  • C — 일관성(Consistency): 트랜잭션 전후로 데이터가 정해진 규칙을 깨지 않는다. "잔액은 음수가 될 수 없다", "총합은 보존된다" 같은 규칙이 트랜잭션이 끝나도 여전히 지켜진 상태여야 해요.
  • I — 고립성(Isolation): 여러 트랜잭션이 동시에 돌아도, 서로 간섭하지 않고 혼자 도는 것처럼 보여야 한다. 이게 안 지켜지면 다음 Step의 이상현상이 터집니다. (그래서 ③④에서 이걸 깊이 팝니다.)
  • D — 지속성(Durability): 한 번 커밋된 결과는 전원이 나가도 사라지지 않는다. 디스크에 안전하게 기록되니까요.

이 넷을 한 줄로 묶으면 — "트랜잭션은 원자적으로(A), 일관성을 지키며(C), 서로 고립된 채(I), 커밋되면 영구히 남게(D) 동작한다" 입니다. 면접에서 ACID를 물으면 네 글자를 나열만 하지 말고, 계좌 이체 하나로 네 성질을 다 엮어 설명하면 깊이가 확 살아요.

🎯 면접에서는 이렇게 답하세요

"트랜잭션은 쪼갤 수 없는 한 묶음의 작업입니다. 계좌 이체처럼 출금과 입금이 반드시 함께 성공하거나 함께 실패해야 하죠. 그 트랜잭션이 지켜야 할 네 성질이 ACID입니다. 원자성은 다 되거나 다 안 되거나(중간 없음), 일관성은 끝난 뒤에도 데이터 규칙이 안 깨짐, 고립성은 동시에 돌아도 혼자 도는 것처럼 보임, 지속성은 커밋되면 전원이 나가도 남는다는 뜻이에요. 실패하면 롤백으로 전부 되돌리고, 성공하면 커밋으로 확정합니다."

🙋 학생 질문 — "튜터님, 그냥 코드에서 if-else로 '출금 성공하면 입금해라'라고 짜면 트랜잭션 없이도 되지 않나요?"

머리로는 그렇게 생각하기 쉬운데, 현실은 더 까다로워요. 두 가지가 문제입니다.

첫째, 중간에 죽으면? if-else로 출금까지 하고 입금하기 직전에 서버가 멈추거나 전원이 나가면, 출금은 이미 디스크에 기록됐는데 입금은 영영 안 됩니다. 코드의 if-else는 "되돌리기" 능력이 없어요. 트랜잭션은 DBMS가 미리 기록(로그)을 남겨뒀다가 자동으로 롤백해줍니다.

둘째, 동시에 여러 명이 같은 계좌를 건드리면? 코드만으로는 두 이체가 같은 잔액을 동시에 읽어 둘 다 성공시켜버리는 사고를 막기 어려워요. 트랜잭션은 고립성(다음 Step)으로 이걸 처리합니다. 그래서 "되돌릴 수 있고, 동시 실행에서도 안전한" 보장을 직접 코드로 구현하는 대신, DBMS의 트랜잭션에 맡기는 거예요.


Step 3: "세 가지 이상현상 — 동시에 돌면 생기는 사고"

방금 ACID의 고립성(I)이 "동시에 돌아도 혼자 도는 것처럼 보여야 한다"고 했죠. 그런데 만약 고립성을 느슨하게 풀면 어떤 사고가 나는지를 봐야, 다음 Step의 격리 수준이 왜 있는지 이해됩니다. 이 Step은 "고립을 안 하면 생기는 세 가지 문제" — 이상현상(anomaly)을 봅니다.

상황은 항상 똑같아요. 두 트랜잭션 T1·T2가 같은 데이터를 동시에 만질 때 벌어집니다. 셋을 차례로 볼게요.

① Dirty Read(더티 리드) — 커밋도 안 된 값을 읽어버린다

시간 T1 (송금 트랜잭션) T2 (잔액 조회)
잔액 1000 → 900 으로 수정 (아직 커밋 전)
잔액 조회 → 900 을 읽음
오류 발생 → ROLLBACK (잔액은 1000 으로 복귀)
T2 는 존재한 적 없는 900 을 본 셈

T1이 아직 확정(커밋)하지도 않은 중간 값을 T2가 읽었어요. 그런데 T1이 롤백해버리면, T2가 읽은 900은 세상에 존재한 적 없는 유령 값이 됩니다. 이게 더티 리드예요. "더럽다(dirty)"는 건 "아직 커밋 안 된 지저분한 값"이라는 뜻입니다.

② Non-Repeatable Read(반복 불가능한 읽기) — 같은 행을 다시 읽으니 값이 바뀐다

T1이 한 트랜잭션 안에서 같은 행을 두 번 읽었는데, 그 사이에 T2가 그 행을 수정하고 커밋해버립니다. 그래서 T1은 처음엔 1000, 두 번째엔 900을 봐요. 같은 데이터를 같은 트랜잭션에서 읽었는데 값이 달라진 거죠. "한 트랜잭션 안에서는 같은 읽기가 같은 결과를 줘야 한다"는 기대가 깨졌습니다.

③ Phantom Read(팬텀 리드) — 같은 조건으로 다시 조회하니 행 개수가 바뀐다

T1이 "팔로워 1000명 넘는 사용자"를 두 번 조회했는데, 그 사이에 T2가 그 조건에 맞는 행을 새로 추가(또는 삭제)하고 커밋합니다. 그래서 처음엔 5명이었는데 두 번째엔 6명이 나와요. 없던 행이 유령(phantom)처럼 나타난 거죠.

②와 ③이 헷갈리기 쉬운데, 핵심 차이는 이겁니다 — ②는 기존 행의 "값"이 바뀐 것, ③은 조건에 맞는 행의 "개수"가 바뀐 것. 셋을 한 표로 정리할게요.

이상현상 무엇이 문제인가 한 줄 정리
Dirty Read 커밋 안 된 값을 읽음 롤백되면 유령 값을 본 셈
Non-Repeatable Read 같은 을 다시 읽으니 이 바뀜 중간에 누가 수정·커밋
Phantom Read 같은 조건으로 다시 조회하니 행 수가 바뀜 중간에 누가 삽입·삭제

이 세 사고를 "어디까지 막을 것이냐"를 단계로 정해둔 게 다음 Step의 격리 수준입니다. 위로 갈수록 가벼운 사고, 아래로 갈수록 막기 어려운 사고예요.

🎯 면접에서는 이렇게 답하세요

"트랜잭션을 동시에 돌릴 때 고립을 느슨하게 하면 세 가지 이상현상이 생깁니다. Dirty Read는 아직 커밋 안 된 값을 읽는 것, Non-Repeatable Read는 같은 을 다시 읽었더니 이 바뀐 것, Phantom Read는 같은 조건으로 다시 조회했더니 행 개수가 바뀐 것이에요. 핵심 구분은 Non-Repeatable은 '값이 변함', Phantom은 '행 수가 변함'입니다. 이 셋을 어디까지 막느냐가 트랜잭션 격리 수준이고요."

🙋 학생 질문 — "튜터님, Non-Repeatable Read랑 Phantom Read가 계속 헷갈려요. 둘 다 '다시 읽으니 달라졌다'잖아요?"

맞아요, 둘 다 "다시 읽으니 달라졌다"라서 헷갈리는 게 당연합니다. 딱 한 단어로 구분하세요 — "값"이냐 "개수"냐.

Non-Repeatable Read이미 있던 한 행의 값이 바뀐 거예요. "7번 사용자의 잔액을 두 번 읽었는데 1000 → 900으로 달라졌다." 행은 그대로 있고, 그 안의 숫자가 변한 거죠.

Phantom Read조건에 맞는 행의 개수가 바뀐 거예요. "팔로워 1000명 넘는 사용자를 두 번 셌는데 5명 → 6명으로 달라졌다." 기존 행의 값이 아니라, 새 행이 끼어들었거나 빠진 겁니다. 그래서 "유령(phantom)"이라는 이름이 붙었어요 — 없던 행이 갑자기 나타나니까요. 정리하면 수정(UPDATE) 때문이면 Non-Repeatable, 삽입·삭제(INSERT/DELETE) 때문이면 Phantom입니다.


Step 4: "트랜잭션 격리 수준 — 어디까지 막을 것인가"

이제 핵심입니다. 앞에서 본 세 이상현상을 어느 선까지 막을지 정하는 게 트랜잭션 격리 수준(isolation level)이에요. 면접에서 "트랜잭션 격리 수준 4단계를 말해보세요"는 거의 단골이라, 표 하나를 통째로 머리에 넣고 가는 게 목표입니다.

먼저 왜 "수준"으로 나누느냐부터요. 고립을 강하게 할수록 사고는 줄지만, 그만큼 트랜잭션끼리 서로 기다리게 돼서 동시에 처리할 수 있는 양(동시성)이 떨어지고 느려집니다. 반대로 고립을 풀면 빠르지만 사고 위험이 커지죠. 그래서 "안전 ↔ 성능"을 저울질해 네 단계 중 하나를 고릅니다.

  • READ UNCOMMITTED(레벨 0) — 커밋 안 된 값도 그냥 읽음. 아무것도 안 막아요. 더티 리드까지 다 허용. 거의 실무에서 안 씁니다.
  • READ COMMITTED(레벨 1)커밋된 값만 읽음. 더티 리드를 막아요. 가장 널리 쓰이는 현실적인 기본값(Oracle·PostgreSQL의 기본).
  • REPEATABLE READ(레벨 2) — 한 트랜잭션 안에서 같은 행은 항상 같은 값으로 보임. 더티 + Non-Repeatable까지 막아요. (MySQL InnoDB의 기본).
  • SERIALIZABLE(레벨 3) — 트랜잭션들을 완전히 한 줄로 세워 차례차례 실행한 것처럼 보장. 팬텀까지 다 막는 가장 안전한 단계지만, 가장 느립니다.

이걸 이상현상과 교차한 표가 면접의 핵심 그림이에요.

격리 수준 Dirty Read Non-Repeatable Phantom 실무 비고
READ UNCOMMITTED 허용 허용 허용 거의 안 씀
READ COMMITTED 방지 허용 허용 Oracle·PostgreSQL 기본
REPEATABLE READ 방지 방지 허용* MySQL(InnoDB) 기본
SERIALIZABLE 방지 방지 방지 가장 안전·가장 느림

읽는 법은 간단해요 — 아래로 내려갈수록 막는 게 늘고, 그만큼 느려집니다. 위에서 아래로 "더티 → Non-Repeatable → 팬텀"이 차례로 차단돼요.

표에 별표(*) 하나 달아뒀죠. 표준 정의상 REPEATABLE READ는 팬텀을 허용하지만, MySQL의 InnoDB는 갭 락이라는 기법으로 팬텀까지 상당 부분 막아줍니다. 면접에서 "MySQL 기본 격리 수준에서 팬텀이 나나요?"라는 꼬리가 오면 이 한 줄로 받으면 깊이가 드러나요. (구체적인 갭 락 동작은 database 과목의 영역입니다.)

🎯 면접에서는 이렇게 답하세요

"격리 수준은 동시 실행의 이상현상을 어디까지 막을지 정하는 네 단계입니다. READ UNCOMMITTED는 아무것도 안 막고, READ COMMITTED는 더티 리드를 막고(Oracle·PostgreSQL 기본), REPEATABLE READ는 Non-Repeatable까지 막고(MySQL 기본), SERIALIZABLE은 팬텀까지 다 막아요. 아래로 갈수록 안전하지만 동시성이 떨어져 느려지므로, 실무에선 보통 READ COMMITTED나 REPEATABLE READ를 기본으로 쓰고 정말 엄격한 정합성이 필요한 일부 트랜잭션만 높입니다."

🙋 학생 질문 — "튜터님, 그럼 그냥 무조건 제일 안전한 SERIALIZABLE을 쓰면 제일 좋은 거 아닌가요?"

안전만 보면 그렇지만, 성능이라는 대가가 있어서 무조건 최고 단계로 가지 않아요. 이게 이 주제의 핵심 트레이드오프입니다.

SERIALIZABLE은 트랜잭션들을 사실상 한 줄로 세워 차례차례 실행한 것처럼 보장합니다. 그러려면 트랜잭션끼리 서로 많이 기다려야 해요(락을 더 오래·넓게 잡거나, 충돌 시 재시도). 동시에 1만 명이 들어오는 서비스에서 모든 트랜잭션을 이렇게 줄 세우면, 안전은 완벽해도 처리량이 바닥나고 응답이 느려집니다.

그래서 현실은 "대부분의 트랜잭션은 READ COMMITTED/REPEATABLE READ로 빠르게, 돈 계산처럼 절대 틀리면 안 되는 일부만 격리를 높인다"는 식으로 갑니다. 안전과 성능 사이에서 필요한 만큼만 격리하는 거죠 — 이게 면접에서 "왜 무조건 SERIALIZABLE을 안 쓰나요?"의 모범 답입니다.


Step 5: "락과 MVCC — 격리를 실제로 구현하는 두 방식"

격리 수준이 "무엇을 막을지"의 목표였다면, 이번 Step은 그걸 실제로 어떻게 구현하는가입니다. 크게 두 방식이 있어요 — (비관적)과 MVCC(낙관적에 가까운 방식).

락(Lock) — 데이터에 자물쇠를 채운다

가장 직관적인 방법은 "내가 쓰는 동안 남이 못 건드리게 잠그는" 거예요. 두 종류가 있습니다.

  • 공유락(Shared Lock, S락)읽기용. 여러 트랜잭션이 동시에 같은 데이터를 읽는 건 서로 방해가 안 되니, 공유락끼리는 함께 잡을 수 있어요.
  • 배타락(Exclusive Lock, X락)쓰기용. 누가 데이터를 고치는 동안엔 혼자만 잡아야 합니다. 다른 읽기·쓰기는 다 기다려야 해요.
보유 \ 요청 공유락(S) 요청 배타락(X) 요청
공유락(S) 보유 가능 대기
배타락(X) 보유 대기 대기

이 표의 결론은 한 줄이에요 — 읽기끼리는 OK, 쓰기가 끼면 무조건 대기. 그런데 락에는 두 가지 비용이 따릅니다. 첫째, 누가 잠그면 다른 쪽이 기다려야 하니 동시성이 떨어져요. 둘째 — B-4에서 배운 그 데드락이 여기서도 똑같이 일어납니다. T1은 A를 잠그고 B를 기다리는데, T2는 B를 잠그고 A를 기다리면, 둘 다 영원히 멈추죠. 좁은 사거리에서 서로 마주 보고 못 가는 그 교착이에요. 다행히 DBMS는 이런 데드락을 자동으로 탐지해서 한 트랜잭션을 강제 롤백시켜 풀어줍니다.

MVCC(다중 버전 동시성 제어) — 덮어쓰지 않고 버전을 쌓는다

락의 약점은 "읽기조차 쓰기에 막힐 수 있다"는 거예요. 이걸 풀려고 나온 게 MVCC(Multi-Version Concurrency Control)입니다. 핵심 발상은 데이터를 고칠 때 기존 값을 덮어쓰지 않고, 새 버전을 따로 만든다는 거예요. 그러면 읽는 쪽은 자기 트랜잭션이 시작된 시점의 버전(스냅샷) 을 보면 되니, 누가 동시에 고치고 있어도 기다리지 않고 일관된 값을 읽을 수 있습니다.

텍스트
   MVCC — 덮어쓰지 않고 버전을 쌓는다

   row #7 (잔액)
     ├─ v1 : 1000    T0 가 커밋한 버전
     ├─ v2 : 900     T1 이 수정 중 (아직 커밋 전)
     │
     읽기 T2 ──▶ 자기 시작 시점 스냅샷(v1)을 본다  1000
                 (T1 을 기다리지 않고, T1 의 미완성 값도 안 본다)

이 방식의 힘은 "읽기가 쓰기를 막지 않고, 쓰기가 읽기를 막지 않는다" 예요. 그래서 읽기가 많은 서비스에서 동시성이 확 올라갑니다. 오늘날 대표적인 RDBMS인 PostgreSQL과 MySQL(InnoDB)이 모두 MVCC를 기반으로 동작해요. 다만 MVCC도 만능은 아니라, 쓰기끼리 충돌할 때는 결국 락이나 충돌 감지가 필요합니다. 그래서 실제 DBMS는 MVCC와 락을 함께 씁니다.

🎯 면접에서는 이렇게 답하세요

"격리를 구현하는 방식은 크게 MVCC입니다. 락은 데이터에 자물쇠를 채우는 방식으로, 읽기용 공유락은 서로 함께 잡을 수 있지만 쓰기용 배타락이 끼면 다른 작업은 기다려야 해요. 단점은 동시성 저하와 데드락 가능성입니다. MVCC는 데이터를 덮어쓰지 않고 버전을 여러 개 유지해서, 읽기는 자기 시점의 스냅샷을 보게 합니다. 덕분에 읽기가 쓰기를 막지 않아 동시성이 좋아요. PostgreSQL·MySQL InnoDB가 MVCC 기반이고, 쓰기 충돌 처리를 위해 락과 함께 씁니다."

🙋 학생 질문 — "튜터님, MVCC가 그렇게 좋으면 락은 왜 아직도 쓰나요?"

좋은 의문이에요. MVCC가 읽기 동시성에선 분명 강점이지만, 쓰기끼리 부딪히는 순간엔 결국 누가 이기고 누가 기다릴지를 정해야 하거든요. 그 조정을 해주는 게 락입니다.

예를 들어 두 트랜잭션이 같은 행을 동시에 수정하려 하면, MVCC만으로는 "둘 다 자기 버전을 만들었는데 누구 걸 최종으로 할까?"가 정해지지 않아요. 이때는 한쪽이 행에 배타락을 잡고, 다른 쪽은 그게 끝날 때까지 기다리거나 충돌로 실패합니다. 또 "지금 이 행을 읽되 남이 못 바꾸게 잠가두고 싶다"는 명시적 잠금(예: 재고 차감 같은 민감한 계산)이 필요할 때도 락을 직접 씁니다.

그래서 MVCC와 락은 경쟁 관계가 아니라 역할 분담이에요 — 읽기는 MVCC로 막힘 없이, 쓰기 충돌은 락으로 안전하게. 둘을 같이 써서 "읽기 많고 쓰기는 가끔 부딪히는" 현실 워크로드를 효율적으로 처리하는 겁니다.


Step 6: "인덱스 — 수억 행에서 한 줄을 즉시 찾는 법"

이제 처음에 약속한 "이 API가 왜 느려요?" 의 핵심 답으로 들어갑니다. 느린 조회의 가장 흔한 범인이자, 면접 단골 질문 "인덱스가 왜 빠른가요?" 를 다룰 차례예요.

상황부터 봅시다. 게시물이 1억 개 있는 표에서 "id가 73,920,041인 게시물"을 찾으려면, 인덱스가 없으면 DBMS는 첫 행부터 끝까지 한 줄씩 다 비교합니다. 이걸 풀 스캔(full scan)이라고 해요. 운 나쁘면 1억 번을 봐야 하니 O(n)이고, 데이터가 커질수록 그대로 느려집니다.

인덱스(index)는 책 뒤에 붙은 색인과 똑같은 발상이에요. 두꺼운 책에서 "트랜잭션"이 나오는 쪽을 찾을 때, 첫 장부터 넘기지 않고 색인에서 "트랜잭션 … 152쪽"을 보고 바로 펴죠. 데이터베이스도 자주 찾는 열의 값을 미리 정렬해 따로 저장해두고, 그걸로 위치를 바로 짚습니다.

그런데 그냥 정렬만 해두면 될 것 같은데, 왜 하필 B+트리(B+ tree)라는 구조를 쓸까요? 이게 면접의 핵심이에요.

텍스트
   B+트리 인덱스 — 정렬된 키로 범위를 좁혀 간다

   루트     [ 30 | 60 ]                  한 노드에 키가 여러 개  높이가 낮다
              │
              ├─ 30 미만  ─▶ 잎 [ 10  20 ]
              ├─ 30~59    ─▶ 잎 [ 30  50 ]
              └─ 60 이상  ─▶ 잎 [ 60  90 ]
                              └─ 잎끼리 정렬 순서로 연결  범위 검색이 빠르다

핵심을 세 가지로 짚을게요.

  • 높이가 낮다. 한 노드에 키를 여러 개 담으니, 트리가 옆으로 넓고 위아래로 낮아요. 실제 B+트리는 3~4단 깊이로 수억 건을 담습니다. 찾을 때 위에서 아래로 단계마다 범위를 좁혀 내려가니 O(log n) — 1억 건이라도 몇 단계면 닿아요. (이 O(log n)은 다음 시간 자료구조에서 다시 만납니다.)
  • 디스크에 맞다. 데이터베이스는 디스크에서 읽는데, 디스크는 한 번에 블록 단위로 읽어요. B+트리의 한 노드를 그 블록 크기에 맞추면, 한 번 읽을 때 키를 무더기로 가져와 디스크 접근 횟수를 줄입니다. 이진 트리(노드당 키 1개)면 높이가 너무 높아져 디스크를 수십 번 들락거려야 해서 느려요.
  • 범위 검색이 빠르다. B+트리는 잎 노드끼리 정렬된 순서로 연결돼 있어요. "팔로워 30~60인 사용자"를 찾으면, 30이 든 잎을 찾은 뒤 옆으로 쭉 훑으면 끝입니다. 정렬돼 있으니 "큰 순서로 정렬", "범위" 같은 질의에도 강해요.

그럼 인덱스를 무조건 많이 걸면 좋을까요? 아닙니다. 트레이드오프가 있어요.

  • 쓰기가 느려진다. 데이터를 넣고 고칠 때마다 인덱스(정렬된 트리)도 같이 갱신해야 하니, INSERT·UPDATE·DELETE가 느려져요.
  • 공간을 더 쓴다. 인덱스도 결국 따로 저장하는 데이터라 디스크를 차지합니다.
  • 남발하면 역효과. 안 쓰는 인덱스까지 잔뜩 걸면 쓰기만 느려지고 이득은 없어요. 또 값 종류가 적은 열(예: 성별처럼 두세 가지)엔 인덱스 효과가 약합니다 — 절반을 걸러줘봤자 결국 많은 행을 읽으니까요.

그래서 인덱스는 "자주 조회 조건으로 쓰이고, 값 종류가 다양한 열에 선별적으로" 거는 게 정석입니다. "이 API가 느려요"를 받으면, 잘하는 사람은 "조회 조건 열에 인덱스가 걸려 있나, 혹시 풀 스캔이 도나"를 먼저 의심해요.

🎯 면접에서는 이렇게 답하세요

"인덱스는 책 뒤의 색인과 같아요. 자주 찾는 열의 값을 미리 정렬해 따로 저장해두고 위치를 바로 짚어서, 풀 스캔(O(n))을 O(log n) 으로 줄입니다. 구조로는 B+트리를 쓰는데, 한 노드에 키를 여러 개 담아 높이가 낮고(3~4단으로 수억 건), 노드를 디스크 블록 크기에 맞춰 접근 횟수를 줄이며, 잎이 정렬 연결돼 범위 검색에도 강하기 때문이에요. 단, 인덱스는 쓰기마다 갱신돼 INSERT/UPDATE를 느리게 하고 공간도 차지하니, 자주 조회되고 값이 다양한 열에 선별적으로 겁니다."

🙋 학생 질문 — "튜터님, 그럼 모든 열에 인덱스를 걸어두면 조회는 다 빨라지니 좋은 거 아닌가요?"

조회만 보면 솔깃하지만, 실제로 그렇게 하면 전체적으로 더 느려지는 경우가 많아요. 인덱스에는 분명한 대가가 있거든요.

인덱스는 "정렬된 사본"이라, 데이터를 하나 넣거나 고칠 때마다 관련 인덱스를 전부 같이 갱신해야 합니다. 열 10개에 다 인덱스를 걸면, 행 하나를 INSERT할 때 인덱스 10개를 동시에 손봐야 해요 — 쓰기가 그만큼 느려집니다. 게다가 인덱스마다 디스크 공간을 차지하니 저장 비용도 커지고요.

그리고 안 쓰는 인덱스는 순수 손해예요. 조회에 한 번도 안 쓰이는데 쓰기 비용만 늘리니까요. 또 값 종류가 적은 열(성별·참/거짓 같은)은 인덱스를 걸어도 후보를 충분히 못 줄여서 효과가 약합니다. 그래서 실무에선 "실제 느린 조회의 조건 열"을 찾아 거기에만 선별적으로 거는 게 원칙이에요. 인덱스는 공짜가 아니라 읽기를 빠르게 하는 대신 쓰기를 느리게 하는 트레이드오프라는 걸 기억하면 됩니다.


Step 7: "SQL이냐 NoSQL이냐 — 그리고 CAP 한 입"

마지막 Step은 "그래서 어떤 데이터베이스를 고를 것인가"입니다. 면접에서 "SQL과 NoSQL의 차이가 뭐고 언제 무엇을 쓰나요?" 로 자주 나오죠. 둘을 정합성·확장성의 트레이드오프로 비교하고, 분산 데이터베이스의 CAP 정리를 한 입만 곁들이겠습니다.

지금까지 본 RDBMS는 전부 SQL 진영입니다. 정해진 표 구조(스키마)에 데이터를 넣고, 표끼리 키로 연결하고, 트랜잭션·ACID로 정합성을 강하게 지켜요. 반면 NoSQL(Not Only SQL)은 이 관계형 틀을 벗어난 데이터베이스들을 묶어 부르는 말이에요. 종류가 여럿입니다.

  • 문서형(Document): JSON 같은 문서 단위로 저장 (MongoDB).
  • 키-값형(Key-Value): 키 하나에 값 하나, 아주 빠름 (Redis).
  • 컬럼형(Wide-Column): 대용량을 열 단위로 (Cassandra).
  • 그래프형(Graph): 관계(친구·연결)를 노드와 간선으로 (Neo4j).

둘의 성격을 표로 비교할게요.

구분 SQL (RDBMS) NoSQL
데이터 구조 정해진 표·스키마 유연 (문서·키값·컬럼·그래프)
관계 표끼리 키로 연결·조인 강함 관계·조인 약함(앱에서 처리)
트랜잭션 ACID 강함 제한적(제품마다 다름)
확장 수직 확장(서버 키우기)에 유리 수평 확장(서버 늘리기)에 유리
어울리는 곳 정합성·복잡한 관계가 중요 대용량·유연한 구조·읽기 폭주

여기서 확장 이야기를 짚고 넘어가요. 데이터가 폭증할 때 서버를 키우는 방법은 두 가지입니다. 수직 확장(scale-up)은 한 서버의 CPU·메모리를 더 좋은 걸로 갈아끼우는 거예요 — 단순하지만 한 대로 버틸 수 있는 한계가 있죠. 수평 확장(scale-out)은 서버 대수를 늘려 데이터를 나눠 담는 거예요 — 거의 무한히 늘릴 수 있지만, 여러 대에 데이터가 흩어지니 관계·트랜잭션을 맞추기 까다롭습니다. NoSQL은 애초에 이 수평 확장을 쉽게 하려고 관계·정합성을 좀 양보한 설계예요.

여러 대에 데이터가 흩어진 분산 데이터베이스라면 피할 수 없는 게 CAP 정리입니다. 분산 시스템은 세 가지를 동시에 다 가질 수 없고, 셋 중 둘만 고를 수 있다는 이론이에요.

텍스트
   CAP 정리 — 분산 DB 는 셋 중 둘만

              C
             / \
            /   \
           /     \
          A ───── P

   C (Consistency·일관성)    : 어느 노드에서 읽어도 같은 최신 값
   A (Availability·가용성)   : 일부가 죽어도 항상 응답은 한다
   P (Partition tolerance·분할 내성) : 노드 사이 통신이 끊겨도 동작한다

   네트워크가 끊기면(P 는 분산이면 사실상 필수)  C 와 A 중 하나를 포기
     · CP : 일관성을 지키고, 확신 없으면 응답을 거부
     · AP : 일단 응답하고, 데이터는 나중에 맞춘다

분산 환경에서는 노드 사이 통신이 끊기는 일(분할, Partition)이 반드시 일어나므로 P는 사실상 포기할 수 없어요. 그래서 현실의 선택은 "끊겼을 때 C를 지킬까(CP), A를 지킬까(AP)" 로 좁혀집니다. 은행처럼 틀리면 안 되는 곳은 CP(차라리 응답을 멈춤)를, "좋아요 수가 잠깐 1~2개 어긋나도 일단 보여주는 게 나은" 곳은 AP를 택하는 식이죠.

마지막으로 한 가지 — "NoSQL이 더 최신이니 더 좋다"는 함정을 조심하세요. NoSQL은 SQL의 상위 호환이 아니라 다른 트레이드오프를 고른 선택입니다. 정합성·복잡한 관계가 중요하면 여전히 SQL이 정답이고, 대용량·유연성·수평 확장이 중요하면 NoSQL이 빛나요. 요즘 큰 서비스는 한쪽만 쓰지 않고 용도별로 섞어 씁니다(핵심 거래는 SQL, 캐시·로그·세션은 NoSQL). 면접에서 둘을 우열로 답하지 말고 트레이드오프로 답하면 좋은 인상을 줍니다.

🎯 면접에서는 이렇게 답하세요

"SQL(RDBMS) 은 정해진 스키마와 표·관계를 두고 ACID로 정합성을 강하게 지키며, 보통 수직 확장에 유리해요. NoSQL 은 스키마가 유연하고(문서·키값·컬럼·그래프) 수평 확장이 쉬워 대용량·읽기 폭주에 강하지만, 관계·트랜잭션은 약합니다. 그래서 정합성과 복잡한 관계가 중요하면 SQL, 대용량·유연성·수평 확장이 중요하면 NoSQL을 택해요. 분산이면 CAP 정리상 통신 단절 시 일관성(CP)이냐 가용성(AP)이냐를 골라야 하고요. 실무에선 한쪽만이 아니라 용도별로 섞어 씁니다."

🙋 학생 질문 — "튜터님, NoSQL이 수평 확장도 쉽고 더 최신이라던데, 그냥 NoSQL만 쓰면 안 되나요?"

흔히 빠지는 오해예요. NoSQL은 "더 좋은 다음 세대"가 아니라 "다른 문제를 풀려고 다른 걸 양보한" 선택입니다. 그 양보가 어떤 상황에선 큰 단점이 돼요.

NoSQL이 수평 확장을 쉽게 하려고 흔히 관계(조인)와 강한 트랜잭션을 양보했거든요. 그런데 우리가 오늘 배운 계좌 이체 같은 일 — 출금과 입금이 반드시 함께 성공해야 하고, 절대 한 푼도 틀리면 안 되는 — 에는 ACID를 강하게 보장하는 SQL이 훨씬 안전합니다. 또 "이 사용자의 주문, 그 주문의 상품, 그 상품의 카테고리"처럼 여러 표를 엮어 보는 복잡한 관계 질의도 관계형이 잘해요.

반대로 초당 수십만 건의 단순 읽기·쓰기, 형태가 제각각인 로그, 빠른 캐시 같은 일엔 NoSQL이 빛나죠. 그래서 정답은 "하나로 통일"이 아니라 "일에 맞는 도구" 예요. 실제 큰 서비스는 핵심 거래 데이터는 SQL로, 캐시·세션·로그·추천은 NoSQL로 나눠 씁니다. 면접에서도 "NoSQL이 더 좋다"가 아니라 "용도에 따라 트레이드오프로 고른다"가 정답입니다.


마무리

오늘은 "서버가 뒤지는 그 데이터베이스" 안으로 들어가, 데이터가 깨지지 않게 지키는 장치부터 빠르게 찾는 장치까지 면접 앵글로 훑었습니다. 세 가지만 또렷이 들고 가세요.

오늘 배운 핵심 세 가지

🎯 하나 — 트랜잭션과 ACID로 데이터를 지킨다. 트랜잭션은 쪼갤 수 없는 한 묶음의 일(계좌 이체)이고, 원자성·일관성·고립성·지속성으로 "다 되거나 다 안 되거나, 동시에 돌아도 안전하게, 커밋되면 영구히"를 보장합니다.

🎯 둘 — 격리 수준으로 동시 실행의 사고를 막는다. 고립을 풀면 더티 리드·Non-Repeatable·팬텀 세 이상현상이 생기고, READ UNCOMMITTED → COMMITTED → REPEATABLE READ → SERIALIZABLE 네 단계로 어디까지 막을지 고릅니다. 안전과 성능의 트레이드오프라 무조건 최고 단계로 가지 않아요. 구현은 (자물쇠·데드락 위험)과 MVCC(버전 스냅샷·읽기가 쓰기를 안 막음)로 합니다.

🎯 셋 — 인덱스로 빠르게 찾고, 도구는 트레이드오프로 고른다. 인덱스(B+트리)는 풀 스캔(O(n))을 O(log n) 으로 줄이되 쓰기를 느리게 하는 대가가 있어 선별적으로 겁니다. SQL이냐 NoSQL이냐는 정합성·관계 대 유연성·수평 확장의 트레이드오프이고, 분산이면 CAP으로 일관성(CP)과 가용성(AP) 중 하나를 택합니다.

다음 시간 예고

다음 시간(D-2)은 면접에 나오는 자료구조와 복잡도입니다. 오늘 인덱스에서 우리는 두 가지를 슬쩍 만났어요 — 트리(B+트리)와 O(log n) 이라는 복잡도요. 다음 시간엔 바로 그 빅오 표기로 알고리즘의 빠르기를 재는 법을 배우고, 배열 vs 연결 리스트, 해시 테이블(평균 O(1)인데 충돌이 나면?), 스택·큐·트리 같은 자료구조가 각각 어떤 상황에서 어떤 복잡도를 내는지를 면접 앵글로 정리합니다. "이 자료구조는 왜 이 연산이 빠른가"를 트레이드오프로 답하는 게 목표예요. 오늘의 인덱스가 그 첫 다리였습니다.


과제

[기초] ACID를 계좌 이체 하나로 엮어 설명하기

A가 B에게 1만 원을 보내는 계좌 이체를 예로, 원자성·일관성·고립성·지속성 네 성질을 각각 한 문장으로 설명해 보세요. 특히 "원자성이 깨지면 무슨 일이 벌어지는가", "지속성이 없으면 무슨 일이 벌어지는가"를 구체적인 사고 시나리오로 적어 보세요. 또 커밋과 롤백이 이 이체에서 각각 언제 일어나는지도 함께 써 보세요.

[응용] 세 이상현상을 타임라인으로 그리고 격리 수준과 잇기

두 트랜잭션 T1·T2가 동시에 도는 상황을 가정하고, Dirty Read·Non-Repeatable Read·Phantom Read 세 가지를 각각 시간 순서 타임라인(누가 언제 무엇을 읽고 쓰는지)으로 그려 보세요. 그런 다음, 각 이상현상이 어느 격리 수준부터 막히는지를 표로 정리하세요. 마지막으로 "우리 서비스가 READ COMMITTED를 쓰는데 Non-Repeatable Read가 문제가 됐다 — 어떻게 해결할까?"에 한두 문장으로 답해 보세요.

[심화] 느린 조회를 인덱스로 진단하고, 도구를 트레이드오프로 고르기

(1) "사용자 목록에서 username으로 특정 사용자를 찾는 조회가 데이터가 많아지자 느려졌다"는 상황입니다. 무엇이 원인이고(풀 스캔), 어떻게 해결할지(어느 열에 인덱스), 그리고 그 인덱스의 대가(트레이드오프) 는 무엇인지 적어 보세요. (2) 다음 두 서비스에 SQL과 NoSQL 중 무엇을 主로 쓸지 이유와 함께 고르세요 — ① 송금·결제를 다루는 핀테크 ② 초당 수십만 건의 사용자 행동 로그를 쌓는 분석 시스템. (SQL 문법은 쓰지 않고 개념으로만 답하세요.)


생각해볼 주제

1. 무조건 가장 안전한 격리 수준을 쓰면 왜 안 될까

SERIALIZABLE은 모든 이상현상을 막아주는 가장 안전한 격리 수준입니다. 그런데 거의 모든 실무 시스템은 이걸 기본으로 쓰지 않고, READ COMMITTED나 REPEATABLE READ를 기본으로 둡니다. "안전이 최고인데 왜 가장 안전한 걸 안 쓸까?" — 안전과 성능(동시성) 사이의 트레이드오프 관점에서, 그리고 "필요한 트랜잭션만 격리를 높인다"는 현실적 접근의 관점에서 생각해 보세요.

2. 인덱스를 거는 것이 항상 이득일까

인덱스는 조회를 극적으로 빠르게 해줍니다. 그렇다면 "조회가 많은 서비스니까 웬만한 열에는 다 인덱스를 걸자"는 판단은 옳을까요? 읽기와 쓰기의 비율, 인덱스 갱신 비용, 값의 다양성(카디널리티)을 함께 놓고 생각해 보세요. "인덱스는 공짜가 아니다"라는 말의 의미를 자기 언어로 설명할 수 있다면 충분합니다.

3. "최신 기술이 더 좋다"는 함정 — SQL과 NoSQL 사이에서

NoSQL은 SQL보다 나중에 나왔고 수평 확장에 강합니다. 그래서 "이왕이면 최신이고 잘 늘어나는 NoSQL을 쓰자"는 생각이 들 수 있어요. 하지만 기술 선택에서 "더 새것"이 "더 나은 선택"과 같은 말일까요? 도구는 우열이 아니라 트레이드오프라는 관점에서, "이 일에는 이 도구"를 판단하는 기준이 무엇이어야 할지 생각해 보세요. 이 사고법은 데이터베이스를 넘어 모든 기술 선택에 똑같이 적용됩니다.

✅ 예시 답안정답 보기

이 문서는 D-1 「면접에 나오는 데이터베이스」의 과제와 생각해볼 주제에 대한 예시답안입니다. 정답을 외우는 용도가 아니라, 원리를 어떻게 풀어 답하는지 흐름을 참고하는 용도로 보세요.


과제 예시답안

🎯 [과제 1 예시답안] ACID를 계좌 이체 하나로 엮어 설명하기

채점 포인트

항목 배점 기준
ACID 네 성질 정확성 40% 원자성·일관성·고립성·지속성을 이체 맥락으로 정확히 설명했는가
깨졌을 때 시나리오 35% 원자성·지속성이 깨지면 무슨 일이 벌어지는지 구체적으로 적었는가
커밋·롤백 시점 25% 이체에서 커밋·롤백이 각각 언제 일어나는지 짚었는가

풀이 예시

A가 B에게 1만 원을 보내는 이체 = (A 출금 1만) + (B 입금 1만), 한 트랜잭션.

  • 원자성(Atomicity) — 출금과 입금은 다 되거나 다 안 되거나여야 한다. 중간 상태("출금만 됨")는 허용되지 않는다.
  • 일관성(Consistency) — 트랜잭션 전후로 규칙이 깨지지 않는다. 이체 전후 두 계좌의 합은 보존되고, 잔액이 음수가 되는 일은 없다.
  • 고립성(Isolation) — 같은 계좌에 동시에 다른 이체가 돌아도, 각 트랜잭션은 혼자 도는 것처럼 보여야 한다. 두 이체가 같은 잔액을 동시에 읽어 둘 다 성공시키는 사고가 없어야 한다.
  • 지속성(Durability) — 이체가 커밋되면, 직후 전원이 나가도 그 결과는 디스크에 남아 사라지지 않는다.

원자성이 깨지면 — A에서 1만 원이 빠졌는데 B 입금 단계에서 오류가 나고 출금이 안 되돌려진다면, 1만 원이 허공으로 증발한다. 반대 순서면 돈이 복사된다.

지속성이 없으면 — "이체 완료"라고 응답을 받았는데 직후 서버 전원이 나가서 그 결과가 사라진다면, 사용자는 보냈다고 믿지만 실제론 안 보내진 상태가 된다. 장부와 현실이 어긋난다.

커밋·롤백 시점 — 출금·입금이 둘 다 성공하면 마지막에 COMMIT으로 확정한다. 둘 중 하나라도 실패하면 ROLLBACK으로 출금까지 깨끗이 되돌려, 아무 일도 없던 상태로 만든다.

💡 튜터의 한마디 — ACID를 물으면 네 글자를 나열만 하지 말고, 계좌 이체 하나로 네 성질을 다 엮어 설명하세요. "출금·입금이 함께 되고(A), 합이 보존되고(C), 동시 이체와 안 엉키고(I), 커밋되면 영구히 남는다(D)" — 이 한 문장이 면접에서 가장 깔끔합니다.

🎯 [과제 2 예시답안] 세 이상현상을 타임라인으로 그리고 격리 수준과 잇기

채점 포인트

항목 배점 기준
세 이상현상 타임라인 45% dirty/non-repeatable/phantom을 시간 순서로 정확히 그렸는가
격리 수준 매핑 표 35% 각 이상현상이 어느 수준부터 막히는지 표로 맞췄는가
해결 한 줄 20% READ COMMITTED에서 Non-Repeatable 해결책을 짚었는가

풀이 예시

1. 세 이상현상 타임라인

Dirty Read — 커밋 안 된 값을 읽음

시간 T1 T2
잔액 1000 → 900 수정 (커밋 전)
잔액 조회 → 900 읽음
ROLLBACK (잔액 1000 복귀)
존재한 적 없는 900 을 본 셈

Non-Repeatable Read — 같은 행, 값이 바뀜

시간 T1 T2
7번 행 읽음 → 1000
7번 행 900 으로 수정 후 COMMIT
7번 행 다시 읽음 → 900
같은 행인데 값이 달라짐

Phantom Read — 같은 조건, 행 개수가 바뀜

시간 T1 T2
팔로워 1000+ 조회 → 5건
팔로워 1200 사용자 INSERT 후 COMMIT
팔로워 1000+ 다시 조회 → 6건
없던 행이 유령처럼 나타남

2. 격리 수준 매핑

격리 수준 Dirty Non-Repeatable Phantom
READ UNCOMMITTED 허용 허용 허용
READ COMMITTED 방지 허용 허용
REPEATABLE READ 방지 방지 허용
SERIALIZABLE 방지 방지 방지

3. READ COMMITTED에서 Non-Repeatable이 문제라면

격리 수준을 REPEATABLE READ로 한 단계 올리면 된다. 그러면 한 트랜잭션 안에서 같은 행은 항상 같은 값으로 보여 Non-Repeatable Read가 막힌다. 단, 전체를 다 올리기보다 그 정합성이 꼭 필요한 트랜잭션만 올리는 게 성능 면에서 낫다.

💡 튜터의 한마디 — 표를 외울 때 "위에서 아래로 더티 → Non-Repeatable → 팬텀이 차례로 막힌다"는 순서만 잡으면 됩니다. 그리고 둘의 구분은 늘 한 단어 — 값이 바뀌면 Non-Repeatable, 행 수가 바뀌면 Phantom.

🎯 [과제 3 예시답안] 느린 조회를 인덱스로 진단하고, 도구를 트레이드오프로 고르기

채점 포인트

항목 배점 기준
풀 스캔 진단 + 인덱스 처방 35% 원인(풀 스캔)과 해결(조회 열 인덱스)을 짚었는가
인덱스 트레이드오프 30% 쓰기 저하·공간 등 대가를 설명했는가
SQL/NoSQL 선택 + 이유 35% 두 서비스에 맞는 도구를 트레이드오프로 골랐는가

풀이 예시

1. 느린 조회 진단과 처방

  • 원인username에 인덱스가 없으면, DBMS는 사용자 표를 첫 행부터 끝까지 다 비교(풀 스캔, O(n))한다. 데이터가 적을 땐 빠르지만, 행이 수백만으로 늘면 그대로 느려진다.
  • 해결 — 자주 조회 조건이 되는 username 열에 인덱스를 건다. 그러면 정렬된 B+트리로 위치를 바로 짚어 O(log n)으로 찾는다.
  • 트레이드오프 — 인덱스는 공짜가 아니다. ① 사용자를 추가·수정할 때마다 인덱스도 갱신해야 해서 쓰기가 느려진다. ② 인덱스 자체가 디스크 공간을 더 쓴다. 그래서 "자주 조회되고 값이 다양한 열"에 선별적으로 건다. username은 사람마다 달라(카디널리티 높음) 인덱스 효과가 좋은 전형적인 경우다.

2. 서비스별 도구 선택

  • ① 송금·결제 핀테크 → SQL(RDBMS). 돈은 한 푼도 틀리면 안 되므로 ACID 트랜잭션의 강한 정합성이 필수다. 출금·입금이 원자적으로 묶이고, 동시 거래가 고립돼야 한다. 관계(계좌·거래·사용자)도 복잡해 관계형이 어울린다.
  • ② 초당 수십만 건 행동 로그 → NoSQL. 형태가 제각각이고 양이 폭발적이며, 강한 트랜잭션·복잡한 조인보다 빠른 쓰기와 수평 확장이 중요하다. 로그 한두 건이 잠깐 어긋나도 치명적이지 않으니 정합성을 양보하고 확장성을 택하는 게 맞다.

💡 튜터의 한마디 — "이 API가 왜 느려요?"를 받으면 잘하는 사람은 "조회 조건 열에 인덱스가 있나, 혹시 풀 스캔이 도나" 를 먼저 의심합니다. 그리고 도구 선택은 늘 우열이 아니라 "이 일에는 이 트레이드오프" 로 답하세요 — 그게 면접관이 듣고 싶어 하는 사고법입니다.


생각해볼 주제 예시답안

🤔 [생각해볼 주제 1] 무조건 가장 안전한 격리 수준을 쓰면 왜 안 될까

[문제 상황 요약]

SERIALIZABLE은 세 이상현상을 모두 막는 가장 안전한 격리 수준이다. 그런데 실무 시스템은 대부분 이걸 기본으로 쓰지 않고 READ COMMITTED나 REPEATABLE READ를 기본으로 둔다. "안전이 최고인데 왜 가장 안전한 걸 기본으로 안 쓸까?"가 이 주제의 질문이다.

[튜터의 가이드 및 해설]

핵심은 안전과 성능(동시성)은 한쪽을 올리면 다른 쪽이 내려가는 시소라는 점이다. SERIALIZABLE은 트랜잭션들을 사실상 한 줄로 세워 차례차례 실행한 것처럼 보장한다. 그러려면 트랜잭션끼리 서로 더 오래·더 넓게 기다리거나(락), 충돌 시 재시도해야 한다. 동시에 수만 명이 들어오는 서비스에서 모든 트랜잭션을 이렇게 줄 세우면, 정합성은 완벽해도 처리량이 떨어지고 응답이 느려진다. 대기·충돌·재시도가 늘면서 오히려 전체 사용자 경험이 나빠진다.

그래서 현실의 전략은 "필요한 만큼만 격리한다" 이다. 대부분의 트랜잭션은 READ COMMITTED나 REPEATABLE READ로 빠르게 처리하고, 틀리면 절대 안 되는 일부 트랜잭션만 격리를 높이거나 명시적 잠금을 건다. 예를 들어 게시물 조회는 낮은 격리로 빠르게, 재고 차감이나 정산처럼 민감한 계산만 강하게 보호하는 식이다.

여기서 한 단계 더 들어가면, "기본값은 왜 DBMS마다 다른가" 도 같은 논리다. Oracle·PostgreSQL은 READ COMMITTED를, MySQL InnoDB는 REPEATABLE READ를 기본으로 둔다. 정답이 하나가 아니라, 각 DBMS가 "대다수 워크로드에서 안전과 성능의 균형점"으로 판단한 지점이 다른 것이다. 결국 격리 수준은 고정된 정답이 아니라, 그 트랜잭션이 요구하는 정합성과 감당할 성능을 저울질해 고르는 설계 결정이다.

🎯 면접관을 홀리는 핵심 멘트

"격리 수준은 안전과 동시성의 트레이드오프라, 무조건 SERIALIZABLE이 정답은 아닙니다. 최고 단계는 트랜잭션을 한 줄로 세우다시피 해서 정합성은 완벽하지만 처리량이 급감하거든요. 그래서 실무는 대부분을 READ COMMITTED/REPEATABLE READ로 빠르게 돌리고, 정말 틀리면 안 되는 트랜잭션만 격리를 높이는 '필요한 만큼만 격리한다' 전략을 씁니다. 정합성을 어디까지 포기하지 않을지를 트랜잭션 단위로 판단하는 게 핵심이에요."

🤔 [생각해볼 주제 2] 인덱스를 거는 것이 항상 이득일까

[문제 상황 요약]

인덱스는 조회를 O(n)에서 O(log n)으로 극적으로 빠르게 해준다. 그렇다면 "조회가 많은 서비스니 웬만한 열에는 다 인덱스를 걸자"는 판단은 옳을까? "인덱스는 공짜가 아니다"라는 말의 의미를 트레이드오프로 풀어내는 게 이 주제다.

[튜터의 가이드 및 해설]

인덱스의 본질은 "정렬된 사본을 따로 유지한다" 는 것이다. 이 사본 덕에 읽기는 빨라지지만, 그 사본을 항상 최신으로 유지하는 비용이 따라온다. 데이터를 하나 넣거나 고치거나 지울 때마다, 관련된 모든 인덱스(정렬된 트리)도 같이 갱신해야 한다. 열 10개에 인덱스를 다 걸면, 행 하나를 INSERT할 때 인덱스 10개를 동시에 손봐야 해서 쓰기가 그만큼 느려진다. 게다가 인덱스마다 디스크 공간을 차지하니 저장 비용도 늘어난다.

그래서 인덱스의 이득은 읽기와 쓰기의 비율에 크게 좌우된다. 읽기가 압도적으로 많은 서비스라면 인덱스의 이득이 쓰기 비용을 압도하지만, 쓰기가 빈번한 테이블(예: 로그·이벤트 기록)에 인덱스를 잔뜩 걸면 쓰기 병목이 생긴다. 또 값의 다양성(카디널리티) 도 중요하다. 성별이나 참/거짓처럼 값 종류가 적은 열은 인덱스를 걸어도 후보를 충분히 못 줄여 효과가 약하다 — 절반을 걸러봐야 여전히 수많은 행을 읽어야 하니까.

결론적으로 "웬만한 열에 다 걸자"는 판단은 대개 손해다. 안 쓰는 인덱스는 순수 비용(쓰기만 느리게, 공간만 차지)이기 때문이다. 올바른 접근은 실제로 느린 조회의 조건 열을 찾아 거기에만 선별적으로 거는 것이다. "인덱스는 공짜가 아니다"라는 말은 곧 "읽기를 빠르게 하는 대신 쓰기와 공간을 희생한다" 는 트레이드오프의 다른 표현이다.

🎯 면접관을 홀리는 핵심 멘트

"인덱스는 읽기를 빠르게 하는 대신 쓰기를 느리게 하는 트레이드오프라, 무조건 많이 거는 게 답이 아닙니다. 인덱스는 '정렬된 사본'이라 데이터를 넣고 고칠 때마다 같이 갱신돼야 하고, 공간도 차지하거든요. 그래서 읽기/쓰기 비율과 값의 다양성(카디널리티) 을 보고, 실제로 느린 조회의 조건 열에만 선별적으로 겁니다. 안 쓰는 인덱스는 쓰기만 느리게 하는 순수 비용이에요."

🤔 [생각해볼 주제 3] "최신 기술이 더 좋다"는 함정 — SQL과 NoSQL 사이에서

[문제 상황 요약]

NoSQL은 SQL보다 나중에 나왔고 수평 확장에 강하다. "이왕이면 최신이고 잘 늘어나는 NoSQL을 쓰자"는 생각이 들 수 있다. 하지만 "더 새것"이 정말 "더 나은 선택"과 같은 말일까? 기술 선택의 기준을 트레이드오프 관점에서 세우는 게 이 주제다.

[튜터의 가이드 및 해설]

먼저 사실 관계부터 바로잡아야 한다. NoSQL은 SQL의 상위 호환 다음 세대가 아니다. 수평 확장을 쉽게 하려고 관계(조인)와 강한 트랜잭션을 의도적으로 양보한 다른 설계일 뿐이다. 그 양보가 어떤 일에는 결정적 단점이 된다. 우리가 배운 계좌 이체 — 출금·입금이 반드시 함께 성공해야 하고 한 푼도 틀리면 안 되는 — 같은 일에는, ACID를 강하게 보장하는 SQL이 훨씬 안전하다. "사용자 → 주문 → 상품 → 카테고리"처럼 여러 표를 엮는 복잡한 관계 질의도 관계형이 잘한다.

반대로 형태가 제각각인 로그, 초당 수십만 건의 단순 읽기·쓰기, 빠른 캐시 같은 일엔 NoSQL이 빛난다. 즉 두 도구는 푸는 문제가 다르다. "더 좋다/나쁘다"가 아니라 "이 일엔 이게, 저 일엔 저게 맞다"가 정확한 판단이다. 그래서 큰 서비스는 한쪽으로 통일하지 않고, 핵심 거래는 SQL로, 캐시·세션·로그·추천은 NoSQL로 용도별로 섞어 쓴다(폴리글랏 저장소).

이 사고법은 데이터베이스를 넘어 모든 기술 선택에 똑같이 적용된다. "최신이라서", "남들이 쓰니까", "멋있어 보여서" 고르는 건 이력서 주도 개발의 함정이다. 기술은 유행이 아니라 풀려는 문제의 요구사항(정합성·확장성·일관성·팀의 숙련도·운영 비용)에 맞춰 고르는 것이다. "왜 이걸 골랐나"에 트레이드오프로 답할 수 있으면 옳은 선택이고, "최신이라서"밖에 못 대면 다시 생각해봐야 한다.

🎯 면접관을 홀리는 핵심 멘트

"기술 선택에서 '더 새것'이 '더 나은 선택'과 같은 말은 아닙니다. NoSQL은 SQL의 상위 호환이 아니라, 수평 확장을 위해 관계와 강한 트랜잭션을 양보한 다른 트레이드오프예요. 그래서 송금·결제처럼 정합성이 생명인 일엔 SQL을, 대용량 로그·캐시엔 NoSQL을 쓰고, 큰 서비스는 용도별로 섞어 씁니다. 도구는 우열이 아니라 풀려는 문제의 요구사항으로 고르는 것이고, '왜 골랐나'에 트레이드오프로 답할 수 있어야 한다고 생각합니다."

전체 목록 CS 기초지식

더 배우려면

실무 프로젝트까지 가고 싶다면

팀스파르타 백엔드 부트캠프에서 인스타그램 클론을 풀스택으로 완성합니다.