Skip to main content
Summary

이전 장에서 인덱스가 데이터를 빠르게 찾기 위한 장치라면, 트랜잭션은 데이터를 안전하게 바꾸기 위한 장치입니다.

실무 데이터베이스에서는 조회 속도만큼 중요한 문제가 있습니다. 바로 여러 사용자가 동시에 데이터를 바꿀 때도 결과가 깨지지 않아야 한다는 점입니다. 쇼핑몰에서 동시에 주문이 들어오고, 은행 계좌에서 동시에 송금이 일어나고, 여러 관리자가 같은 데이터를 수정하는 상황은 흔합니다.

트랜잭션과 동시성 제어는 이런 상황에서 데이터의 일관성을 지키는 핵심 개념입니다.

트랜잭션과 동시성 제어

트랜잭션이 필요한 이유

트랜잭션은 여러 SQL 작업을 하나의 논리적 작업 단위로 묶는 방법입니다.

예를 들어 A 계좌에서 B 계좌로 10,000원을 송금한다고 가정해 보겠습니다. 실제로는 최소 두 가지 작업이 필요합니다.

  1. A 계좌 잔액에서 10,000원을 뺀다.

  2. B 계좌 잔액에 10,000원을 더한다.

첫 번째 작업만 성공하고 두 번째 작업이 실패하면 어떻게 될까요? A 계좌의 돈은 줄었지만 B 계좌에는 입금되지 않습니다. 데이터베이스 입장에서는 각각의 SQL이 실행되었을 수 있지만, 업무 입장에서는 실패한 송금입니다.

트랜잭션은 이런 문제를 막습니다. 여러 작업이 모두 성공하면 확정하고, 중간에 하나라도 실패하면 처음 상태로 되돌립니다.

COMMIT과 ROLLBACK

MySQL에서는 보통 InnoDB 스토리지 엔진을 사용합니다. InnoDB는 트랜잭션을 지원합니다.

트랜잭션에서 가장 중요한 명령은 세 가지입니다.

  • START TRANSACTION: 트랜잭션 시작

  • COMMIT: 지금까지의 변경 사항 확정

  • ROLLBACK: 지금까지의 변경 사항 취소

먼저 송금 예제를 위한 테이블을 만들어 보겠습니다.

CREATE TABLE accounts (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    owner_name VARCHAR(50) NOT NULL,
    balance INT NOT NULL
);

INSERT INTO accounts (owner_name, balance)
VALUES
    ('Alice', 50000),
    ('Bob', 30000);

현재 잔액을 확인합니다.

SELECT * FROM accounts;

이제 Alice가 Bob에게 10,000원을 송금하는 작업을 트랜잭션으로 처리해 보겠습니다.

START TRANSACTION;

UPDATE accounts
SET balance = balance - 10000
WHERE owner_name = 'Alice';

UPDATE accounts
SET balance = balance + 10000
WHERE owner_name = 'Bob';

COMMIT;

COMMIT을 실행하면 두 UPDATE 결과가 확정됩니다. 다른 사용자도 변경된 잔액을 볼 수 있습니다.

반대로 중간에 문제가 생겼다면 ROLLBACK을 실행합니다.

START TRANSACTION;

UPDATE accounts
SET balance = balance - 10000
WHERE owner_name = 'Alice';

-- 중간에 문제가 생겼다고 가정
ROLLBACK;

ROLLBACK을 실행하면 Alice의 잔액 차감도 취소됩니다. 트랜잭션을 시작하기 전 상태로 돌아갑니다.

자동 커밋 이해하기

MySQL은 기본적으로 자동 커밋 모드가 켜져 있습니다.

자동 커밋이 켜져 있으면 SQL 한 문장이 실행될 때마다 즉시 확정됩니다. 예를 들어 다음 UPDATE는 별도로 COMMIT을 하지 않아도 바로 반영됩니다.

UPDATE accounts
SET balance = balance + 1000
WHERE id = 1;

