컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
db · 05

락 / 동시성

DB락동시성

여러 트랜잭션이 동시에 같은 데이터를 건드리면 값이 꼬일 수 있습니다. 락(Lock)은 트랜잭션이 데이터를 다루는 동안 다른 트랜잭션이 끼어들지 못하게 막는 장치입니다. 그런데 락을 서로 다른 순서로 요청하면, 두 트랜잭션이 서로가 가진 락을 기다리며 영원히 멈추는 데드락(Deadlock)이 생길 수 있습니다.

핵심 개념

공유락 (Shared Lock, S)

읽기용 락입니다. 여러 트랜잭션이 같은 데이터에 동시에 걸 수 있습니다 — 읽기끼리는 서로 방해가 안 됩니다. MVCC를 쓰는 DB(PostgreSQL, Oracle, MySQL InnoDB)의 일반 SELECT는 기본 격리 수준에서 행 읽기 락을 걸지 않습니다. 락 기반으로 읽는 DB는 다르고, InnoDB SERIALIZABLE 같은 예외도 있습니다(아래 오해 참고).

핵심 개념

배타락 (Exclusive Lock, X)

쓰기(INSERT/UPDATE/DELETE)용 락입니다. 한 번에 하나의 트랜잭션만 가질 수 있고, 같은 데이터에 대한 다른 S·X 락과 호환되지 않습니다(MVCC의 일반 SELECT는 행 락을 요청하지 않아 행 쓰기에 막히지 않습니다).

핵심 개념

락 대기 (Lock Wait)

이미 다른 트랜잭션이 배타락을 쥐고 있으면, 후발 트랜잭션은 그 락이 풀릴 때까지 멈춰서 기다립니다.

핵심 개념

데드락 (Deadlock)

트랜잭션 A가 B의 락을, B가 A의 락을 동시에 기다리는 순환 대기 상태입니다. 아무도 스스로 풀려날 수 없습니다.

핵심 개념

데드락 감지와 희생자(Victim)

많은 DB는 대기-그래프(wait-for graph)에서 순환을 찾아내면, 그중 한 트랜잭션(또는 문장)을 롤백시켜 순환을 끊습니다. 방식과 롤백 범위는 DB마다 다릅니다.

핵심 개념

데드락을 줄이는 방법

여러 테이블/행을 잠글 때 항상 같은 순서로 접근하면, 서로 반대 순서로 기다리는 상황 자체가 생기지 않습니다.

락 대기는 정상, 데드락은 비정상 — "락 대기" 탭처럼 한쪽이 끝날 때까지 기다리는 건 정상적인 동시성 제어입니다. 문제는 서로가 서로를 기다려 아무도 끝날 수 없을 때입니다.
순서만 바꿔도 데드락을 피할 수 있습니다 — 이 시각화의 데드락 예시에서, B가 A와 똑같이 "김민준 → 이서연" 순서로 락을 요청했다면 데드락은 아예 발생하지 않았을 것입니다.
희생자는 누가 될까 — 어느 트랜잭션을 희생시킬지는 DB마다 다릅니다. 예를 들어 MySQL InnoDB는 변경한 행이 적은 작은 트랜잭션을 고르려고 합니다. 이 시각화에서는 B가 선택되어 롤백되고, A는 계속 진행합니다.
이 시각화에서 확인할 것 — "락의 종류" 탭에서 S/X 호환표를 먼저 익히고, "락 대기" 탭에서 정상적인 대기를, "데드락" 탭에서 두 화살표가 서로를 가리키는 순환이 만들어지는 순간과 그 이후 희생자 롤백을 확인하세요.

락의 단위와 종류 더 보기

락 단위(granularity) — 락은 행, 페이지, 테이블 같은 단위로 걸 수 있습니다. 단위가 작을수록 동시에 처리할 수 있는 일이 많지만 락을 관리하는 비용이 늘어납니다.
의도 락 — 행에 락을 걸기 전에 테이블 같은 상위 단위에 "아래에서 락을 걸 예정"이라는 표시를 해 두는 방식입니다. 상위 단위 락과 충돌하는지를 빠르게 판단하게 해 줍니다. 이름과 구현은 DB마다 다릅니다.
비관적 락 vs 낙관적 락 — 비관적 락은 충돌이 날 것으로 보고 읽을 때부터 락을 겁니다(SELECT ... FOR UPDATE). 낙관적 락은 DB 락을 미리 잡지 않고 진행하다가 저장할 때 버전 값이 바뀌었는지 확인해 충돌하면 실패시킵니다(JPA의 @Version).

