1. 정규화 (Normalization) & 반정규화 (Denormalization)
데이터베이스를 설계할 때 가장 기본이 되는 개념입니다. 데이터를 어떻게 구성하고 저장할지 결정하는 과정입니다.
정규화 (Normalization)
정의: 데이터의 중복을 최소화하고 데이터 무결성을 보장하기 위해, 관계형 데이터베이스의 테이블을 구조화하고 분해하는 과정입니다. 쉽게 말해, 중복되는 데이터를 여러 테이블로 쪼개서 관리하는 것입니다.
목적: '데이터 이상 현상(Anomaly)'을 방지하기 위해서입니다.
- 삽입 이상 (Insertion Anomaly): 불필요한 데이터가 없으면 특정 데이터를 삽입할 수 없는 경우. (예: '수강과목' 테이블에 학생이 수강신청을 해야만 그 학생의 정보를 등록할 수 있는 경우)
- 수정 이상 (Update Anomaly): 중복된 데이터 중 일부만 수정되어 데이터 일관성이 깨지는 경우. (예: A학생의 주소가 10개 행에 중복 저장되어 있는데, 그중 5개만 수정하고 5개는 누락하는 경우)
- 삭제 이상 (Deletion Anomaly): 특정 정보를 삭제할 때, 원치 않는 다른 정보까지 함께 삭제되는 경우. (예: 학생이 수강신청을 모두 취소했더니, 그 학생의 학적 정보까지 DB에서 사라지는 경우)
주요 정규형 (단계):
- 제1정규형 (1NF): 테이블의 모든 컬럼이 '원자값(Atomic Value)'을 갖도록 합니다. 즉, 하나의 컬럼에 여러 개의 값이 들어가지 않도록 분리합니다. (예: '취미' 컬럼에 '등산, 독서'가 같이 있으면 안 되고, '등산'과 '독서'로 행을 나누거나 테이블을 분리해야 함)
- 제2정규형 (2NF): 제1정규형을 만족하고, '부분 함수 종속'을 제거합니다. 기본 키(Primary Key)가 여러 컬럼으로 구성된 '복합 키'일 때, 키의 일부에만 종속되는 컬럼을 별도 테이블로 분리합니다.
- 제3정규형 (3NF): 제2정규형을 만족하고, '이행 함수 종속'을 제거합니다. 기본 키가 아닌 일반 컬럼이 다른 일반 컬럼에 종속되는 관계(A -> B -> C)를 제거합니다. (예: 학생ID -> 학과코드 -> 학과명. 이 경우 '학과' 테이블을 분리)
장점: 데이터 중복 최소화, 데이터 무결성 보장, 데이터 구조의 유연성 증가.
단점: 테이블이 과도하게 분리되면, 데이터 조회(SELECT) 시 많은 조인(JOIN)이 필요해져 성능이 저하될 수 있습니다.
반정규화 (Denormalization)
- 정의: 정규화와 반대로, 성능 향상을 위해 의도적으로 데이터의 중복을 허용하거나 테이블을 병합하는 과정입니다.
- 목적: 데이터베이스의 읽기(조회) 성능을 향상시키기 위해서입니다. 정규화로 인해 발생하는 과도한 조인(JOIN)을 줄이는 것이 핵심입니다.
- 시기: 주로 시스템 개발 후, 데이터 조회가 많은 특정 부분에서 성능 저하가 발생할 때 고려합니다.
- 방법: 테이블 통합, 중복 컬럼 추가, 계산된 컬럼 추가 등.
- 장점: 복잡한 조인 감소로 인한 쿼리 응답 속도 향상.
단점: 데이터 중복으로 인한 저장 공간 증가, 데이터 수정/삽입/삭제 시 일관성을 유지하기 어려워짐 (즉, 정규화의 장점을 포기하는 것).
결론: 정규화는 데이터 무결성을, 반정규화는 조회 성능을 우선시합니다. 현대의 시스템 설계에서는 우선 정규화를 원칙으로 설계한 뒤, 시스템 운영 중 성능 문제가 발생하는 지점에 한해 선별적으로 반정규화를 적용하는 것이 일반적입니다.
2. 트랜잭션 (Transaction)과 ACID
트랜잭션 (Transaction)
정의: 데이터베이스의 상태를 변화시키기 위해 수행하는 '작업의 논리적 단위'입니다. 여러 개의 쿼리(SQL)가 하나의 작업처럼 묶여서 실행되는 것을 의미합니다.
핵심: 'All or Nothing' (전부 성공하거나 전부 실패해야 함).
- Commit (커밋): 트랜잭션의 모든 작업이 성공적으로 완료되었음을 선언하고, 변경 내용을 데이터베이스에 영구적으로 저장합니다.
- Rollback (롤백): 트랜잭션 작업 중 하나라도 실패하면, 이전에 수행했던 모든 작업을 취소하고 트랜잭션 시작 전의 상태로 되돌립니다.
예시: 은행 계좌 이체 ('A가 B에게 10만 원 송금')
- A의 잔고에서 10만 원 차감 (UPDATE)
- B의 잔고에 10만 원 추가 (UPDATE)
이 두 작업은 '하나의 트랜잭션'으로 묶여야 합니다. 1번만 성공하고 2번이 실패하면(예: 시스템 오류), 10만 원이 공중에 뜨게 됩니다. 따라서 2번이 실패하면 1번 작업도 반드시 취소(Rollback)되어야 합니다.
ACID (트랜잭션의 4가지 특성)
데이터베이스 트랜잭션이 안전하게 수행되기 위해 반드시 보장해야 할 4가지 성질입니다.
- Atomicity (원자성): 트랜잭션의 모든 작업은 '전부 실행되거나 전부 실행되지 않아야' 합니다. (All or Nothing). 롤백(Rollback) 기능으로 보장됩니다.
- Consistency (일관성): 트랜잭션이 성공적으로 완료된 후에도 데이터베이스는 항상 '일관된 상태'를 유지해야 합니다. 데이터베이스의 제약 조건(예: Primary Key, Foreign Key, Not Null 등)이나 비즈니스 규칙을 위반하지 않아야 합니다.
- Isolation (고립성/격리성): 트랜잭션이 동시에 실행될 때, '각 트랜잭션은 다른 트랜잭션의 작업에 영향을 받지 않아야' 합니다. 즉, 하나의 트랜잭션이 실행되는 동안에는 마치 데이터베이스에 혼자 접근하는 것처럼 보여야 합니다. (이 개념이 아래 '격리 수준'으로 이어집니다.)
- Durability (지속성): 일단 '커밋(Commit)'된 트랜잭션의 결과는 시스템에 장애(예: 정전, 서버 다운)가 발생하더라도 영구적으로 저장되어야 합니다. 데이터베이스는 보통 로그(Log)를 디스크에 기록하는 방식으로 이를 보장합니다.
3. 트랜잭션 격리 수준 (Isolation Levels) & 동시성 제어
트랜잭션의 ACID 특성 중 '고립성(Isolation)'은 현실에서 성능과 타협해야 하는 경우가 많습니다.
왜 격리 수준이 필요한가?
완벽한 고립성(마치 모든 트랜잭션이 순서대로 실행되는 것처럼)을 보장하면 '동시성(Concurrency)'이 떨어집니다. 즉, 한 번에 하나의 트랜잭션만 처리하게 되어 시스템 전체의 성능이 급격히 저하됩니다.
반대로 동시성을 너무 높이면(여러 트랜잭션이 마구잡이로 동시에 데이터에 접근하게 두면) 고립성이 깨져 다음과 같은 심각한 데이터 부정합 문제가 발생합니다.
동시성 문제 (Concurrency Problems):
- Dirty Read (더티 리드): 한 트랜잭션(T1)이 데이터를 수정하고 아직 '커밋(Commit)'하지 않았는데, 다른 트랜잭션(T2)이 그 데이터를 읽어가는 현상. 만약 T1이 '롤백(Rollback)'하면, T2는 존재하지 않는 '더러운' 데이터를 가지고 작업을 수행한 셈이 됩니다. (가장 심각한 문제)
- Non-Repeatable Read (반복 불가능한 읽기): 한 트랜잭션(T1)이 동일한 데이터를 두 번 읽었는데, 그 사이에 다른 트랜잭션(T2)이 해당 데이터를 '수정(UPDATE)'하고 커밋하는 바람에, T1이 두 번째 읽었을 때의 데이터가 첫 번째와 달라지는 현상.
- Phantom Read (유령 읽기): 한 트랜잭션(T1)이 일정 범위의 데이터를 두 번 읽었는데, 그 사이에 다른 트랜잭션(T2)이 그 범위 내에 새로운 데이터를 '삽입(INSERT)'하거나 '삭제(DELETE)'하는 바람에, T1이 두 번째 읽었을 때 데이터의 개수가 달라지는 현상. (Non-Repeatable Read와 비슷하지만, 이는 범위(Range) 조회 시 발생합니다.)
트랜잭션 격리 수준 (ANSI 표준 4단계)
위의 동시성 문제들을 얼마나 허용할 것인지에 대한 '레벨'을 정한 것입니다. 레벨이 높아질수록 고립성은 강해지지만(데이터 정합성 증가), 동시성(성능)은 저하됩니다.
- LEVEL 0: READ UNCOMMITTED (커밋되지 않은 읽기)
- 모든 문제(Dirty Read, Non-Repeatable Read, Phantom Read)가 발생합니다.
- 커밋되지 않은 데이터도 읽을 수 있습니다. 정합성에 심각한 문제가 생길 수 있어 거의 사용하지 않습니다.
- LEVEL 1: READ COMMITTED (커밋된 읽기)
- 'Dirty Read'를 방지합니다. (커밋된 데이터만 읽음)
- 하지만 Non-Repeatable Read와 Phantom Read는 여전히 발생합니다.
- 많은 상용 데이터베이스(예: Oracle, PostgreSQL)의 기본 격리 수준입니다.
- LEVEL 2: REPEATABLE READ (반복 가능한 읽기)
- 'Dirty Read'와 'Non-Repeatable Read'를 방지합니다.
- 한 트랜잭션이 시작될 때의 데이터 버전을 기준으로 읽기 때문에, 다른 트랜잭션이 중간에 데이터를 수정해도 같은 데이터를 반복해서 읽을 수 있습니다.
- 하지만 Phantom Read(데이터 개수가 달라지는)는 여전히 발생할 수 있습니다.
- MySQL(InnoDB)의 기본 격리 수준입니다.
- LEVEL 3: SERIALIZABLE (직렬화 가능)
- 모든 문제(Dirty Read, Non-Repeatable Read, Phantom Read)를 방지합니다.
- 트랜잭션을 마치 순서대로 하나씩 실행하는 것처럼 동작시킵니다.
- 가장 강력한 정합성을 보장하지만, 동시성이 크게 저하되어 성능이 가장 느립니다.
동시성 제어 (Concurrency Control)
데이터베이스가 이런 격리 수준을 보장하기 위해 사용하는 기술입니다.
- 잠금 (Locking): 가장 보편적인 방식. 트랜잭션이 데이터에 접근할 때 '잠금'을 걸어 다른 트랜잭션의 접근을 제어합니다.
- 공유 잠금 (Shared Lock, S-Lock): 데이터를 '읽을(Read)' 때 사용. 여러 트랜잭션이 동시에 S-Lock을 가질 수 있습니다. (읽기-읽기 가능)
- 배타적 잠금 (Exclusive Lock, X-Lock): 데이터를 '쓸(Write)' 때 사용. 오직 하나의 트랜잭션만 X-Lock을 가질 수 있으며, 이 동안 다른 트랜잭션은 S-Lock, X-Lock 모두 가질 수 없습니다. (읽기-쓰기, 쓰기-쓰기 모두 불가)
- MVCC (Multi-Version Concurrency Control, 다중 버전 동시성 제어):
- 잠금을 사용하지 않고(정확히는 잠금을 최소화하고), 데이터의 여러 '버전(스냅샷)'을 관리하는 방식입니다.
- 트랜잭션이 시작될 때의 데이터 스냅샷을 만들어, 다른 트랜잭션이 데이터를 수정하더라도 자신만의 버전을 읽게 합니다.
- 이 방식 덕분에 '읽기 작업(SELECT)'이 '쓰기 작업(UPDATE)'을 방해하지 않고, '쓰기 작업'도 '읽기 작업'을 방해하지 않아 동시성이 크게 향상됩니다.
- Oracle, PostgreSQL, MySQL(InnoDB) 등이 MVCC를 사용하여 READ COMMITTED나 REPEATABLE READ 수준을 효율적으로 구현합니다.
흐름대로 읽기 좋았습니다!