학습 중에는 이 점이 헷갈릴 수 있습니다. 트랜잭션을 직접 제어하려면 START TRANSACTION으로 시작하고, 끝에서 COMMIT 또는 ROLLBACK을 명확히 실행하는 습관을 들이는 것이 좋습니다.

현재 자동 커밋 설정은 다음처럼 확인할 수 있습니다.

SELECT @@autocommit;

값이 1이면 자동 커밋이 켜져 있다는 뜻입니다.

자동 커밋을 세션 단위로 끌 수도 있습니다.

SET autocommit = 0;

하지만 초보 단계에서는 전역적으로 자동 커밋을 끄기보다, 필요한 작업에서만 START TRANSACTION을 사용하는 편이 안전합니다.

또 하나 기억할 점이 있습니다. CREATE TABLE, ALTER TABLE, DROP TABLE 같은 대부분의 DDL 문은 MySQL에서 암시적 커밋을 일으킬 수 있습니다. 즉, 트랜잭션 안에서 실행해도 예상처럼 ROLLBACK되지 않을 수 있습니다. 트랜잭션은 주로 INSERT, UPDATE, DELETE 같은 데이터 변경 작업을 안전하게 묶을 때 사용한다고 이해하면 됩니다.

ACID: 안전한 트랜잭션의 네 가지 조건

트랜잭션을 설명할 때 자주 나오는 용어가 ACID입니다. 네 가지 특성의 앞 글자를 모은 말입니다.

Atomicity, 원자성

원자성은 트랜잭션 안의 작업이 전부 성공하거나 전부 실패해야 한다는 뜻입니다.

송금 예제에서 출금과 입금은 함께 성공해야 합니다. 출금만 성공하고 입금이 실패하는 상태는 허용하면 안 됩니다. COMMIT과 ROLLBACK은 원자성을 구현하는 핵심 명령입니다.

Consistency, 일관성

일관성은 트랜잭션이 끝난 뒤에도 데이터가 정해진 규칙을 지켜야 한다는 뜻입니다.

예를 들어 계좌 잔액은 음수가 될 수 없다는 규칙이 있다면, 트랜잭션이 끝난 후에도 이 규칙이 유지되어야 합니다. 데이터베이스의 제약 조건, 외래키, 애플리케이션의 검증 로직이 함께 일관성을 지킵니다.

Isolation, 격리성

격리성은 동시에 실행되는 트랜잭션들이 서로를 방해하지 않도록 분리되어야 한다는 뜻입니다.

두 사용자가 동시에 같은 상품을 주문할 때, 서로의 중간 작업을 함부로 읽거나 덮어쓰면 재고가 잘못 계산될 수 있습니다. 격리성은 이런 문제를 줄이기 위한 특성입니다.

Durability, 지속성

지속성은 COMMIT된 결과가 저장 장치에 안전하게 남아야 한다는 뜻입니다.

한번 확정된 주문, 결제, 송금 결과는 서버가 재시작되더라도 사라지면 안 됩니다. MySQL은 로그와 저장 구조를 사용해 커밋된 데이터를 보존합니다.

주문과 재고 차감 예제

트랜잭션은 송금뿐 아니라 주문 처리에서도 중요합니다.

상품 주문은 보통 다음 작업을 포함합니다.

  1. 재고가 충분한지 확인한다.

  2. 재고를 차감한다.

  3. 주문 내역을 저장한다.

  4. 결제 또는 후속 처리를 진행한다.

간단한 테이블을 만들어 보겠습니다.

CREATE TABLE products (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    stock INT NOT NULL
);

CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL,
    quantity INT NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (product_id) REFERENCES products(id)
);

INSERT INTO products (name, stock)
VALUES ('Keyboard', 5);

재고가 있을 때만 주문을 넣고 재고를 차감하려면 다음처럼 처리할 수 있습니다.

START TRANSACTION;

