이동봉사 공고마다 하나의 신청만을 허용하는 프로젝트에서,
여러 요청이 동시에 들어왔을 때 처음 들어온 요청만 허용하도록 하기 위한 과정을 거쳤다!
여러 가지 해결책 중 프로젝트에 가장 적합하다고 생각하는 해결책을 적용해 보았다.
1. synchronized 사용 - (X)
Java의 synchronized는 모니터 방식으로 구현되어, 메서드가 실행되는 시점에 모니터를 소유하고, 실행이 끝난 후 release하는 방식으로 진행된다.
하지만 이 방법은 멀티 스레드에서 트랜잭션이 시작되는 시점이 동일할 때, 모니터를 소유하고 비즈니스 로직을 수행한 후 release를 하더라도 Transaction Commit이 진행되기 이전에 공고에 접근하여 신청한다면 한 공고에 여러 개의 신청이 들어갈 수 있어 문제가 될 것이다.
신청이 DB에 반영되는 시점은 트랜잭션이 커밋되는 시점이기 때문!
Thread1: 트랜잭션 시작 -> 모니터 -> 로직 수행 -> release -> -> Commit
Thread2: 트랜잭션 시작 -> 모니터 -> 로직 수행 -> release -> Commit
2. 낙관적 락 (Optimistic Lock)
데이터 갱신 시 충돌이 발생하지 않을 것이라는 가정을 두고 진행하는 락
DB단에 Lock을 설정하지 않고, Version을 관리하는 컬럼을 테이블에 추가해 데이터 수정 시 버전이 변한 데이터를 수정하지 않는지 판단하는 방식이다.
DB에서 제공하는 특징을 이용한 것이 아닌 Application Level에서 잡아주는 Lock이다.
충돌 발생 확률이 낮은 작업들에서 지속적인 락으로 인한 성능 저하를 막을 수 있다.
public class Application extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//
@Version
private Integer version;
}
해당 설정을 통해 version을 확인하여 맞지 않다면 ObjectOptimisticLockingFailureException이 발생한다. 첫 신청 이외에는 모두 실패하는 작업이 진행된다.
낙관적 락은 트랜잭션 대부분이 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법으로
동시성 문제가 빈번하게 일어나면 롤백 처리와 재시도 로직을 개발자가 작성해야 한다.
3. 비관적 락 (Pessimistic Lock)
데이터 갱신 시, 충돌이 발생할 것이라는 가정을 두고 진행하는 락
실제로 DB 단에 Lock을 설정하여 동시성을 제어하는 방법으로, DB단에서 자원에 대한 점유는 트랜잭션 단위로 수행된다.
한 트랜잭션에서 Lock을 설정한다면, 해당 트랜잭션이 종료되기 전까지는 다른 트랜잭션에서 해당 데이터를 수정할 수 없다.
Race Condition이 빈번하게 일어난다면 롤백의 횟수를 줄일 수 있어 낙관적 락보다 성능이 좋다는 장점이 있지만,
DB단의 Lock을 설정하여 다른 트랜잭션이 접근하지 못하게 되면 성능의 저하가 발생한다.
JPA에서 비관적 락을 구현하는 LockModType의 종류
- PESSIMISTIC_READ: 다른 트랜잭션에서 읽기만 가능
- PESSIMISTIC_WRITE: 다른 트랜잭션에서 읽기 및 쓰기 불가능
- PESSIMISTIC_FORCE_INCREMENT: 다른 트랜잭션에서 읽기 및 쓰기 불가능 + 버저닝 기능
4. Application 테이블에 UniqueConstraint 설정
동시성 문제를 해결하기 위해 Application 테이블에 UniqueConstraint 제약 조건을 설정하면 한 개의 post에 대한 공고 신청이 중복되는 것을 방지할 수 있다.
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"post_id"}))
public class Application extends BaseTimeEntity {
제약 조건을 만족하지 않는다면 DataIntegrityViolationException이 발생하게 되어,
기존에 존재하던 해당 공고에 대한 신청이 이미 존재할 경우에 대한 검증 로직이 대체될 수 있다.
if (applicationRepository.existsByPostId(postId)) {
throw new BadRequestException(ALREADY_EXIST_APPLICATION);
}
충돌이 많지 않을 것이라 생각해 낙관적 락과 UniqueConstraint를 고민했지만, 현재 한 공고에 대해 한 신청만을 허용하고
Application 데이터의 중복의 발생하지 않아야 한다는 것이 중점이므로 UniqueConstraint를 사용하는 것이 적합하다고 생각해 프로젝트에 적용해 보았다.
낙관적 락으로 구현한다면, post에 version을 두어 상태 변화를 확인하고(공고에 신청이 들어오면 공고의 상태가 변화함) 변하지 않았다면, Application을 저장하는 과정을 거칠 것인데 이 부분에서 UniqueConstraint를 사용했을 때보다 이점을 찾기 어려웠다.
동시에 발생하는 일 가운데 수정이 일어나는 자원에 대해서는 그것의 빈도에 따라 낙관적 락과 비관적 락을 도입하는 것이 더 유리할 것이다. 추후 프로젝트에서 동시성 문제가 발생하는 작업들에 대해 낙관적 락과 비관적 락 모두 고려하여 적합한 방식을 도입할 예정이다!
참고 자료:
[Spring] 스프링 동시성 처리 방법(feat. 비관적 락, 낙관적 락, 네임드 락)
0. 들어가기 전 이전에는 DB 단의 동시성 처리 방법인 Lock에 대해서 알아봤습니다. https://ksh-coding.tistory.com/121 [DB] DB Lock이란? (feat. Lock 종류, 블로킹, 데드락) 0. 락(Lock)이란? 여러 커넥션에서 동시
ksh-coding.tistory.com
우리 프로젝트에서 동시성 이슈를 해결하는 방법 - 2. 낙관적 잠금
이전 글에서 하나의 청원에 동시에 한 유저로부터 여러 동의 요청이 발생했을 때, DB의 유니크 조건을 활용함으로써 하나의 청원 동의만을 허용할 수 있었다. 이번 글에서는 청원에 관리자가 답
wannte.tistory.com
'Trouble Shooting' 카테고리의 다른 글
saveAll 사용 시 쿼리가 여러 번 나가는 문제 이유는? (1) | 2023.11.11 |
---|---|
S3 이미지 업로드 시 나타나는 warn 로그 (1) | 2023.11.11 |
게시글 테이블과 게시글 이미지 테이블의 연관 관계 설정 (0) | 2023.11.11 |
RDB vs NoSQL (0) | 2023.10.20 |
소셜 로그인, JWT 관련 의문점 정리 (0) | 2023.10.18 |