비관적 락 구현 방식 문의 건
1. 현재 학습 진도
몇 챕터/몇 강을 수강 중이신가요?
여기까지 이해하신 내용은 무엇인가요?
강사님 안녕하세요 :)
현재 4-9 낙관적 락, 비관적 락 강의를 수강하는 중이며,
단일 행에 대해 배타 락을 거는 방식을 학습한 후 관련 문의 드립니다.
2. 어려움을 겪는 부분
어느 부분에서 막히셨나요?
코드의 어떤 로직이 이해가 안 되시나요?
어떤 개념이 헷갈리시나요?
만약비관적 락(PESSIMISTIC_WRITE) 방식으로 한 테이블의 여러 행에 락을 걸어야 한다면,
아래 두 방식 중 어느 방식이 적절한지 궁금합니다.
N번의 DB 조회를 방지하기 위해 아래 1번 방식을 고려했으나,관련 케이스가 많지 않아 제가 놓치는 부분이 있는지 여쭤보고 싶습니다.
더불어 실무에서는 어떤 방식으로 접근하시는지도 궁금합니다. :)
3. 시도해보신 내용
문제 해결을 위해 어떤 시도를 해보셨나요?
에러가 발생했다면 어떤 에러인가요?
현재 작성하신 코드를 공유해주세요
1. IN절 조회 후 락 걸기@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from Product p where p.productNumber in :productNumbers")
List<Product> findAllByProductNumberIn(@Param("productNumbers") List<String> productNumbers);만약 1번 방식이 적절하다면, 데드락 방지를 위해 ORDER BY 정렬이 필수인지 궁금합니다.
2. 루프를 돌며 조회 후 락 걸기
for (String productNumber : productNumbers) { Product product = productRepository.findByProductNumberWithLock(productNumber) .orElseThrow( ··· ); ··· }
@Lock(LockModeType.PESSIMISTIC_WRITE)@Query("SELECT p FROM Product p WHERE p.productNumber = :productNumber")
Optional<Product> findByProductNumberWithLock(@Param("productNumber") String productNumber);
이렇게 구체적으로 알려주시면, 더 정확하고 도움이 되는 답변을 드릴 수 있습니다!
Câu trả lời 2
0
안녕하세요 shsf 님!! 좋은 질문 해주셔서 감사합니다.
여러 행에 락을 걸어야 할 때 두 방식을 모두 비교해주신 것 자체가 동시성 관점에서 제대로 접근하고 계신다는 신호입니다. 결론부터 말씀드리면 1번(IN절) 방식이 맞고, ORDER BY 정렬(혹은 입력 리스트 사전 정렬)은 실무에서 반드시 챙겨야 합니다. 하나씩 풀어드리겠습니다.
두 방식의 본질적 차이는 "트랜잭션 길이"입니다
2번 루프 방식은 N개의 상품을 처리하려면 N번의 SELECT FOR UPDATE가 발생합니다. 네트워크 RTT, 커넥션 점유, 트랜잭션 홀딩 시간이 모두 N배가 되는 구조입니다. 비관적 락의 가장 큰 단점이 "락 보유 시간이 길어질수록 동시성이 떨어진다"는 것인데, 2번 방식은 이 단점을 정확히 악화시킵니다.
수치로 감을 잡아보면, 한 쿼리가 20ms라고 가정할 때 상품 10개를 처리하는 루프 방식은 최소 200ms 동안 10개의 행에 대한 락을 잡고 있게 됩니다. IN절 방식은 한 번의 쿼리로 같은 작업을 20~30ms 안에 끝낼 수 있고요. 이 차이가 피크 시간대 TPS와 P99 응답시간을 크게 가릅니다.
IN절 방식에서 ORDER BY는 데드락 방지에 반드시 필요합니다
질문해주신 대로, IN절에서도 ORDER BY는 매우 중요합니다. 이유는 MySQL InnoDB가 락을 획득하는 순서가 "IN절에 나열한 순서"가 아니라 "실행 계획에 따라 레코드를 스캔하는 순서"이기 때문입니다.
상황을 가정해보겠습니다. 트랜잭션 A는 productNumbers = ["P-100", "P-200"]로 요청하고, 트랜잭션 B는 ["P-200", "P-100"]으로 요청한다고 해보죠. ORDER BY가 없으면 옵티마이저 판단에 따라 A는 P-100을 먼저 잠그고, B는 P-200을 먼저 잠글 수 있습니다. 서로 다른 상품을 먼저 잠그는 순간 강의 36번 차시에서 다뤘던 교차 락 대기 상황이 그대로 재현됩니다.
해결 방법은 두 가지고, 둘 다 쓰셔도 됩니다. 쿼리에 order by p.productNumber를 추가하거나, 애플리케이션 레벨에서 Collections.sort(productNumbers)로 입력 자체를 정렬한 뒤 IN절에 넘기는 방식입니다.
개인적으로는 애플리케이션 레벨 정렬을 더 권장합니다. 쿼리 레벨 ORDER BY는 옵티마이저가 파일소트를 유발할 여지가 있고, 결국 락 획득은 인덱스 스캔 순서를 따르기 때문입니다. 입력 자체를 정렬해서 넘기면 어떤 실행 계획이 나오든 "모든 트랜잭션이 productNumber 오름차순으로 락을 획득한다"는 불변 조건이 확보됩니다. MySQL 공식 문서의 데드락 방지 가이드에서도 "트랜잭션마다 동일한 순서로 리소스를 획득하라"를 1순위 원칙으로 제시하고 있습니다.
실무에서의 현실적인 접근
실제 프로덕션에서는 보통 이런 순서로 고민합니다. 가장 먼저 "정말 비관적 락이 필요한가?"를 묻습니다. 여러 상품에 동시 수정이 자주 발생하지 않는 도메인이라면 낙관적 락(@Version)이나 조건부 UPDATE(update ... where version = ?)로도 충분한 경우가 많습니다. 여러 행에 비관적 락을 거는 건 락 범위가 넓어져서 전체 처리량에 영향이 크기 때문입니다.
비관적 락이 필요하다고 판단되면 IN절 + 입력 리스트 정렬이 기본 패턴이 됩니다. 여기에 SKIP LOCKED나 NOWAIT 옵션을 상황에 따라 조합하기도 합니다. 예를 들어 재고 차감 배치에서 이미 잠긴 행은 건너뛰고 다음 후보를 잡아야 할 때 SKIP LOCKED가 유용합니다. Spring Data JPA의 @Lock 레퍼런스에서 관련 옵션들을 확인하실 수 있습니다.
마지막으로 락 타임아웃과 재시도 전략을 반드시 함께 설계합니다. innodb_lock_wait_timeout 기본값 50초는 사용자 API 기준으로는 너무 긴 시간이라 도메인에 맞게 조정하고, 데드락 감지 시(SQLState 40001) Spring Retry 같은 도구로 재시도 로직을 두는 식으로 구성합니다.
고민해주신 내용 자체가 좋은 이력서 소재입니다. 단순히 "비관적 락을 적용했다"가 아니라 구체적인 서사로 풀어주세요. 예시 문장은 이렇게 써볼 수 있습니다.
"다중 상품 동시 주문 시 루프 기반 SELECT FOR UPDATE로 인해 트랜잭션당 평균 180ms 락 보유 발생 → IN절 단일 쿼리와 입력 리스트 정렬 기반 데드락 방지 전략 적용 → 트랜잭션 평균 시간 25ms로 86% 단축, 동시 요청 100건 기준 데드락 발생 0건 / TPS 약 4배 개선"
실제 프로젝트에서 이 패턴을 적용해보시고, 루프 방식 vs IN절 방식의 실측 차이를 K6로 수치화해서 이력서에 써보세요. 그 수치가 여러분의 경험을 증명하는 가장 확실한 근거가 될 거에요!! 빠이팅입니다
0
안녕하세요, 인프런 AI 인턴입니다. shsf님.
비관적 락을 구현하는 방식에 대해 고민하고 계신 질문에 대해 답변 드리겠습니다. 비관적 락(PESSIMISTIC_WRITE)을 사용할 때, 특히 여러 데이터 행에 대해서 락을 걸어야 할 경우의 접근 방식에 대해서 설명드립니다.
1. IN절 조회 후 락 걸기
이 방법은 현명한 선택일 수 있습니다. 데이터베이스 쿼리에 IN 절을 사용하여 여러 행을 한꺼번에 락을 걸 수 있어, 여러 번의 DB 접근을 최소화할 수 있습니다. 주의할 점은 데드락 방지를 위해 ORDER BY 절을 추가하여 정렬된 방식으로 데이터를 처리함으로써 충돌 가능성을 줄일 수 있습니다. 데이터가 있는 테이블의 성능에 따라 ORDER BY가 성능에 영향을 줄 수 있으므로 이 점을 고려하면 좋습니다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from Product p where p.productNumber in :productNumbers order by p.productNumber")
List<Product> findAllByProductNumberIn(@Param("productNumbers") List<String> productNumbers);
2. 루프를 돌며 조회 후 락 걸기
이 방식은 각 개별 행을 락으로 보호하므로, 세밀하게 제어할 수 있습니다. 특정 행에서 문제가 생길 경우에만 반복 흐름을 제어할 수 있게 됩니다. 하지만 각각의 조회가 트랜잭션으로 관리되어야 하고, 트랜잭션 경계 간의 적절한 관리가 필요합니다. 특히 다수의 트랜잭션을 생성할 경우, 자원 소모가 클 수 있어 성능 비용이 증가할 수 있습니다.
for (String productNumber : productNumbers) {
Product product = productRepository.findByProductNumberWithLock(productNumber)
.orElseThrow(() -> new RuntimeException("Product not found"));
// 추가 로직
}
실무에서의 접근 방법
대규모 데이터베이스 환경에서는 IN절을 활용한 방법으로 가능한 한 서버 쪽의 거래 비용을 줄이는 방법을 많이 사용합니다. 동시에 행을 선택하고 트랜잭션을 관리하기 때문에 성능상의 이점이 있습니다. 그러나, 데이터 동시 액세스가 매우 빠르게 일어나는 환경에서는 오히려 트랜잭션보다 낙관적 락이나 다른 동시성 제어 메커니즘을 고려하기도 합니다.
관련하여 추가적인 상황이나 테스트 환경에서 어떻게 구현되는지 이해하기 위해 몇 가지 실제 사례나 질문에 대해서 더 알아보고 싶다면, 유사한 비관적 락 구현 사례와 실무 적용 방법에 대한 자료를 참조하여 보시길 추천드립니다.
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.
Build 관련 문제 (테스트 관련 문제)
0
30
1
인덱스 관련 질문 있습니다.
0
51
1
외부 api 처리 방안에 대하여 궁금한 점이 있습니다.
0
87
2
네임드 락 사용 시 커넥션 풀을 분리하는 방법에 대한 질문
0
78
2
이벤트) 백엔드 기술면접 실전문제집
0
99
2
로컬에서 테스트 한 결과를 이력서에 써도 괜찮을까요?
0
127
2
데드락 발생 시, 외래 키를 제거하는 방법 관련 질문
0
82
2
Redis 캐싱을 도입하는데 db조회와 성능이 차이가 거의 없습니다.
0
107
2
k6 부하테스트 중인데 개선 전 성능이 너무 안나와서 고민
0
115
2
강의와 성능수치 비교
0
100
2
13강 강의 뒷부분의 과제 안내부분은 어디있나요?
0
49
2
이벤트 참가자 수 증가 후, save 메서드 호출 코드 질문
0
64
2
[수업 자료 질문] Cache Aside의 특징 문의
0
82
2
[수업자료 문의] RedisTemplate으로 SETNX 시 리턴값 문의
0
79
2
블로그에 학습 내용 정리 포스트를 올려도괜찮나요?
0
107
2
멀티스레드 상황인데 currentParticipants 가 AtomicInteger가 아닌 이유?
0
86
3
클라우드 환경 배포시 부하 테스트 방식에 대하여
0
133
2
k6 dashboad 안나오는 상
0
111
2
2-4 도커 빌드 에러가 계속 납니다.
0
295
2
AWS EC2에 도커 컨테이너가 동작하지 않을 때 확인 해야하는 것
0
112
2
성능 측정시
0
104
2
API 별 실행 쿼리 모니터링 구현 질문 있습니다.
0
79
2
이력서 작성에 대한 질문
0
105
2
트랜잭션 격리성 설계도 어필포인트로 가져갈 수 있을까요?
1
63
2