UPDATE products
SET stock = stock - 1
WHERE id = 1
  AND stock >= 1;

INSERT INTO orders (product_id, quantity)
VALUES (1, 1);

COMMIT;

하지만 이 코드는 아직 부족합니다. UPDATE가 실제로 성공했는지 확인하지 않았기 때문입니다. 재고가 0이면 UPDATE는 아무 행도 바꾸지 않습니다. 그런데 바로 주문을 넣으면 재고가 없는데도 주문이 생성될 수 있습니다.

MySQL에서는 ROW_COUNT()로 직전 변경 행 수를 확인할 수 있습니다.

START TRANSACTION;

UPDATE products
SET stock = stock - 1
WHERE id = 1
  AND stock >= 1;

SELECT ROW_COUNT();

ROW_COUNT() 결과가 1이면 재고 차감에 성공한 것입니다. 그때 주문을 생성하고 COMMIT합니다.

INSERT INTO orders (product_id, quantity)
VALUES (1, 1);

COMMIT;

결과가 0이면 재고가 부족한 것입니다. 이때는 주문을 만들지 말고 되돌립니다.

ROLLBACK;

실제 애플리케이션에서는 이 흐름을 코드로 제어합니다. 중요한 점은 재고 차감과 주문 생성이 하나의 트랜잭션 안에 있어야 한다는 것입니다.

동시에 실행될 때 생기는 문제

데이터베이스는 여러 사용자가 동시에 접근합니다. 그래서 단순히 SQL이 맞는지만 봐서는 부족합니다. 같은 데이터를 동시에 읽고 쓰는 상황을 고려해야 합니다.

대표적인 동시성 문제는 다음과 같습니다.

문제의미예시
Dirty Read아직 커밋되지 않은 데이터를 다른 트랜잭션이 읽음취소될 송금 결과를 미리 읽음
Non-repeatable Read같은 행을 두 번 읽었는데 값이 달라짐처음 잔액은 50,000원이었는데 같은 트랜잭션 안에서 다시 읽으니 40,000원
Phantom Read같은 조건으로 다시 조회했는데 없던 행이 생기거나 사라짐주문 목록을 다시 조회했더니 새 주문 행이 나타남
Lost Update동시에 수정하면서 한쪽 변경이 덮어써짐두 사용자가 같은 재고를 읽고 각각 차감했지만 한 번만 반영됨

이 문제들을 모두 완벽하게 막으면 안전하지만, 그만큼 동시 처리 성능이 떨어질 수 있습니다. 반대로 너무 느슨하게 허용하면 빠르지만 데이터가 흔들릴 수 있습니다.

그래서 데이터베이스는 격리 수준이라는 선택지를 제공합니다.

격리 수준 이해하기

격리 수준은 동시에 실행되는 트랜잭션들이 서로의 변경 사항을 어느 정도까지 볼 수 있는지를 정하는 규칙입니다.

MySQL InnoDB에서 자주 다루는 격리 수준은 다음 네 가지입니다.

격리 수준특징주의할 점
READ UNCOMMITTED커밋되지 않은 변경도 읽을 수 있음Dirty Read 가능. 실무에서는 거의 사용하지 않음
READ COMMITTED커밋된 데이터만 읽음같은 트랜잭션 안에서도 다시 읽으면 값이 달라질 수 있음
REPEATABLE READ트랜잭션 중 같은 조회는 같은 결과를 보도록 함MySQL InnoDB의 기본값. 일반 조회는 안정적이지만 잠금 읽기와 쓰기는 별도로 이해해야 함
SERIALIZABLE트랜잭션을 거의 순서대로 실행하는 수준으로 강하게 격리안전하지만 동시성이 크게 떨어질 수 있음

MySQL InnoDB의 기본 격리 수준은 보통 REPEATABLE READ입니다. 이 수준에서는 트랜잭션이 시작된 뒤 일반 SELECT로 읽은 데이터가 같은 트랜잭션 안에서 일관되게 보입니다.