자주 하는 오해

SELECT는 항상 공유락을 건다 — PostgreSQL, Oracle, MySQL InnoDB처럼 MVCC를 쓰는 DB는 기본 격리 수준에서 일반 SELECT가 행 읽기 락 없이 스냅샷을 읽습니다(autocommit을 끈 InnoDB SERIALIZABLE처럼 예외가 있습니다). 읽기에 락을 거는 것은 SELECT ... FOR SHARE(PostgreSQL, MySQL)나 FOR UPDATE처럼 명시했을 때나, 락 기반으로 격리를 구현하는 DB에서입니다.
데드락은 DB의 버그다 — 락을 서로 다른 순서로 요청하는 정상적인 코드에서도 생길 수 있고, DB가 이를 감지해 한쪽을 중단시키는 것이 설계된 동작입니다. 중단된 트랜잭션은 애플리케이션이 다시 시도해야 합니다.
데드락이 나면 DB가 둘 다 알아서 풀어 준다 — DB는 한쪽 트랜잭션(DB에 따라 문장)을 에러로 롤백시켜 순환을 끊고 다른 한쪽을 계속 진행시킵니다. 롤백된 쪽은 오류를 받으므로 재시도 처리가 없으면 요청이 실패합니다.
락이 없는 편이 더 안전하다 — 락 없이 동시에 같은 데이터를 고치면 값이 꼬입니다. 락 대신 낙관적 방식을 쓰더라도 충돌을 감지하는 장치는 필요합니다.
트랜잭션을 길게 열어 두어도 문제없다 — 쓰기 락은 보통 커밋이나 롤백까지 유지되므로 트랜잭션이 길수록 다른 트랜잭션이 오래 기다리고 데드락 가능성도 커집니다.

면접에서 이렇게 물어요

면접 질문

데드락이 생기는 조건은 무엇인가요?

상호 배제(락을 한 번에 하나만 가짐), 점유와 대기(락을 쥔 채 다른 락을 기다림), 비선점(락을 강제로 뺏을 수 없음), 순환 대기(서로의 락을 기다림)의 네 가지가 모두 성립할 때 생길 수 있습니다. 이 중 하나를 깨면 예방할 수 있습니다. 운영체제의 데드락과 같은 조건입니다.

면접 질문

데이터베이스에서 데드락을 줄이는 방법은 무엇인가요?

여러 행이나 테이블을 항상 같은 순서로 잠그고, 트랜잭션을 짧게 유지하며, 인덱스를 써서 필요 이상으로 많은 행에 락이 걸리지 않게 합니다. 그래도 생길 수 있으므로 데드락 오류에 대한 재시도 로직을 둡니다.

면접 질문

비관적 락과 낙관적 락은 언제 각각 쓰나요?

일반적으로 충돌이 드물고 읽기가 많으면 낙관적 락이 락 대기를 줄여 줍니다. 충돌이 잦으면 재시도 비용이 커지므로 비관적 락을 고려합니다. 충돌 빈도에 따라 고릅니다.

면접 질문

DB는 데드락을 어떻게 알아내나요?

트랜잭션 사이의 기다림 관계를 그래프(wait-for graph)로 그려 순환이 있는지 확인합니다. PostgreSQL은 락을 일정 시간 기다린 뒤에 이 검사를 시작하고, MySQL InnoDB는 감지를 끄고 락 대기 시간 초과(innodb_lock_wait_timeout)로 롤백하게 설정할 수도 있습니다. 순환이 있으면 한 트랜잭션을 골라 중단시킵니다.

실제 시스템에서는

PostgreSQL — 락을 기다린 시간이 deadlock_timeout(기본 1초)을 넘으면 데드락 검사를 시작하고, 데드락이면 한 트랜잭션에 "deadlock detected" 오류를 냅니다. pg_locks 뷰로 현재 락 상태를 볼 수 있습니다.
MySQL InnoDB — SHOW ENGINE INNODB STATUS 출력의 "LATEST DETECTED DEADLOCK" 부분에 마지막 데드락의 관련 트랜잭션과 쿼리가 나옵니다. 희생자를 고를 때는 변경한 행이 적은 작은 트랜잭션을 우선하려고 합니다.
JPA와 Spring Data JPA — 엔티티에 @Version 필드를 두면 낙관적 락이, 조회에 @Lock(PESSIMISTIC_WRITE)를 주면 비관적 락이 적용됩니다.

함께 보면 좋은 토픽

