DB 다룰 때 Optimistic Lock과 Pessimistic Lock
SI나 서비스 개발할때 얼마나 세밀하게 조정하면서 개발하시나요?
트랜잭션 격리수준이 PostgreSQL과 MySql도 다르던데
트랜잭션 격리수준 레벨들과 Optimistic Lock과 Pessimistic Lock 류의 다양한 LockModeType 들
JPA에서 제공하는거 DB자체에서의 2PL .. API 종류에 따라서 이거 다 신경 제대로 쓰면서 개발하면 엄청 빡셀거 같은데..
JPA로 백엔드 개발 시작한 주니어분들은 어느정도 이해하고 하려나 싶고..
항상 정해져있는 답안지들이 있어서 그 정도만 하시려나요?
(비관적락 추가 설정이 반드시 필요한 곳 & Phantom Read 방지 할 필요가 있는 곳)
사실 이런게 프로덕트의 다양한 퀄리티 요소중 하나이기도 할 꺼고..
돈 많이주고 기간 충분하고 오너(혹은 갑)의 의지가 충만하면 퀄리티 높아지는거고
그냥 돌아가면 땡이면 필요도 없고.. 즉 개발자의 차이보다는 발주자(혹은 서비스사 CEO,CTO) 의 차이겠고..
GPT + 개인적으로 상세 설명 추가)
1. 트랜잭션 격리 - 상대 트랜잭션의 행위를 얼만큼 받아 드릴 것인가에 대한 문제
💡 배경 예시: 가족 가계부를 쓰는 상황
아빠(A), 엄마(B), 아들(C)이 하나의 가계부를 같이 씀
각자 잔액을 조회하거나 돈을 기록(지출/입금)함
격리 수준은 이들이 얼마나 동시에 안전하게 쓸 수 있는지를 조절함
🔹 Read Uncommitted (커밋되지 않은 읽기 허용)
실생활 비유: 아빠가 아직 계산 다 안 끝났는데 엄마가 가계부 몰래 들춰봄
문제: 아직 확정되지 않은 내용을 읽고 잘못된 결정을 내림 (Dirty Read)
DB 상황: 한 트랜잭션이 아직
COMMIT안 한 데이터를 다른 트랜잭션이 읽을 수 있음
기술적으로는 A트랜잭션이 새 값으로 수정하고 아직 Commit이 안했는데, B트랜잭션은 변경된 로그를 읽어갔던 것MySQL:
READ UNCOMMITTED(InnoDB 지원)PostgreSQL: 지원하지 않음
🔹 Read Committed (커밋된 것만 읽기)
실생활 비유: 아빠가 계산 완료한 가계부만 엄마가 볼 수 있음
문제: 엄마가 두 번 조회할 때 아빠가 그 사이에 기록을 바꾸면 서로 다른 금액을 보게 됨 (Non-repeatable Read)
DB 상황:
COMMIT된 데이터만 읽을 수 있지만, 트랜잭션 중간에 다른 사람이 내용을 바꾸면 다시 읽었을 때 값이 달라짐
기술적으로 B트랜잭션은 COMMIT 상태의 값만 읽음. 즉 A트랜잭션이 고치고 있던 것을 읽을 수 없음. 근데 B트랜잭션에서 데이터를 읽고 먼가를 하다가 다시 또 읽을 경우 그 중간에 A트래잭션이 고치고 커밋해놓으면,B트랜잭션은 고쳐진 값을 읽게됨. 즉 한번의 트랜잭션중에 두번 읽을 때 달라 질 수 있음.MySQL:
READ COMMITTEDPostgreSQL: 기본 격리 수준
🔹 Repeatable Read (반복 가능한 읽기)
실생활 비유: 엄마가 가계부를 열면 그때 본 기록만 계속 유지됨. 아빠가 그 사이 다른 걸 바꿔도 엄마는 처음 본 것만 계속 봄
문제: 아빠가 새로운 항목을 추가하면, 엄마는 그것도 못 봄 (Phantom Read)
DB 상황: 읽은 행은 변경되지 않지만 새로운 행의 삽입(팬텀)은 감지 못함
이건 한번 읽은 값은 계속 그 버전의 값을 읽으므로 하나의 트랜잭션에서 동일한 값을 사용함. 다만 > 5 이렇게 범위로 select 할 경우, 새로운 값이 추가 됬다는 것을 감지 못하여 동일성이 깨질 수가 있음.MySQL:
REPEATABLE READ(기본값)PostgreSQL: Snapshot isolation을 사용해 팬텀도 방지함
🔹 Serializable (직렬화)
실생활 비유: 아빠가 가계부 쓰는 동안, 엄마는 절대 접근 금지. 끝날 때까지 대기
문제: 성능이 낮고 동시성이 떨어짐
DB 상황: 마치 트랜잭션이 순서대로 하나씩 실행된 것처럼 보이게 보장. 팬텀도 완전히 방지
즉 어떤 레코드를 A트랜잭션이 읽으면, 다른 트랜잭션은 절대 못건드린다. A트랜잭션이 종료시까지 락 걸림MySQL:
SERIALIZABLE명시 필요PostgreSQL:
SERIALIZABLE명시 필요
2. 락(Lock) - 어떤 데이터(레코드)를 공유하는 방식에 관한 이야기
🧾 상황 설정
하나의 장부(테이블)에 아빠, 엄마, 아들 셋이 거래 내역을 적는다.
누가 적을 때는 다른 사람은 접근하면 안 되고, 누가 읽을 때는 겹치지만 않게 하려고 한다.
이때 등장하는 개념이 바로 락(Lock)입니다.
🟦 1. S Lock (Shared Lock, 공유 잠금)
읽기 전용이고 여러 사람이 동시에 읽을 수 있음
📖 비유: 엄마가 장부를 보고 있음. 읽기만 할 거니까 아빠도 옆에서 같이 봐도 됨. 다만, 누군가 적는 건 절대 안 됨.
여러 명이 동시에 "읽기"는 가능
"쓰기" 하려는 사람은 기다려야 함
📌 실제 DB에서: SELECT ... 시 락을 걸 수 있음 (SELECT ... LOCK IN SHARE MODE 등)
누군가 읽을 때에는 다른 누군가 읽을 수는 있는데 쓰려는 사람은 못함.
누군가 적을 때에는 누구도 읽을 수 도 쓸 수도 없음.
🟥 2. X Lock (Exclusive Lock, 배타 잠금)
쓰기 전용이고, 아무도 접근 못함
📖 비유: 아빠가 지출 항목을 적는 중. 혼자 독점적으로 장부를 쓰고 있기 때문에, 엄마와 아들은 절대 열람 금지.
쓰기할 때 반드시 필요
쓰는 동안 읽기와 쓰기 모두 막힘
📌 실제 DB에서: UPDATE, DELETE, INSERT 시 자동으로 사용됨
🟨 3. Gap Lock (틈 잠금, 간격 잠금)
행과 행 사이의 '빈 공간'에 대한 잠금
📖 비유: 엄마가 "1~100번 항목 사이에 아무도 새로운 내용을 넣지 말아줘!" 라고 선을 긋는 것
→ 다른 가족이 새로운 거래를 그 사이에 추가하려고 하면 막힘
Repeatable Read 이상의 트랜잭션 격리 수준에서
팬텀 리드 방지를 위해 존재
존재하지 않는 데이터 삽입도 막을 수 있음
📌 실제 DB에서: MySQL(InnoDB)에서 SELECT ... FOR UPDATE 같은 쿼리 시 자동으로 발생할 수 있음
✅ 요약 비교
Lock 종류 | 의미 | 동시에 가능한 작업 | 예시 상황 |
|---|---|---|---|
S Lock | 공유 잠금 | 여러 읽기 OK, 쓰기는 대기 | 여러 명이 장부 읽는 중 |
X Lock | 배타 잠금 | 아무도 접근 불가 | 누군가 장부를 수정 중 |
Gap Lock | 간격 잠금 | 중간 삽입 차단 | "1~100번 사이엔 누구도 새 항목 못 넣어!" |
3. 출금시 여러트랜잭션에서 내 잔고를 확인한 후 출금 할 때 Read Committed 와 X Lock을 활용할 텐데, 이것을 스프링데이터 JPA 에서 실제 코드
💡 시나리오
여러 사용자가 동시에 출금을 시도할 수 있다.
우리는 현재 잔고를 확인하고, 충분할 경우 금액을 차감해야 한다.
이 과정에서 동시성 문제(이중 인출 등) 를 방지해야 하며, 그 방법이 바로
X Lock을 사용하는 비관적 락이다.
🔧 사용 기술
데이터베이스 격리 수준:
READ COMMITTED(기본적으로 대부분의 RDBMS에서 지원)JPA 락:
@Lock(LockModeType.PESSIMISTIC_WRITE)→ X Lock
📦 엔티티 예제
@Entity
public class Account {
@Id
private Long id;
private String owner;
private Long balance; // 잔액: 원 단위
}
🧾 Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM Account a WHERE a.id = :id")
Optional<Account> findByIdForUpdate(@Param("id") Long id);
}여기서
@Lock(PESSIMISTIC_WRITE)는 DB에서SELECT ... FOR UPDATE로 바뀜해당 레코드는 다른 트랜잭션에서 읽거나 쓸 수 없음 (X Lock)
⚙️ 서비스 코드
@Service
public class AccountService {
private final AccountRepository accountRepository;
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void withdraw(Long accountId, Long amount) {
Account account = accountRepository.findByIdForUpdate(accountId)
.orElseThrow(() -> new RuntimeException("Account not found"));
if (account.getBalance() < amount) {
throw new IllegalArgumentException("잔액 부족");
}
account.setBalance(account.getBalance() - amount);
// 저장은 @Transactional 안에서 변경 감지로 자동 반영
}
}
📌 핵심 설명
기술 요소 | 설명 |
|---|---|
| DB 레벨의 X Lock → 다른 트랜잭션 접근 불가 |
| 트랜잭션은 커밋된 데이터만 읽음 |
| JPA dirty checking (변경 감지)로 자동 반영 |
🧪 결과
동시에 여러 출금 요청이 들어와도
SELECT FOR UPDATE때문에 순차적으로 처리됨낙관적 락이 아닌 확실한 락을 걸기 때문에 동시성 안전
성능은 살짝 손해보지만 은행 같은 상황에선 매우 안전한 방식
이번에는 위 예제를 낙관적 락 (@Version) 을 사용해서 구현해보겠습니다.
비관적 락(X Lock) 과 비교하며 볼 수 있게 구성할게요.
📌 1. 낙관적 락 (@Version)이란?
데이터 충돌이 자주 발생하지 않는다고 가정
락을 걸지 않고 먼저 값을 읽고 변경 → 트랜잭션 커밋 시점에 버전 비교로 충돌 감지
충돌 시
OptimisticLockException발생
📦 2. 엔티티 수정 – @Version 추가
@Entity
public class Account {
@Id
private Long id;
private String owner;
private Long balance;
@Version
private Long version; // 낙관적 락에 사용됨
}@Version필드가 JPA에서 내부적으로 사용됨트랜잭션 커밋 시점에
UPDATE ... WHERE id=? AND version=?조건이 걸림
🧾 3. Repository – 기본 메서드 사용 가능
public interface AccountRepository extends JpaRepository<Account, Long> {
// findById 사용해도 자동으로 version 체크
}
⚙️ 4. 서비스 코드
@Service
public class AccountService {
private final AccountRepository accountRepository;
public AccountService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@Transactional
public void withdraw(Long accountId, Long amount) {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new RuntimeException("Account not found"));
if (account.getBalance() < amount) {
throw new IllegalArgumentException("잔액 부족");
}
account.setBalance(account.getBalance() - amount);
// 커밋 시점에 version 조건으로 충돌 여부 판단
}
}
🧪 5. 결과 요약
구분 | 비관적 락 ( | 낙관적 락 ( |
|---|---|---|
락 시점 | SQL 실행 시점 (즉시) | 트랜잭션 종료 시점 |
충돌 처리 | 대기 (block) | 예외 발생 (충돌 시) |
성능 | 낮음 (락 overhead 있음) | 높음 (경쟁 적을 경우) |
동시성 강도 | 높음 | 낮음 |
사용 상황 | 금융 출금, 재고 감소 등 | 관리자 화면, 낮은 경쟁 환경 |