다만 모든 상황이 단순하지는 않습니다. 일반 SELECT는 잠금을 걸지 않는 읽기이고, SELECT ... FOR UPDATE 같은 잠금 읽기는 실제 행에 잠금을 겁니다. 격리 수준을 이해할 때는 읽기 방식도 함께 봐야 합니다.

현재 세션의 격리 수준은 다음처럼 확인할 수 있습니다.

SELECT @@transaction_isolation;

격리 수준은 트랜잭션을 시작하기 전에 설정합니다.

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

START TRANSACTION;

-- 필요한 작업 실행

COMMIT;

현재 트랜잭션에만 적용하려면 다음처럼 사용할 수 있습니다.

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

START TRANSACTION;

-- 필요한 작업 실행

COMMIT;

격리 수준을 무조건 높이는 것이 정답은 아닙니다. 중요한 데이터는 강하게 보호해야 하지만, 너무 강한 격리는 잠금 대기와 성능 저하를 만들 수 있습니다.

READ COMMITTED와 REPEATABLE READ 비교

두 격리 수준의 차이는 같은 데이터를 다시 읽을 때 잘 드러납니다.

예를 들어 트랜잭션 A가 상품 재고를 조회합니다.

START TRANSACTION;

SELECT stock
FROM products
WHERE id = 1;

그 사이 트랜잭션 B가 같은 상품의 재고를 변경하고 커밋합니다.

START TRANSACTION;

UPDATE products
SET stock = stock - 1
WHERE id = 1;

COMMIT;

이제 트랜잭션 A가 다시 같은 상품을 조회한다고 해 보겠습니다.

SELECT stock
FROM products
WHERE id = 1;

READ COMMITTED에서는 트랜잭션 B가 커밋한 최신 값을 볼 수 있습니다. 그래서 같은 트랜잭션 안에서 첫 번째 조회와 두 번째 조회 결과가 달라질 수 있습니다.

REPEATABLE READ에서는 일반 조회 기준으로 처음 읽은 시점의 일관된 결과를 계속 봅니다. 그래서 같은 트랜잭션 안에서는 값이 바뀌지 않은 것처럼 보입니다.

어느 쪽이 더 좋은지는 상황에 따라 다릅니다.

  • 최신 커밋 데이터를 매번 보는 것이 중요하면 READ COMMITTED가 적합할 수 있습니다.

  • 하나의 작업 흐름 안에서 같은 기준의 데이터를 유지하는 것이 중요하면 REPEATABLE READ가 적합할 수 있습니다.

MySQL을 처음 배울 때는 기본값인 REPEATABLE READ를 기준으로 익히고, 실무에서 필요할 때 격리 수준을 조정하는 방식이 좋습니다.

행 잠금과 SELECT ... FOR UPDATE

동시성 제어에서 가장 중요한 도구 중 하나가 잠금입니다. 잠금은 다른 트랜잭션이 같은 데이터를 동시에 바꾸지 못하도록 막는 장치입니다.

MySQL InnoDB는 주로 행 단위 잠금을 사용합니다. 행 단위 잠금은 테이블 전체가 아니라 필요한 행만 잠그는 방식입니다. 덕분에 서로 다른 행을 수정하는 트랜잭션은 동시에 진행될 수 있습니다.

예를 들어 특정 상품의 재고를 안전하게 확인한 뒤 차감하려면 SELECT ... FOR UPDATE를 사용할 수 있습니다.

START TRANSACTION;

SELECT stock
FROM products
WHERE id = 1
FOR UPDATE;

UPDATE products
SET stock = stock - 1
WHERE id = 1
  AND stock >= 1;

INSERT INTO orders (product_id, quantity)
VALUES (1, 1);

COMMIT;

FOR UPDATE는 조회한 행을 수정할 예정이니 다른 트랜잭션이 건드리지 못하게 잠그겠다는 뜻입니다. 잠금은 COMMIT 또는 ROLLBACK할 때 해제됩니다.

