카테고리 없음

데이터 무결성, 트랜잭션

도멩 2025. 5. 30. 10:44

데이터 무결성

데이터의 무결성이란 데이터의 정확성, 일관성, 유효성이 유지되는 것을 말합니다. 데이터의 무결성을 유지하는 것은 데이터베이스 관리시스템 (DBMS)의 중요한 기능이며, 주로 데이터에 적용되는 연산에 제한을 두어 데이터의 무결성을 유지합니다. 데이터베이스에서 말하는 무결성에는 다음과 같은 3가지 종류가 있습니다.

 

1. 개체 무결성

모든 테이블이 기본 키 (primary key)로 선택된 필드 (column)를 가져야 합니다. 기본 키로 선택된 필드는 고유한 값을 가져야 하며, 빈 값은 허용하지 않습니다.

 

2. 참조 무결성

관계형 데이터베이스 모델에서 참조 무결성은 참조 관계에 있는 두 테이블의 데이터가 항상 일관된 값을 갖도록 유지되는 것을 말합니다. 아래의 그림은 관계형 데이터베이스 모델에서 참조 무결성이 깨지는 경우를 나타냅니다. 이 예시에서는 department 테이블을 참조하는 student 테이블을 보여주고 있습니다. 이러한 참조 관계에서 만약 department 테이블에서 id 값이 310인 레코드가 삭제되면 student 테이블의 3번째 레코드는 더 이상 존재하지 않는 데이터를 참조하게 됩니다.

 

3. 도메인 무결성

도메인 무결성은 테이블에 존재하는 필드의 무결성을 보장하기 위한 것으로 필드의 타입, NULL값의 허용 등에 대한 사항을 정의하고, 올바른 데이터의 입력 되었는지를 확인하는 것입니다. 예를 들어, 주민등록번호 필드에 알파벳이 입력되는 경우는 도메인 무결성이 깨지는 경우라고 볼 수 있습니다. DBMS의 기본값 설정, NOT NULL 옵션 등의 제약 사항으로 도메인 무결성을 보장할 수 있습니다.


데이터 무결성 제약 조건

데이터 무결성 제약조건이란 테이블에 부적절한 데이터가 입력되는 것을 방지하기 위해 테이블을 설계할 때, 각 컬럼에 대해서 정의한 여러 가지 규칙을 말합니다.

 

1. 개체 무결성 제약 조건

기본 키를 구성하는 어떤 속성도 NULL 값을 가질수 없으며, 오직 하나의 값만 존재해야 합니다.

기본 키는 테이블에 저장된 행 데이터를 임의로 식별하기 위한 것이며, 하나의 테이블에 하나의 기본 키 제약을 정의할 수 있습니다.

 

추가적으로 기본 키 설정 시, 자동으로 기본 키에 해당하는 컬럼으로 인덱스가 생성됩니다.

 

👉  기본 키를 구성하는 속성에는 Null 입력 X + 중복 허용 X → PK = UNIQUE + NOT NULL

 

2. 참조 무결성 제약 조건

자식 릴레이션의 외래 키의 값은 참조된 부모 릴레이션의 기본 키 값과 같아야 하며, 자식 릴레이션의 값이 변경될 때 부모 릴레이션의 제약을 받는다는 조건입니다.

RDBMS에서는 컬럼의 값에 따라 테이블끼리 연결하는데 값을 참조하는 쪽의 테이블을 자식 테이블, 값을 가진 컬럼을 외래 키 라고 합니다.

 

참조되는 쪽의 테이블을 부모 테이블, 값을 가진 컬럼을 부모 키 라고 부르며 부모 키는 기본 키 또는 고유 키여야 합니다.

(외래 키는 NULL 값이거나 또는 부모 테이블의 기본 키 or 고유 키 값과 동일해야 한다)

 

3. 도메인 무결성 제약 조건

  • 널 값의 허용 여부 - NOT NULL

👉  NULL을 허용하지 않는 조건

 

  • 고유한 값 - UNIQUE

👉  해당 컬럼의 값이 테이블 안에서 중복되지 않은 항상 유일한 값이 되도록 하되 NULL을 허용한다.

 

👉  NULL 값은 여러 개가 입력되어도 상관없으며 기본 키는 테이블당 1개만 설정할 수 있는 반면에 고유 키는 여러개 설정할 수 있다.

 

  • 체크 - CHECK

👉  CHECK 제약 조건은 특정 컬럼의 입력 가능한 값의 범위를 지정할 때 사용한다.

 

👉  만약 특정 컬럼에 CHECK 제약 조건을 설정한 다음, 레코드를 입력할 때 해당 필드의 값이 CHECK 제약 조건에 해당하지 않으면 에러가 발생!

 

  • 속성의 기본 값 - DEFAULT

