"콘서트 티켓이 딱 1장 남았습니다. A와 B가 동시에 '결제하기' 버튼을 눌렀습니다."
만약 아무런 안전장치가 없다면, A와 B 모두에게 결제 성공 메시지가 뜨고 티켓은 -1장이 되는 "갱신 손실(Lost Update)" 문제가 발생합니다. 이를 막기 위해 **"누가, 언제, 어떻게 데이터를 점유할 것인가"**를 결정하는 두 가지 상반된 패턴이 있습니다.
1. 방식 1: 비관적 락 (Pessimistic Lock) - "독점적 잠금"
이름 그대로 세상을 비관적으로 바라봅니다. "분명히 내가 수정하는 동안 남들도 건드릴 거야"라고 가정하고, 아예 트랜잭션이 시작될 때 DB 줄(Row) 자체에 자물쇠를 채워버리는 방식입니다.
특징
- DB 기능 이용: SELECT ... FOR UPDATE 구문을 사용하여 물리적인 락을 겁니다.
- 무결성 최강: 락을 획득하지 못한 다른 트랜잭션은 대기(Block) 상태가 됩니다.
코드 예시 (JPA)
Java
public interface TicketRepository extends JpaRepository<Ticket, Long> {
// 1. 비관적 락 걸기 (쓰기 잠금)
// 이 쿼리가 실행되는 순간, 커밋될 때까지 다른 누구도 이 row를 건들지 못함
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select t from Ticket t where t.id = :id")
Optional<Ticket> findByIdWithLock(Long id);
}
@Service
@Transactional
public class TicketService {
public void buyTicket(Long ticketId) {
// 락 획득 (대기 발생 가능)
Ticket ticket = ticketRepository.findByIdWithLock(ticketId)
.orElseThrow();
if (ticket.getStock() > 0) {
ticket.decreaseStock();
}
}
}
장단점
- 장점: 데이터 충돌을 원천 차단합니다. 데이터 정합성이 생명인 금융권 로직에서 선호합니다.
- 단점:
- 성능 저하: 락을 기다리는 대기 시간이 길어집니다.
- 데드락(Deadlock): 서로 락을 잡고 놔주지 않는 교착 상태에 빠질 위험이 큽니다.
2. 방식 2: 낙관적 락 (Optimistic Lock) - "버전 관리"
세상을 낙관적으로 바라봅니다. "설마 동시에 수정하겠어? 충돌 나면 그때 가서 해결하지 뭐."라고 생각하며 DB 락을 걸지 않습니다. 대신 버전(Version) 정보를 이용해 논리적으로 충돌을 감지합니다.
특징
- 애플리케이션 레벨: DB 락 기능을 쓰지 않고, 엔티티에 version 컬럼을 추가하여 관리합니다.
- CAS (Compare And Set): "내가 읽었을 때 버전이 1이었는데, 지금 수정하려고 보니 여전히 1이니? 그럼 2로 바꾸고 수정해. 아니면 에러 내." 라는 식입니다.
코드 예시 (JPA)
Java
// 1. 엔티티에 버전 필드 추가
@Entity
public class Ticket {
@Id @GeneratedValue
private Long id;
private int stock;
@Version // 낙관적 락의 핵심!
private Long version;
}
@Service
@Transactional
public class TicketService {
public void buyTicket(Long ticketId) {
try {
// 락 없이 그냥 조회 (빠름)
Ticket ticket = ticketRepository.findById(ticketId).orElseThrow();
ticket.decreaseStock();
// 트랜잭션 커밋 시점에 버전 체크 발생!
// UPDATE ticket SET stock=stock-1, version=2 WHERE id=1 AND version=1
} catch (ObjectOptimisticLockingFailureException e) {
// 충돌 발생 시 개발자가 직접 재시도 로직을 짜야 함
retryBuyTicket(ticketId);
}
}
}
장단점
- 장점: DB에 락을 걸지 않으므로 동시 처리 성능(Throughput)이 매우 좋습니다.
- 단점:
- 충돌 처리 비용: 충돌이 났을 때(예외 발생 시), 재시도(Retry) 로직을 개발자가 직접 구현해야 합니다. (while 루프 등)
- 충돌이 잦은 환경에서는 재시도가 계속 일어나 오히려 성능이 떨어집니다.
3. 실무 비교: 언제 무엇을 쓰는가?
| 구분 | Pessimistic Lock (비관적) | Optimistic Lock (낙관적) |
| 제어 주체 | Database (레코드 잠금) | Application (버전 체크) |
| 성능 | 락 대기로 인한 저하 발생 | 락이 없어 빠름 (충돌 없을 시) |
| 충돌 발생 시 | 대기 후 순차 처리 | 예외 발생 (재시도 필요) |
| 데드락 위험 | 있음 (설계 주의 필요) | 없음 |
| 추천 상황 | 충돌이 빈번한 경우 (선착순 이벤트, 돈 계산) | 충돌이 거의 없는 경우 (게시글 수정, 일반 주문) |
결론
"데이터가 틀어지면 절대 안 되고, 충돌이 자주 일어난다"면 성능을 좀 포기하더라도 **비관적 락(Pessimistic)**을 써야 합니다. (예: 은행 계좌 이체)
반면 "대부분의 경우 충돌이 안 나는데, 가끔 나는 충돌만 잡으면 된다"면 **낙관적 락(Optimistic)**을 사용하여 성능을 확보하는 것이 현대적인 백엔드 설계의 정석입니다.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 페이지네이션 패턴(Pagination Pattern) 완벽 정리 (0) | 2025.12.07 |
|---|---|
| 콜백 패턴(Callback Pattern) 총정리: 동기(Blocking) vs 비동기(Non-blocking) (0) | 2025.12.07 |
| 의존성 주입(DI) 패턴 완벽 정리 (0) | 2025.12.07 |
| 널 객체 패턴(Null Object Pattern) 총정리 (0) | 2025.12.07 |
| 인터프리터 패턴(Interpreter Pattern) 완벽 정리 (0) | 2025.12.07 |