이 기능은 다음 상황에서 유용합니다.

  • 재고를 확인하고 차감할 때

  • 계좌 잔액을 확인하고 출금할 때

  • 같은 쿠폰이 중복 사용되지 않도록 막을 때

  • 처리 대기 중인 작업을 하나의 작업자만 가져가게 할 때

단, FOR UPDATE를 남용하면 대기 시간이 길어질 수 있습니다. 꼭 필요한 행만 잠그고, 트랜잭션을 짧게 유지해야 합니다.

인덱스와 잠금의 관계

행 잠금은 인덱스와도 관련이 깊습니다. InnoDB는 조건에 맞는 행을 찾기 위해 인덱스를 사용하고, 찾은 행 또는 범위에 잠금을 겁니다.

조건에 적절한 인덱스가 없으면 MySQL은 더 많은 행을 훑어야 합니다. 이 과정에서 예상보다 넓은 범위가 잠기거나, 다른 트랜잭션이 오래 기다릴 수 있습니다.

예를 들어 products.id는 기본키이므로 다음 쿼리는 특정 행을 빠르게 찾고 잠급니다.

SELECT stock
FROM products
WHERE id = 1
FOR UPDATE;

반면 인덱스가 없는 열로 많은 데이터를 검색하면서 잠금을 걸면 부담이 커질 수 있습니다.

SELECT stock
FROM products
WHERE name = 'Keyboard'
FOR UPDATE;

name에 인덱스가 없다면 MySQL은 테이블을 더 넓게 스캔할 수 있습니다. 그래서 잠금을 사용하는 쿼리일수록 조건 열에 적절한 인덱스가 있는지 확인해야 합니다.

이전 장에서 배운 인덱스는 단순히 조회 속도만 높이는 도구가 아닙니다. 트랜잭션 환경에서는 잠금 범위를 줄이고 대기 시간을 줄이는 데도 중요합니다.

잠금 대기 상황

잠금 대기는 한 트랜잭션이 이미 잠근 데이터를 다른 트랜잭션이 수정하려고 할 때 발생합니다.

두 개의 MySQL 세션을 열고 실험해 보면 쉽게 이해할 수 있습니다.

세션 A에서 다음을 실행합니다.

START TRANSACTION;

UPDATE products
SET stock = stock - 1
WHERE id = 1;

아직 COMMIT하지 않습니다.

이제 세션 B에서 같은 행을 수정합니다.

START TRANSACTION;

UPDATE products
SET stock = stock - 1
WHERE id = 1;

세션 B의 쿼리는 바로 끝나지 않고 기다릴 수 있습니다. 세션 A가 같은 행을 이미 잠그고 있기 때문입니다.

세션 A에서 COMMIT하면 세션 B의 쿼리가 이어서 실행됩니다.

COMMIT;

잠금 대기 자체는 오류가 아닙니다. 동시에 같은 데이터를 수정할 때 자연스럽게 발생할 수 있습니다. 문제는 대기가 너무 길어지는 경우입니다.

잠금 대기를 줄이려면 다음 원칙을 지켜야 합니다.

  • 트랜잭션 안에서 불필요한 작업을 하지 않는다.

  • 사용자 입력을 기다리는 동안 트랜잭션을 열어 두지 않는다.

  • 외부 API 호출은 가능하면 트랜잭션 밖에서 처리한다.

  • 필요한 행만 정확히 잠그도록 인덱스를 준비한다.

  • 같은 데이터를 수정하는 쿼리 순서를 일관되게 유지한다.

데드락 이해하기

데드락은 두 개 이상의 트랜잭션이 서로의 잠금 해제를 기다리면서 더 이상 진행하지 못하는 상태입니다.

예를 들어 두 계좌 사이의 송금을 생각해 보겠습니다.

트랜잭션 A는 1번 계좌를 먼저 잠그고, 이어서 2번 계좌를 잠그려고 합니다.

