티스토리 뷰

 

 

상품 주문하기를 개발하던 중, 

단 1초의 오차도 없이 동시다발적으로 상품을 주문한다고 가정할 때 문제가 발생한다. 

 

일단 동시적으로 값을 보내주기 위해 테스트를 만들어보자.

 

#상황
원래는 로그인 환경에서 해야하지만, 그러면 테스트가 더 복잡해지니 시큐리티에서 로그인 안해도 되도록 설정을 바꿔준 후 로컬에서 테스트를 진행했다. 

 

 

포스트 맨에서 Body를 작성한 후에, 옆에 </> 누르면 자동으로 curl 코드를 만들어준다. 그대로 복사. 

 

 

 

 

메모장에 여러번 복붙해서 테스트 케이스를 만들자, 여러개 붙일 때에는 & 를 붙여준다. 

그대로 전부 복사해서 터미널에 실행시키면 끝

 

 

 

 

📌  문제 상황

 

 

 

요청을 7개를 보냈으나, 응답이 7개는 정상적으로 온다.
그렇지만, DB에는 1개밖에 처리되지 않은 상태.
어찌보면 예외 발생보다 더 최악인 상황이다

 

 

💡 그래서 주문할 때 주문을 처리하는동안 락 처리를 해주어, 다른 주문들이 접근을 못하도록 막는 것이다. 

 


 

이런 상황을 대비해 스프링에서는 @Lock 이라는 어노테이션을 제공한다. 

 

🌟 Lock 이란?

Lock이란 트랜잭션 처리의 순차성을 보장하기 위한 방법.
공유 락 (Shared Lock) : 데이터를 읽을 때 사용되어지는 Lock으로, 공유 Lock은 공유 Lock끼리는 동시에 접근이 가능함. 
즉 하나의 데이터를 읽는 것은 여러 사용자가 동시에 가능함. 
베타 락 (Exclusive Lock) : 데이터를 변경하고자 할 때 사용되며, 트랜잭션이 완료될 때까지 유지된다. 
베타 락이 해제될 때까지 다른 트랜잭션은 해당 리소스에 접근이 불가능. 

 

 

🌟 JPA 에서 제공하는 @Lock 어노테이션 종류

📌 비관적 락(Pessimistic Lock)
트랜잭션 충돌이 발생한다고 가정하고 락을 우선 락을 걸고보는 방식.
데이터베이스에서 제공하는 락 기능을 사용.
데이터를 수정시, 즉시 트랜잭션 충돌을 감지할 수 있다.

📌 낙관적 락(Optimistic Lock)
트랜잭션의 충돌이 발생하지 않는다고 가정하에 진행하는 방식.
데이터베이스에서 제공하는 Lock을 사용하지 않고 JPA가 제공하는 버전 관리 기능을 사용.
트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다는 단점이 있음.
출처: [인생을 코딩하다.:티스토리]

 

 

비관적 락을 사용했다. 낙관적 락은 트래내잭션의 충돌이 발생하지 않는다고 가정하에 진행하기 때문에, 

다소 위험 부담이 있을 수 있다고 생각했다. 실제로 충돌이 나면 개발자가 일일이 롤백을 해주어야한다 << 오바라고 생각했다. 

그래서 비관적 락을 사용하여, 충돌이 발생하지 않더라도 일단 락을 걸고보는 방식을 택했다. 

 

 

🌟 비관적 락의 종류 

PESSIMISTIC_READ [공유 락]
해당 리소스에 공유락을 걸어 데이터를 읽는 것은 여러 사용자가 가능하게 한다.
단, 타 트랜잭션에서 쓰기는 불가능하다. 

PESSIMISTIC_WRITE [배타 락]
해당 리소스에 베타락을 걸어, 타 트랜잭션에서는 읽기와 쓰기 모두 불가능하게 한다. 

 

 

 

처음에는 읽기는 가능하도록 PESSIMISTIC_READ [공유 락] 처리를 해주었다. 

 

 

📌 데드락의 발생

 

 

처음이랑 상황이 똑같이 데이터베이스에서는 요청이 7개인 것에 비해 count 가 1개만 감소됐다. 

그리고 데드락이 발생했다. 

 

 

🧐 데드락 (DeadLock), 즉 교착 상태란?

데드락(교착상태)란 두개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태를 가리킨다. 한정된 자원을 여러 곳에서 사용하려고 할 때 발생할 수 있다.

 

이미지 출처 : https://hckcksrl.medium.com/deadlock-%EC%9D%B4%EB%9E%80-8100261a66c3

 

 

