[JPA] 비관적락을 사용해 동시성 문제 해결하기 (curl command로 동시요청)
✅ 선행 개념
먼저 비관적 락이 무엇인지에 대한 개념은 아래 글에 적어놓았다.
https://jie0025.tistory.com/603
✅ 동시에 API 요청 넣어보기
락을 사용하지 않았을때 발생하는 문제를 직접 눈으로 확인해보자.
동시 요청을 보내는 가장 간단한 방법이 있다.
⏺ curl 커맨드 ?
CLI를 이용해 API 데이터를 요청할 수 있는,
HTTP 클라이언트 도구 중 하나
* GET방식
옵션 없이 아래처럼 사용하면 된다.
curl [원하는 요청 URL/URI]
$ curl https://jsonplaceholder.typicode.com/posts/1
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}%
* POST / DELETE , 다른 방식을 사용해야할 땐
-X, --request 옵션을 사용한다.
$ curl https://jsonplaceholder.typicode.com/posts/1 -X DELETE
{}%
* Body, Header 설정
$ curl https://jsonplaceholder.typicode.com/posts \
-d '{"title": "foo", "body": "bar", "userId": 1}' \
-H 'Content-type: application/json; charset=UTF-8'
{
"title": "foo",
"body": "bar",
"userId": 1,
"id": 101
}%
💻 Terminal
아래 형태로 명령어를 입력하면 된다. (완전 동시를 보장하지는 않지만, 동시에 여러 API 를 서버에 보내는것과 유사한 효과가 발생한다.)
😆 Example
콘서트 좌석 <2번> 에
3명의 유저<2,5,6>가 동시 예약 요청
curl -X POST 'http://localhost:8080/seats/reservations' -H 'Content-Type: application/json' -d '{"user": 2, "concertSeat": 2}' &
curl -X POST 'http://localhost:8080/seats/reservations' -H 'Content-Type: application/json' -d '{"user": 5, "concertSeat": 2}' &
curl -X POST 'http://localhost:8080/seats/reservations' -H 'Content-Type: application/json' -d '{"user": 6, "concertSeat": 2}' &
# ... 이런 식으로 계속 지정 가능
테스트 Response 응답
MYSQL까지 직접 확인해보자
절대 일어나면 안되는 일이 발생하는걸 볼 수 있다.
이런 이유로 우리는 비관적 락을 사용해서 동시성 제어를 해줘야한다.
✅ JPA와 Lock 사용하기
JPA에선 @Lock 어노테이션을 이용하면 간단하게 락을 사용할 수 있다.
✔️ Lock의 적용 범위
- EntityManager.lock() , EntityManager.find(), EntityManager.refresh()
- Query.setLockMode()
- @NamedQuery
✔️ JPA - Lock 옵션
설명 | 타입 | 설명 |
낙관적 락(Optimisstic Lock) | OPTIMISTIC | 낙관적 Lock 사용 |
낙관적 락(Optimisstic Lock) | OPTIMISTIC_FORCE_INCREMENT | 낙관적 Lock + 버전 정보 강제 증가 |
비관적 락(Pessimistic Lock) | PESSIMISTIC_READ | 비관적 Lock, 읽기 Lock 사용 |
비관적 락(Pessimistic Lock) | PESSIMISTIC_WRITE | 비관적 Lock, 쓰기 Lock 사용 |
비관적 락(Pessimistic Lock) | PESSIMISTIC_FORCE_INCREMENT | 비관적 Lock + 버전 정보 강제 증가 |
기타 | NONE | 엔티티에 @Version이 있으면 낙관적 Lock을 적용함 |
기타 | READ | 하위 호환을 위한 것으로 OPTIMISTIC와 같음 |
기타 | WRITE | 하위 호환을 위한 것으로 OPTIMISTIC_FORCE_INVREMENT와 같음 |
콘서트 좌석 요청에 대한 것이기 때문에
비관적 락 - PESSIMISTIC_WRITE 타입을 사용해보면 될것 같다.
( *도서 - 자바 ORM 표준 JPA 프로그래밍 )
✅ JPA Repository 인터페이스를 사용하는 경우
Repository에 정의된 메서드 위에 @Lock 어노테이션을 쓰면 된다.
✍️ Java
나는 QueryDsl 사용 경우에 적용을 해서
다른 곳에서 예제를 가져왔다.
https://sabarada.tistory.com/187
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryV1Custom {
@Lock(LockModeType.PESSIMISTIC_READ) // Lock 적용 (LockModeType을 이용해 옵션 변경)
Optional<User> findWithPessimisticLockById(Long id);
}
✍️ kotlin
위 자바코드를 참고해서 만든 코틀린 예제!
@Repository
interface SeatRepository : JpaRepository<Seat, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
fun findBySeatNumber(number: Int): Seat?
}
✅ QueryDSL을 사용하는 경우
setLockMode() 메서드를 사용하면 된다.
override fun findByNumber(number: Long?): myTable? {
return jpaQueryFactory
.selectFrom(qUser)
.where(조건문)
.setLockMode(LockModeType.PESSIMISTIC_WRITE) // 비관적 Lock 적용
.fetchOne()
}
✅ 동시 요청 테스트 결과
락이 잘 걸렸다면, 아래처럼 결과가 출력되는 것을 확인할 수 있다!
✔️ DB에서 확인해봐도 데이터는 1개만 들어갔다
References