START TRANSACTION;

UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;

UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;

동시에 트랜잭션 B는 반대로 2번 계좌를 먼저 잠그고, 이어서 1번 계좌를 잠그려고 합니다.

START TRANSACTION;

UPDATE accounts
SET balance = balance - 1000
WHERE id = 2;

UPDATE accounts
SET balance = balance + 1000
WHERE id = 1;

상황에 따라 A는 2번 계좌 잠금을 기다리고, B는 1번 계좌 잠금을 기다릴 수 있습니다. 서로가 서로를 기다리므로 진행할 수 없습니다. 이것이 데드락입니다.

MySQL InnoDB는 데드락을 감지하면 둘 중 하나의 트랜잭션을 실패시켜 상황을 풀어냅니다. 실패한 트랜잭션은 애플리케이션에서 다시 시도해야 합니다.

데드락은 데이터베이스가 고장 났다는 뜻이 아닙니다. 동시 수정이 많은 시스템에서는 발생할 수 있는 정상적인 예외 상황입니다. 중요한 것은 줄이는 설계와 재시도 로직입니다.

데드락을 줄이는 방법

데드락을 완전히 없애기는 어렵지만, 자주 발생하지 않도록 줄일 수는 있습니다.

가장 중요한 원칙은 잠그는 순서를 통일하는 것입니다.

계좌 송금이라면 항상 작은 계좌 ID부터 잠그도록 정할 수 있습니다.

START TRANSACTION;

SELECT *
FROM accounts
WHERE id IN (1, 2)
ORDER BY id
FOR UPDATE;

UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;

UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;

COMMIT;

모든 트랜잭션이 같은 순서로 잠금을 잡으면 서로 반대 방향으로 기다리는 상황이 줄어듭니다.

데드락을 줄이는 실무 원칙은 다음과 같습니다.

  • 여러 행을 수정할 때는 항상 같은 기준으로 정렬해서 접근한다.

  • 트랜잭션을 가능한 짧게 유지한다.

  • 큰 범위의 UPDATE, DELETE를 한 번에 실행하지 않는다.

  • 조건 열에 적절한 인덱스를 만든다.

  • 데드락이 발생하면 전체 트랜잭션을 다시 시도한다.

특히 마지막 원칙이 중요합니다. 데드락으로 실패한 트랜잭션은 일부만 다시 실행하면 안 됩니다. 처음부터 트랜잭션 전체를 다시 실행해야 데이터 일관성을 지킬 수 있습니다.

SAVEPOINT로 일부만 되돌리기

트랜잭션 전체를 되돌리는 ROLLBACK 외에, 중간 지점까지만 되돌리는 기능도 있습니다. 이를 SAVEPOINT라고 합니다.

START TRANSACTION;

UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;

SAVEPOINT after_withdraw;

UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;

-- 입금 처리에 문제가 있다고 가정
ROLLBACK TO SAVEPOINT after_withdraw;

ROLLBACK;

SAVEPOINT는 복잡한 처리에서 일부 작업만 되돌릴 때 유용합니다. 하지만 처음 학습할 때는 전체 트랜잭션을 COMMIT하거나 ROLLBACK하는 흐름을 먼저 확실히 익히는 것이 좋습니다.

트랜잭션을 설계할 때의 기준

트랜잭션은 길게 잡을수록 안전해 보일 수 있지만, 실제로는 그렇지 않습니다. 트랜잭션이 길어지면 잠금이 오래 유지되고, 다른 작업이 기다릴 가능성이 커집니다.

좋은 트랜잭션은 짧고 명확합니다.

다음 기준으로 생각하면 됩니다.

  • 반드시 함께 성공해야 하는 작업만 하나의 트랜잭션으로 묶는다.

  • 조회만 필요한 작업은 불필요하게 잠그지 않는다.

  • 수정 대상 행을 가능한 한 빨리 찾고, 빨리 커밋한다.

  • 트랜잭션 안에서 파일 업로드, 이메일 발송, 외부 결제 승인 대기처럼 오래 걸리는 작업을 피한다.

  • 실패했을 때 ROLLBACK할 수 있는 범위를 명확히 정한다.

