이전 40단계에서는 거대한 트래픽이 몰려올 때 Redis 인프라를 활용하여 재고 초과 판매(동시성) 문제를 막아내는 '분산 락'을 배웠습니다. 하지만 Redis는 구축하고 유지보수하기 까다로운 외부 인프라입니다. 만약 "우리 회사는 그 정도로 트래픽이 몰리지 않는데 굳이 Redis까지 깔아야 하나요? 그냥 JPA랑 MySQL 기능만으로 동시성을 막을 수는 없나요?"라고 묻는다면 정답은 "가능하다!"입니다. 실무에서는 동시성 문제가 터질 확률(충돌 빈도)에 따라, 애플리케이션 레벨에서 가볍게 막아내는 낙관적 락(Optimistic Lock)과 DB 레벨에서 아예 진짜 자물쇠를 걸어버리는 비관적 락(Pessimistic Lock)을 골라서 사용합니다.
이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "JPA 동시성 제어의 두 가지 철학!" 낙관적 락과 비관적 락의 작동 원리와 실무 적용 시나리오에 대하여 다뤄보겠습니다.

🕊️ 1. 낙관적 락 (Optimistic Lock): "설마 충돌 나겠어?"
낙관적 락은 이름 그대로 세상(트래픽)을 긍정적으로 바라봅니다. "동시에 들어올 확률은 거의 없으니 평소엔 자물쇠 없이 빠르게 넘어가고, 만약 진짜 우연히 동시에 들어와서 충돌이 나면 그때 에러를 뱉어버리자!"라는 마인드입니다. 이는 Version(버전)이라는 정수 필드를 통해 마법처럼 동작합니다.

// Entity 클래스
@Entity
public class Item {
@Id @GeneratedValue
private Long id;
private int stock;
@Version // 🌟 이것 하나만 붙이면 스프링 데이터 JPA가 알아서 낙관적 락을 켭니다!
private Long version;
}
작동 원리: 철수와 영희가 동시에 재고 1개(version=1)짜리 상품을 조회했습니다. 철수가 먼저 0개로 수정(UPDATE)하면서 version을 2로 올립니다. 간발의 차이로 영희가 0개로 수정하려 UPDATE Item SET stock=0, version=2 WHERE id=1 AND version=1 쿼리를 날립니다. 하지만 DB에는 이미 철수가 버전을 2로 올려두었기 때문에, WHERE 조건에 맞는 version=1인 데이터가 없어서 업데이트가 0건 처리됩니다! 이때 JPA는 OptimisticLockException 예외를 빵 터뜨리며 영희의 결제를 튕겨냅니다.
⛓️ 2. 비관적 락 (Pessimistic Lock): "아무도 믿을 수 없어!"
반대로 비관적 락은 세상을 불신합니다. "무조건 동시에 들어와서 꼬일 테니, 내가 조회(SELECT)하는 순간 무조건 다른 놈들은 건드리지도 못하게 DB Row에 진짜 자물쇠를 걸어버릴 거야!"라는 마인드입니다.
// Repository 인터페이스
public interface ItemRepository extends JpaRepository<Item, Long> {
// 🌟 PESSIMISTIC_WRITE 모드를 주면, 실제 쿼리가 "SELECT ... FOR UPDATE" 로 날아갑니다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT i FROM Item i WHERE i.id = :id")
Optional<Item> findByIdWithPessimisticLock(Long id);
}
작동 원리: 철수가 먼저 findByIdWithPessimisticLock()을 호출하면, DB(MySQL)는 해당 데이터 줄(Row)에 베타 락(X-Lock)을 겁니다. 0.001초 뒤 영희가 똑같은 메서드를 호출하면, 영희의 쓰레드는 DB 락이 풀릴 때까지 DB 단에서 무한정 대기(Blocking)하게 됩니다. 철수가 재고 차감을 끝내고 트랜잭션(Commit)을 닫아야 비로소 영희가 깨어나서 남은 재고(0개)를 보고 얌전히 "품절" 에러를 내뱉습니다.
⚖️ 3. 실무에서는 무엇을 써야 할까?
- 게시글 조회수 증가, 내 정보 수정: 충돌 빈도가 매우 낮습니다. 이런 곳에 비관적 락을 걸면 성능만 뚝 떨어집니다. 낙관적 락(@Version)을 걸고, 충돌 시 자연스럽게 "다시 시도해 주세요" 알람을 띄우면 됩니다.
- 선착순 티켓팅, 쿠폰 발급, 금융 송금: 무조건 충돌이 발생합니다. 낙관적 락을 쓰면 1만 명 중 1명만 성공하고 9,999명에게
Exception이 터져버립니다. 비관적 락(PESSIMISTIC_WRITE)을 사용하여 무식하더라도 확실하게 줄을 세우거나, 성능이 중요하다면 40단계에서 배운 Redis 분산 락을 도입해야 합니다.
🎯 4. 마무리 및 다음 단계
지금까지 트래픽 양상과 충돌 빈도에 따라 적절한 무기를 꺼내 들어 완벽하게 데이터 정합성을 지켜내는 시니어급 JPA 동시성 제어, 낙관적 락과 비관적 락의 실무 비교 가이드에 대해 상세히 다루어 보았습니다. 이제 여러분은 무작정 Redis부터 깔지 않고 "이게 정말 분산 락이 필요한 규모인가?"를 아키텍트의 시선으로 판단할 수 있습니다.
백엔드 서버의 안정성은 완성되었습니다. 이제 프론트엔드 개발자(React/iOS)와의 협업 이야기로 넘어가 보겠습니다. 프론트엔드가 백엔드 API를 호출했을 때, "상태: 결제완료"라는 JSON 데이터를 받았습니다. 그런데 프론트엔드가 "결제가 완료됐으면 그다음에 '배송조회' API를 호출해야 하나? 아니면 '환불' API를 호출해야 하나?"라며 API 문서를 뒤적거립니다. "만약 백엔드가 응답 JSON 안에 다음에 클라이언트가 할 수 있는 행동(Link)을 몽땅 다 적어서 던져주면 어떨까?" 이어지는 42단계 포스팅에서는 "REST API 성숙도 모델의 최고봉(Level 3)!" 스스로 다음 행동을 지시하는 마법의 아키텍처, Spring HATEOAS 완벽 가이드에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| GraphQL (그래프QL): REST API의 한계를 부수고 오버패칭을 끝내다 (0) | 2026.08.05 |
|---|---|
| [Spring Boot] Spring HATEOAS: 진정한 REST API (Level 3) 달성기 (0) | 2026.08.05 |
| [Spring Boot] Redis 분산 락(Distributed Lock): 분산 환경의 동시성 제어 (0) | 2026.08.05 |
| [Spring Boot] Resilience4j 서킷 브레이커: 장애의 도미노를 끊어라 (0) | 2026.08.05 |
| [Spring Boot] Eureka 서비스 디스커버리 (0) | 2026.08.05 |