1. Process 1 -> Resource 1 사용 
2. Process 2 -> Resource 2 사용
3. Process 1 -> Resource 2 요청 
4. Process 2 -> Resource 1 요청

 

3, 4번 에서 서로의 작업이 끝나기를 기다리기 때문에 교착 상태에 빠지게 된다. 

 

 

나의 주문 처리 로직 중 일부는 다음과 같다. 

//1. 일단 해당 날짜에 주문이 가능한지 확인. -> 안되면? 예외 발생
// Lock 거는 시점 
List<ProductInfoPerNight>[] allProductInfoList = isPossibleToOrder(orderRequest);

//2. 모든 주문이 유효하다면, productInfoPerNight -> 해당 날짜에 관한것들 전부 count - 1
processOrder(allProductInfoList);

 

1번 에서 PESSIMISTIC_READ [공유 락]을 걸게된다. 그렇게 되면 먼저 접근란 프로세스 1은 ProductInfoPerNight 테이블의 리소스에 쓰기 권한을 갖게 된다. 2번 프로세스는 같은 데이터를 원하므로, ProductInfoPerNight 테이블의 리소스에 대해 읽을 순 있으나 쓰기 권한은 없게 된다. 

2번 프로세스가 읽기만 했으므로 2번 작업에 먼저 도착한다. 하지만 2번 작업은 (count - 1)이라는 update 문, 즉 쓰기 작업을 요청하므로, 2번 프로세스는 대기하게 된다. 

3번 프로세스도 다음과 같은 과정을 하고, 4번도 하고, 5번도 한다. 

1번 프로세스는 모든 작업이 성공적으로 마칠 수 있지만, 그 다음 요청부터는 .. 모르겠다. 나중에 디버깅 해야겠다. 

아무튼 생각만 해도 에러가 안나는게 이상한 상황이다. 

 

 

 

📌 PESSIMISTIC_WRITE [배타 락] 의 사용

 

 

1번 프로세스가 ProductInfoPerNight 테이블의 리소스에 대해 읽기, 쓰기를 모두 Lock을 걸면서, 

2번, 3번, .. n번 프로세스가 순차적으로 ProductInfoPerNight 테이블의 리소스에 접근하도록 했다. 

 

결과는 성공적. 병렬이어도 미세한 차이로 늦게 나간 프로세스들은 상품을 못 획득한다.

마치 내가 티켓팅에서 분명 좌석을 눌렀는데 결제가 안 된 이유가.. 이거구나를 알았다. ㅋ ㅋ  ㅠㅠ 

 

 

데이터 베이스에서도 다음과 같이 정상적으로 나간다.

 

 

 

 

📌 근데 문제가 있다.

 

 

@Lock을 걸어준 메소드에는 사용하는 곳이 2곳이다.

- 주문 처리 (위에서 처리한 곳)

- 단순 조회용 

주문 처리에서는 @Lock이 필요하지만, 밑의 경우에는 필요가 없다. 

그리고 비관적 락을 걸어줌으로써, 오히려 성능이 느려지는 상황이 발생 🚨

오버 로딩도 안 되는 상황 

 

 

 

다음과 같이 @Query를 사용해서 분리해주었다.

단순 읽기할 때에는 @Lock을 걸어주지 않았다.  

끝. 

 

 

 

 

출처 

 

[SPRING, DATABASE] 상품 주문 시, Pessimistic Lock으로 동시성 제어

안녕하세요. 오늘은 Pessimistic Lock으로 동시성 제어를 하는 법에 관해 글을 작성해보려 합니다. 저는 상품을 주문하는 API를 개발하고 있었습니다. 여러 사용자가 동시다발적으로 상품을 주문 할

junghyungil.tistory.com

 

 

[데이터베이스] Lock에 대해서 알아보자 - 기본편

안녕하세요. 오늘은 DataBase의 Lock에 대해서 알아보고 정리해보는 시간을 가져보도록 하겠습니다. DataBase는 데이터를 영속적으로 저장하고 있는 시스템입니다. 이런 시스템은 같은 자원(데이터)

sabarada.tistory.com

 

 

상품 주문 동시성 문제 해결하기 - DeadLock, 낙관적 락(Optimistic Lock) & 비관적 락(Pessimistic Lock)

여러명의 사용자가 동시에 상품의 주문을 요청했을때 발생하는 동시성 문제의 해결과정

velog.io

 

 

DeadLock 이란

DeadLock

hckcksrl.medium.com