👉  DEFAULT 제약 조건은 해당 필드의 기본 값을 설정할 수 있게 해주며, 레코드를 입력할 때 해당 필드 값을 전달하지 않으면 자동으로 설정된 기본값을 저장한다.


정규화

정규화(Normailzation) : 중복데이터와 이상현상을 제거하기 위해 테이블을 함수종속성 기반으로 분해하는 과정

 

이상현상? = 데이터 중복성에 의해서 발생하는 데이터 불일치현상

 

갱신이상(Modification Anomaly) : 반복된 데이터 중 일부를 갱신할 경우 데이터 불일치가 발생하는 것

삽입이상(Insertion Anomaly) : 불필요한 정보를 같이 저장하지 않고서는 특정 정보를 저장하는 것이 불가능한 상태

삭제이상(Deletion Anomaly) : 필요한 정보를 함께 삭제하지 않고는 특정 정보를 삭제하는 것이 불가능한 상태

 

종 류 관련 내용 개 념
제 1정규화 원자성 도메인이 원자값이 될 수 있도록 분해
제 2정규화 부분함수 종속 부분함수 종속을 제거하여 완전함수 종속을 달성
제 3정규화 이행함수 종속 이행함수 종속을 제거
보이시코드 정규화 결정자, 후보키 결정자이면서 후보키가 아닌 것을 제거
제 4정규화 다치 종속 다치 종속을 제거
제 5정규화 조인 종속 조인 종속을 제거

 

해당 함수종속성을 제거하는 과정을 정규화(Normalization)라 하고, 완료된 상태를 정규형(Normal Form)이라고 합니다. 

ㆍ 일반적으로 3정규형 또는 보이시코드 정규화까지만 수행하고, 4,5정규화는 상황을 고려하여 추가적으로 실시합니다.


트랜잭션 관리

특징 (ACID)

  • 무결성 보장을 위한 트랜잭션이 가져야 할 특성
  • Atomicity(원자성) 
    • 트랜잭션 관련 일의 수행 여부를 보장합니다.
    • 트랜잭션 수행을 정상적으로 완료시 DB에 모두 반영되도록 커밋합니다.
    • 트랜잭션에 하나라도 오류가 발생해 문제가 생길 경우 롤백을 통해 세이브포인트까지 전부 취소합니다.
  • Consistency(일관성)
    • 시스템의 고정 요소는 트랜잭션 수행 전/후 아야 합니다.
  • Isolation(독립성, 격리성, 순차성)
    • 복수의 트랜잭션 병행 실행시 각각 격리되어 하나만 실행되고 다른건 대기합니다(참조 불가 상태).
    • 순차적으로 실행시 성능이 좋지 않으므로 격리 수준을 나누어 격리성을 보장합니다.
  • Durability(영속성, 지속성)
    • 트랜잭션 완료 후에는 시스템 고장시에도 영구적 반영(및 회복)이 되어야 합니다.

트랜잭션 격리 수준

트랜잭션의 격리 수준(Isolation Level)이란 여러 트랜잭션이 동시에 처리될 때, 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 여부를 결정하는 것입니다. 트랜잭션의 격리 수준은 격리(고립) 수준이 높은 순서대로 SERIALIZABLE, REPEATABLE READ, READ COMMITTED, READ UNCOMMITED가 존재합니다. 참고로 아래의 예제들은 모두 자동 커밋(AUTO COMMIT)이 false인 상태에서만 발생합니다.

  • SERIALIZABLE
  • REPEATABLE READ
  • READ COMMITTED
  • READ UNCOMMITED

1. SERIALIZABLE

 

SERIALIZABLE은 가장 엄격한 격리 수준으로, 이름 그대로 트랜잭션을 순차적으로 진행시킵니다. SERIALIZABLE에서 여러 트랜잭션이 동일한 레코드에 동시 접근할 수 없으므로, 어떠한 데이터 부정합 문제도 발생하지 않습니다. 하지만 트랜잭션이 순차적으로 처리되어야 하므로 동시 처리 성능이 매우 떨어집니다.

MySQL에서 SELECT FOR SHARE/UPDATE는 대상 레코드에 각각 읽기/쓰기 잠금을 거는 것입니다. 하지만 순수한 SELECT 작업은 아무런 레코드 잠금 없이 실행되는데, 잠금 없는 일관된 읽기란 순수한 SELECT 문을 통한 잠금 없는 읽기를 의미하는 것입니다.