트랜잭션 — 락이 격리 수준과 어떻게 이어지는지 봅니다.
교착 상태(운영체제) — 같은 현상을 프로세스와 자원 관점에서 봅니다.
인덱스 — 인덱스가 락이 걸리는 범위에 영향을 주는 이유를 봅니다.
NoSQL — NoSQL 저장소가 동시성을 다루는 방식을 봅니다.

참고 자료

교재 — Silberschatz, Korth, Sudarshan, "Database System Concepts" 7판, "Concurrency Control" 장

SQL — SELECT ... FOR UPDATE로 명시적 배타락 걸기

START TRANSACTION;

-- 이 행에 배타락(X)을 걸고 조회 — 다른 트랜잭션은 이 행을 잠금이 풀릴 때까지 못 건드림
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;

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

COMMIT;

FOR UPDATE는 단순 조회가 아니라 "이 값을 곧 수정할 거니 아무도 건드리지 마라"는 의미입니다 — 이 시각화의 "락 대기" 탭에서 A가 김민준 행에 배타락을 거는 것과 같은 동작입니다.
이 구문이 없으면 SELECT는 보통 락을 걸지 않아서, 조회한 값을 기준으로 계산한 뒤 UPDATE하는 사이에 다른 트랜잭션이 값을 바꿔버릴 수 있습니다.

Java — JPA에서 비관적 락(Pessimistic Lock) 사용

public interface AccountRepository extends JpaRepository<Account, Long> {

    // FOR UPDATE와 동일한 배타락을 걸고 조회
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT a FROM Account a WHERE a.id = :id")
    Optional<Account> findByIdForUpdate(@Param("id") Long id);
}

@Service
@RequiredArgsConstructor
public class TransferService {

    private final AccountRepository accountRepository;

    @Transactional
    public void withdraw(Long accountId, long amount) {
        Account account = accountRepository.findByIdForUpdate(accountId)
                .orElseThrow(() -> new NotFoundException("계좌 없음"));
        account.withdraw(amount);
    }
}

PESSIMISTIC_WRITE가 SQL의 FOR UPDATE로 변환됩니다. 메서드가 끝나(COMMIT) 락이 풀릴 때까지, 같은 행을 조회하는 다른 트랜잭션은 이 메서드가 끝날 때까지 대기합니다.

Java — 데드락 예외를 잡아 재시도하기

@Service
@RequiredArgsConstructor
public class TransferRetryService {

    private final TransferService transferService;

    public void transferWithRetry(Long fromId, Long toId, long amount) {
        int attempts = 0;
        while (true) {
            try {
                transferService.transfer(fromId, toId, amount);
                return;
            } catch (CannotAcquireLockException e) {
                attempts++;                                // DB가 데드락 희생자로 골라 롤백시킨 경우
                if (attempts >= 3) throw e;
                log.warn("데드락으로 롤백됨, 재시도 {}회", attempts);
            }
        }
    }
}

DB가 데드락의 희생자로 트랜잭션을 골라 강제 ROLLBACK시키면, Spring은 이를 CannotAcquireLockException으로 변환해 던집니다.
이 시각화의 "데드락" 탭에서 B가 희생자로 롤백되는 것처럼, 실제 애플리케이션에서도 한쪽 트랜잭션은 예외를 받게 되므로, 짧은 대기 후 재시도하는 로직을 흔히 함께 둡니다.

Q1

공유락(Shared Lock)과 배타락(Exclusive Lock)의 차이는?

'공유'는 여럿이 같이, '배타'는 혼자만입니다.
공유락(S-Lock)은 읽기용으로 여러 트랜잭션이 동시에 획득할 수 있지만, 배타락(X-Lock)은 쓰기용으로 한 번에 하나의 트랜잭션만 획득할 수 있어 다른 트랜잭션의 읽기/쓰기를 모두 막습니다.

Q2

두 트랜잭션이 서로 상대방이 가진 락을 기다리며 영원히 대기하는 상황을 무엇이라 하나요?

OS의 교착 상태(Deadlock)와 같은 개념이 DB에도 있습니다.
데드락은 두 개 이상의 트랜잭션이 서로 상대가 점유한 락을 기다리며 순환적으로 대기해, 아무도 진행하지 못하는 상황입니다.

Q3

락으로 인한 데드락을 완화하는 일반적인 방법은?

순서를 통일하면 순환 대기가 생기기 어렵습니다.
여러 트랜잭션이 자원에 접근하는 순서를 일관되게 정하면 순환 대기 조건이 깨져 데드락을 예방할 수 있고, DB는 보통 데드락 감지 시 한 트랜잭션을 타임아웃/강제 롤백시켜 해소합니다.