예를 들어 주문 처리에서 결제 승인 요청까지 데이터베이스 트랜잭션 안에 넣으면, 외부 결제 서버가 응답할 때까지 재고 행이 잠길 수 있습니다. 이런 구조는 동시 주문이 많은 서비스에서 병목이 됩니다.

보통은 데이터베이스 안에서 반드시 원자적으로 처리해야 하는 부분과 외부 시스템 호출을 분리해서 설계합니다.

실무에서 자주 쓰는 패턴

재고 차감처럼 경쟁이 많은 작업에서는 다음 패턴이 유용합니다.

START TRANSACTION;

UPDATE products
SET stock = stock - 1
WHERE id = 1
  AND stock >= 1;

-- 애플리케이션에서 변경 행 수 확인
-- 변경 행 수가 0이면 ROLLBACK
-- 변경 행 수가 1이면 주문 생성

INSERT INTO orders (product_id, quantity)
VALUES (1, 1);

COMMIT;

이 방식은 재고 확인과 차감을 하나의 UPDATE로 합칩니다. 먼저 SELECT로 읽고 나중에 UPDATE하는 방식보다 단순하고 안전한 경우가 많습니다.

쿠폰 사용처럼 중복 처리가 문제 되는 경우에도 비슷한 방식으로 생각할 수 있습니다.

UPDATE coupons
SET used_at = NOW()
WHERE id = 10
  AND used_at IS NULL;

변경 행 수가 1이면 쿠폰 사용에 성공한 것이고, 0이면 이미 사용되었거나 존재하지 않는 쿠폰입니다.

동시성 제어의 핵심은 읽고 판단하고 쓰는 과정을 최대한 안전하게 만드는 것입니다. 가능하면 데이터베이스의 조건부 변경, 유니크 제약, 외래키, 트랜잭션을 함께 사용해야 합니다.

이번 장의 핵심 정리

트랜잭션은 여러 SQL을 하나의 작업처럼 묶는 방법입니다. 모두 성공하면 COMMIT하고, 중간에 문제가 생기면 ROLLBACK합니다.

ACID는 안전한 트랜잭션이 가져야 할 성질을 설명합니다. 원자성은 전부 성공 또는 전부 실패를 뜻합니다. 일관성은 데이터 규칙이 깨지지 않는다는 뜻입니다. 격리성은 동시에 실행되는 트랜잭션이 서로를 방해하지 않도록 하는 성질입니다. 지속성은 커밋된 결과가 사라지지 않는다는 뜻입니다.

격리 수준은 동시 실행 중 서로의 변경 사항을 얼마나 볼 수 있는지 정합니다. MySQL InnoDB의 기본값은 일반적으로 REPEATABLE READ입니다. READ COMMITTED는 커밋된 최신 데이터를 더 자주 보지만, 같은 트랜잭션 안에서 조회 결과가 달라질 수 있습니다.

잠금은 동시에 같은 데이터를 수정할 때 충돌을 막습니다. SELECT ... FOR UPDATE는 수정할 행을 미리 잠글 때 사용합니다. 잠금은 필요한 만큼만 사용하고, 트랜잭션은 짧게 유지해야 합니다.

데드락은 서로의 잠금을 기다리다 멈추는 상황입니다. 잠금 순서를 통일하고, 트랜잭션을 짧게 만들고, 실패 시 재시도하는 방식으로 대응합니다.

실무 데이터베이스에서는 정확한 결과만큼이나 동시에 접근해도 깨지지 않는 구조가 중요합니다. 트랜잭션과 동시성 제어는 그 구조를 만드는 기본 도구입니다.

책 목록

처음부터 실무까지 배우는 MySQL

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기