하지만 SERIALIZABLE 격리 수준에서는 순수한 SELECT 작업에서도 대상 레코드에 넥스트 키 락을 읽기 잠(공유락, Shared Lock)으로 겁니다. 따라서 한 트랜잭션에서 넥스트 키 락이 걸린 레코드를 다른 트랜잭션에서는 절대 추가/수정/삭제할 수 없습니다. SERIALIZABLE은 가장 안전하지만 가장 성능이 떨어지므로, 극단적으로 안전한 작업이 필요한 경우가 아니라면 사용해서는 안됩니다.

 

2. REPEATABLE READ

 

 Non Repeatable Read 문제를 해결하는 격리 수준으로, 커밋된 데이터만 읽을 수 있되 자신보다 낮은 트랜잭션 번호를 갖는 트랜잭션에서 커밋한 데이터만 읽을 수 있는 격리수준입니다. Undo 로그로 인해 가능하게 됩니다. 또한 트랜잭션 ID를 통해 Undo 영역의 데이터를 스냅샷처럼 관리하여 동일한 데이터를 보장하는 것을 MVCC(Multi Version Concurrency Control) 라고 합니다.

 

 아래 그림에서 10번 트랜잭션은 10번 보다 작은 트랜잭션에서 커밋한 데이터만 읽을 수 있으므로 13번 트랜잭션에서 변경한 내용은 조회할 수 없습니다. 같은 SELECT 쿼리가 두 번 실행됐을 때 같은 결과가 조회되므로 Non-Repeatable-Read 현상이 해결됨을 확인할 수 있습니다.

 

3. READ COMMITTED

 

다른 트랜잭션에서 커밋된 데이터로만 접근할 수 있게 하는 격리 수준입니다. MySQL을 제외하고 대부분 이를 기본 격리수준으로 사용합니다. 10번 트랜잭션이 '박기영'이라는 데이터를 '박경'으로 UPDATE 한 후 Commit 하지 않았을 때, 13번 트랜잭션에서 이를 조회할 경우 UPDATE 전 데이터인 '박기영' 이라는 값이 조회됩니다. 그럼 어떻게  Read Committed는 UPDATE 전 값을 조회한걸까? 그 키는 바로 Undo 영역에 있습니다.

Undo 영역

트랜잭션에 대한 로그가 반드시 남아있어야 합니다. 즉, 복구는 로그를 기반으로 처리됩니다. 이 로그는 크게 두 가지가 있습니다. 오류에 의한 복구에 사용되는 Redo Log 트랜잭션 롤백을 위해 사용되는 Undo Log입니다.

다시실행의 뜻을 갖는 Redo는 커밋된 트랜잭션에 대한 정보를 갖고 있고, 실행 취소의 뜻을 갖는 Undo는 데이터베이스의 변경이 발생할 경우 변경되기 전 값과 이에 대한 PK 값을 갖고 있습니다.

 

그런데 Undo 영역이라고 말한 이유는 Undo Log가 Undo Log Buffer 형태로 메모리에 저장되고, 특정 시점에 디스크에 저장된 Undo Log File 에 I/O 작업으로 쓰여지기 때문입니다. 추가로 이렇게 단계가 나눠지는 이유는 데이터에 변경사항이 생길때마다 Disk에 I/O 작업을 하는것보다 메모리 입력하고, 읽는것이 속도와 리소스 측면에서 유리하기 때문입니다.

 

정리하면, Undo 영역이란 변경 전 데이터가 저장된 영역이고, Commit 하기 전 데이터를 읽어올 수 있는 이유는 Undo 영역에 있는 데이터를 읽어오기 때문입니다.

 

4. READ UNCOMMITED

 

다른 트랜잭션에서 커밋되지 않은 데이터에 접근할 수 있게 하는 격리 수준입니다. 가장 저수준의 격리수준이며, 일반적으로 사용하지 않는 격리수준입니다.

10번 트랜잭션이 '박기영'이라는 데이터를 '박경'으로 UPDATE 한 후 Commit 하지 않았을 때 13번 트랜잭션에서 접근하여 커밋되지 않은 데이터를 읽을 수 있습니다.

그런데 13번 트랜잭션이 데이터를 읽은 후 10번 트랜잭션에 문제가 발생하여 롤백된다면 데이터 부정합을 발생시킬 수 있습니다. 

데이터 부정합은 어플리케이션에 치명적인 문제를 야기할 수 있습니다. 그래서인지 오라클에서는 이 수준을 아예 지원하지 않습니다. 이처럼 커밋되지 않는 트랜잭션에 접근하여 부정합을 유발할 수 있는 데이터를 읽는 것을 더티읽기(Dirty Read)라고 합니다. 


동시성 문제 해결

