락 / 동시성
여러 트랜잭션이 동시에 같은 데이터를 건드리면 값이 꼬일 수 있습니다. 락(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마다 다릅니다.
핵심 개념
데드락을 줄이는 방법
여러 테이블/행을 잠글 때 항상 같은 순서로 접근하면, 서로 반대 순서로 기다리는 상황 자체가 생기지 않습니다.
락의 단위와 종류 더 보기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
데드락이 생기는 조건은 무엇인가요?
상호 배제(락을 한 번에 하나만 가짐), 점유와 대기(락을 쥔 채 다른 락을 기다림), 비선점(락을 강제로 뺏을 수 없음), 순환 대기(서로의 락을 기다림)의 네 가지가 모두 성립할 때 생길 수 있습니다. 이 중 하나를 깨면 예방할 수 있습니다. 운영체제의 데드락과 같은 조건입니다.
면접 질문
데이터베이스에서 데드락을 줄이는 방법은 무엇인가요?
여러 행이나 테이블을 항상 같은 순서로 잠그고, 트랜잭션을 짧게 유지하며, 인덱스를 써서 필요 이상으로 많은 행에 락이 걸리지 않게 합니다. 그래도 생길 수 있으므로 데드락 오류에 대한 재시도 로직을 둡니다.
면접 질문
비관적 락과 낙관적 락은 언제 각각 쓰나요?
일반적으로 충돌이 드물고 읽기가 많으면 낙관적 락이 락 대기를 줄여 줍니다. 충돌이 잦으면 재시도 비용이 커지므로 비관적 락을 고려합니다. 충돌 빈도에 따라 고릅니다.
면접 질문
DB는 데드락을 어떻게 알아내나요?
트랜잭션 사이의 기다림 관계를 그래프(wait-for graph)로 그려 순환이 있는지 확인합니다. PostgreSQL은 락을 일정 시간 기다린 뒤에 이 검사를 시작하고, MySQL InnoDB는 감지를 끄고 락 대기 시간 초과(innodb_lock_wait_timeout)로 롤백하게 설정할 수도 있습니다. 순환이 있으면 한 트랜잭션을 골라 중단시킵니다.
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
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)의 차이는?
Q2
두 트랜잭션이 서로 상대방이 가진 락을 기다리며 영원히 대기하는 상황을 무엇이라 하나요?
Q3
락으로 인한 데드락을 완화하는 일반적인 방법은?