[DB]데이터 충돌 방지: 트랜잭션
데이터베이스는 여러 사용자가 동시에 접근하고 데이터를 수정하는 환경에서 데이터의 무결성과 일관성을 유지하는 것이 매우 중요합니다. 이때 핵심적인 역할을 하는 것이 바로 트랜잭션과 잠금 메커니즘입니다. 이번 글에서는 이 두 가지 개념을 자세히 살펴보고, 실제 상황에서 어떻게 활용되는지 알아보겠습니다. 🚀
1. 트랜잭션: 데이터 작업의 단위 📦
트랜잭션은 데이터베이스에서 수행되는 일련의 작업들을 하나의 논리적인 단위로 묶는 것을 의미합니다. 마치 하나의 업무 처리 과정과 같습니다. 중요한 것은 트랜잭션은 반드시 ACID 속성을 만족해야 데이터의 신뢰성을 보장할 수 있다는 점입니다.
- 원자성 (Atomicity): 트랜잭션 내의 모든 작업은 전부 성공하거나 전부 실패해야 합니다. 부분적인 성공은 허용되지 않습니다. 마치 'All or Nothing'과 같습니다. 예를 들어, 계좌 이체 시 출금과 입금 작업이 모두 성공해야 이체가 완료된 것으로 간주하며, 둘 중 하나라도 실패하면 모든 작업이 취소됩니다.
- 일관성 (Consistency): 트랜잭션이 완료된 후 데이터베이스는 미리 정의된 규칙(제약 조건)을 준수하는 일관된 상태를 유지해야 합니다. 예를 들어, 잔액은 항상 0 이상이어야 한다는 제약 조건이 있다면, 이체 후에도 이 조건이 만족되어야 합니다.
- 고립성 (Isolation): 동시에 실행되는 여러 트랜잭션은 서로에게 영향을 주지 않아야 합니다. 각 트랜잭션은 마치 독립적으로 실행되는 것처럼 동작해야 합니다. 이를 통해 데이터의 오염을 방지합니다.
- 지속성 (Durability): 성공적으로 완료된 트랜잭션의 결과는 영구적으로 데이터베이스에 저장되어야 합니다. 시스템 장애가 발생하더라도 데이터는 유실되지 않아야 합니다.
🚩예제: 트랜잭션의 기본 사용법
-- MySQL에서 트랜잭션 사용
START TRANSACTION;
UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 1;
UPDATE Accounts SET Balance = Balance + 100 WHERE AccountID = 2;
COMMIT;
-- Oracle에서 트랜잭션 사용
BEGIN
UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 1;
UPDATE Accounts SET Balance = Balance + 100 WHERE AccountID = 2;
COMMIT;
END;
위 예제는 계좌 간 이체 작업을 트랜잭션으로 처리하여 데이터의 일관성을 유지합니다.
2. ACID 속성 심화 탐구 💡
원자성과 일관성: 트랜잭션은 원자성을 통해 데이터의 일관성을 보장합니다. 모든 작업이 성공적으로 완료되지 않으면 롤백(Rollback)을 통해 데이터베이스를 트랜잭션 시작 이전의 상태로 되돌립니다.
제약 조건 (Constraints): 데이터의 일관성을 유지하기 위해 데이터베이스에는 다양한 제약 조건을 설정할 수 있습니다. 예를 들어, NOT NULL, UNIQUE, CHECK, FOREIGN KEY 등의 제약 조건을 사용하여 데이터의 유효성을 검사할 수 있습니다.
예제: 제약 조건 위반 방지
-- MySQL에서 제약 조건 적용
ALTER TABLE Accounts ADD CONSTRAINT chk_balance CHECK (Balance >= 0);
-- Oracle에서 제약 조건 적용
ALTER TABLE Accounts ADD CONSTRAINT chk_balance CHECK (Balance >= 0);
위 예제는 잔액이 음수가 되는 것을 방지합니다.
3. 락[Lock] 메커니즘: 동시 접근 제어 🔒
락은 여러 사용자가 동시에 동일한 데이터에 접근하려 할 때 발생할 수 있는 데이터 충돌을 방지하는 핵심적인 메커니즘입니다.
- 공유 잠금 (Shared Lock - S Lock): 데이터를 읽기 위해 사용되는 잠금입니다. 여러 트랜잭션이 동시에 같은 데이터를 읽을 수 있습니다. 읽기 작업 간에는 충돌이 발생하지 않기 때문입니다.
- 배타 잠금 (Exclusive Lock - X Lock): 데이터를 수정하기 위해 사용되는 잠금입니다. 하나의 트랜잭션이 데이터를 수정하는 동안 다른 트랜잭션은 해당 데이터에 접근할 수 없습니다. 이를 통해 데이터의 무결성을 보장합니다.
예제: 잠금의 활용
-- MySQL에서 잠금 사용
LOCK TABLES Accounts WRITE;
UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 1;
UNLOCK TABLES;
-- Oracle에서 잠금 사용
SELECT * FROM Accounts WHERE AccountID = 1 FOR UPDATE;
MySQL에서는 LOCK TABLES 명령어를 사용하여 명시적으로 테이블에 잠금을 걸 수 있지만, 일반적으로는 데이터베이스 시스템이 자동으로 관리하는 암시적 잠금을 사용하는 것이 권장됩니다. UPDATE, DELETE, INSERT 등의 DML 구문 실행 시 자동으로 필요한 잠금이 걸립니다.
예제: 암시적 잠금 예제 (MySQL)
START TRANSACTION;
SELECT * FROM Accounts WHERE AccountID = 1 FOR UPDATE; -- AccountID = 1인 행에 배타 잠금 획득
-- 이후 UPDATE 쿼리 실행
UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = 1;
COMMIT;
FOR UPDATE 구문을 사용하면 해당 행에 배타 잠금을 획득하여 다른 트랜잭션이 해당 행을 수정하지 못하도록 막습니다.
4. 격리 수준: 고립성 보장 🎚️
격리 수준은 트랜잭션 간의 격리 정도를 나타내는 것으로, 어떤 수준의 데이터 오염까지 허용할 것인지를 결정합니다. 격리 수준이 높을수록 데이터 일관성은 높아지지만, 동시성은 낮아질 수 있습니다.
- Read Uncommitted (RU): 커밋되지 않은 데이터도 읽을 수 있습니다. 가장 낮은 격리 수준으로, 데이터 오염 가능성이 매우 높습니다. (더티 리드 발생 가능)
- Read Committed (RC): 커밋된 데이터만 읽을 수 있습니다. 대부분의 데이터베이스 시스템에서 기본적으로 사용하는 격리 수준입니다. (더티 리드 방지)
- Repeatable Read (RR): 동일 트랜잭션 내에서 동일한 데이터를 여러 번 읽어도 항상 같은 결과를 보장합니다. (팬텀 리드 발생 가능)
- Serializable (SI): 가장 높은 격리 수준으로, 트랜잭션이 순차적으로 실행되는 것과 동일한 효과를 냅니다. 동시성은 가장 낮지만 데이터 일관성은 가장 높습니다.
예제: 격리 수준 설정
-- MySQL에서 격리 수준 설정
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- Oracle에서 격리 수준 설정
ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE;
격리 수준을 적절히 설정하여 데이터 일관성을 유지할 수 있습니다.
5. 실습: 은행 계좌 간 이체 구현 🏦
실습 목표
트랜잭션과 잠금을 활용하여 은행 계좌 간 이체를 안전하게 구현합니다.
CREATE TABLE Accounts (
AccountID INT PRIMARY KEY,
Balance DECIMAL(10, 2) NOT NULL DEFAULT 0 CHECK (Balance >= 0) -- 잔액 제약 조건 추가
);
INSERT INTO Accounts (AccountID, Balance) VALUES (1, 1000.00), (2, 500.00);
DELIMITER // -- MySQL에서 프로시저 정의를 위한 구분자 변경
CREATE PROCEDURE TransferMoney(IN fromAccount INT, IN toAccount INT, IN amount DECIMAL(10, 2))
BEGIN
DECLARE exit handler for sqlexception
BEGIN
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '이체 중 오류가 발생했습니다.';
END;
START TRANSACTION;
UPDATE Accounts SET Balance = Balance - amount WHERE AccountID = fromAccount;
UPDATE Accounts SET Balance = Balance + amount WHERE AccountID = toAccount;
COMMIT;
END //
DELIMITER ; -- 구분자 복원
CALL TransferMoney(1, 2, 100); -- 계좌 1에서 계좌 2로 100원 이체
SELECT * FROM Accounts;
이 예제는 프로시저를 사용하여 이체 작업을 트랜잭션으로 처리하고, 예외 발생 시 롤백하도록 구현하여 데이터의 안정성을 높였습니다. 또한 잔액 제약 조건을 테이블 생성 시 추가하여 데이터의 일관성을 더욱 강화했습니다.
6. 마무리 🌟
트랜잭션과 잠금은 데이터베이스 시스템에서 데이터의 무결성과 일관성을 유지하는 데 필수적인 요소입니다. ACID 속성을 이해하고 적절한 격리 수준과 잠금 메커니즘을 활용함으로써 안정적인 데이터베이스 환경을 구축할 수 있습니다. 질문이 있다면 댓글로 남겨주세요! 😊