동시성 문제 = 동시성 문제는 여러 개의 프로세스나 스레드가 동시에 같은 자원에 접근하거나 수정하려고 할 때 발생하는 문제입니다. 이런 경우 데이터가 꼬이거나 예상치 못한 결과가 나올 수 있습니다. 그로 인해 발생하는 문제로는 대표적으로 데이터 손실, 중복 처리, 오류 등이 발생할 수 있습니다.

 

해결 방법은 다양하지만 크게 3가지 방법에 대해 정리해 보려고 합니다.

  • 낙관적 락/ 비관적 락
  • 충돌 방지 전략
  • 트랜잭션 격리 수준 설정

1. 낙관적 락(Optimistic Lock)/ 비관적 락(Pessimistic Lock)

먼저 소개할 방법은 낙관적 락입니다. 낙관적 락은 이름처럼 동시성 문제에 대해 낙관적입니다. 즉, 동시성 문제가 발생할 가능성이 낮다고 판단하기 때문에 데이터에 접근할 때 락을 걸지 않고 변경을 시도할 때 충돌 여부를 확인합니다. 

 

낙관적 락을 구현하는 대표적인 방법은 버전 번호나 타임스탬프 값을 확인해 다른 트랜잭션이 데이터를 수정했는지 확인하고 만약 데이터가 변경되었다면 현재 트랜잭션을 롤백하고 다시 시도하도록 합니다. 

 

버전 정보를 활용해서 낙관적 락을 구현했을 때 DB에 저장된 버전 정보와 요청 시점에서 버전 정보가 일치하지 않는다면 해당 데이터를 stale data로 판단합니다. stale data란, 애플리케이션이 참조하는 데이터가 DB에 저장된 최신 상태의 데이터와 일치하지 않는 데이터를 의미합니다. 

 

다음은 비관적 락입니다. 비관적 락은 낙관적 락과 다르게 동시성 문제가 발생할 가능성이 높다고 판단하기 때문에 데이터에 접근할 때 락을 걸어 다른 트랜잭션이 데이터를 수정하지 못하도록 막는 방법입니다. 즉, 락이 해제되기 전까지는 다른 트랜잭션은 해당 데이터에 접근할 수 없는 차단(blocking) 접근 방식입니다. 낙관적 락은 반대로 (non-blocking) 방법입니다. 

위 이미지 처럼 비관적 락은 이전 트랜잭션이 락을 걸었다면 이후에 동일한 데이터에 대한 접근을 필요로 하는 다른 트랜잭션의 접근을 차단합니다. 만약 낙관적 락의 경우라면 T2 시점에서 접근은 허용되지만 수정 시 버전 정보나 타임스탬프 값을 보고 수정여부를 결정할 것입니다.

낙관적 락에 비해 비관적 락이 더 안정적으로 동시성 문제를 해결할 수 있다는 분명한 장점이 있지만, 비관적 락을 사용하는 것은 치명적인 단점이 존재합니다. 락을 걸어둔 트랜잭션 외에 다른 트랜잭션의 접근을 막기 때문에 성능이 떨어진다는 단점이 존재합니다. 때문에 낙관적 락과 비관적을 적절하게 사용함으로써 동시성 문제와 성능을 함께 고려하는 것이 중요합니다. 

 

동시성 문제를 해결할 수 있는 두번째 방법은 충돌 방지 전략입니다. 충돌 방지전략은 크게 두 가지로 나뉘며 충돌 회피 전략과 충돌 감지 전략으로 나뉩니다.

 

2. 충돌 방지 전략

동시성 문제를 해결할 수 있는 두번째 방법은 충돌 방지 전략입니다. 충돌 방지전략은 크게 두 가지로 나뉘며 충돌 회피 전략과 충돌 감지 전략으로 나뉩니다. 

 

충돌 회피 전략의 방법중 하나는 데이터 파티셔닝 방법이 있습니다. 데이터 파티셔닝은 DB 수준에서 동시성 문제를 해결하는 방법입니다. 데이터의 변경이 자주 발생할 것이라고 판단되는 데이터를 분산해서 저장함으로써 다수의 사용자의 데이터 접근 가능성을 낮추는 방법입니다. 

 

충돌 감지 전략은 위에서 언급한 낙관적 락과 비관적 락 등이 존재합니다.

 

3. 트랜잭션 격리 수준

마지막 방법인 트랜잭션 격리 수준이란, DB에서 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리하는 정도를 결정하는 방법입니다. SQL 표준은 네 가지 트랜잭션 격리 수준을 정의하고 있으며, 각각의 수준은 동시성 제어와 성능 사이의 균형을 다르게 설정합니다.

 

자세한 내용은 위 내용을 참고해주세요.