들어가기 앞서
Volunteer 사이드프로젝트를 진행하면서, 사용자는 공지사항에 대해서 읽음 표시를 할 수 있었다.
읽음 표시 및 해제가 가능하며 흔히 좋아요 기능과 같은 기능이다.
평소에 커뮤니티 서비스를 사용하면서 좋아요 버튼을 광클하면 어떻게 될까? 라는 생각을 했었고, 좋아요 기능과 비슷한 기능이 사이드프로젝트 내에 존재해 실제 무슨 일이 생길지 테스트 해보았다.
같은 사용자가 같은 공지사항의 읽음을 광클을 한다는 상황을 가정하고 Jmeter를 통해 간단히 실험해보았다.
하나의 공지사항에 대해서 같은 사용자는 한번만 읽음 확인이 가능한데, 같은 사용자의 정보 2개가 들어오는것을 볼 수 있었다. 동시성 이슈가 발생한 것이다.❗
같은 사용자 읽음 정보가 들어왔을 때, 읽음 확인 사용자 리스트 조회 시 중복된 사용자의 정보가 보여지는 문제가 발생한다.
왜 동시성 이슈가 발생할까?
시퀀스 | Tx1 | Tx2 |
1 | 봉사 공지사항 A 찾음 | |
2 | 봉사 공지사항 A 찾음 | |
3 | 공지사항 A 읽음 확인이 존재하지 않는가? True |
|
4 | 공지사항 A 읽음 확인이 존재하지 않는가? True |
|
5 | 공지사항 A 읽음 수 증가, 읽음 저장 | |
6 | 공지사항 A 읽음 수 증가, 읽음 저장 | |
7 | Tx1 commit | |
8 | Tx2 commit |
그럼 어떻게 해결할까?! 나는 아래 2가지 방식을 생각했다.
1. unique Constraints 사용
2. 낙관적 락 사용
그럼 두가지 방식을 모두 적용해 문제를 해결해보자.
Unique 제약 조건 사용
하나의 공지사항에 대해서 같은 사용자는 한번만 읽음 확인이 가능하므로(즉 한개의 정보만 insert 가능),
유니크 제약조건을 사용해 스프링의 DataViolationException 예외를 핸들링 하는 방식으로 해결할 수 있을거 같다고 생각했다.
필요한 복합키를 유니크 제약 조건으로 설정했다.
총 10개의 스레드로 테스트해보았고, 가장 먼저 commit 된 스레드를 제외하고 9개의 스레드는 DB 유니크 제약조건으로 인해 DataViolationException 예외가 발생하고 롤백을 예상했다.
예상한 결과와 같이 9개의 스레드는 commit 시 유니크 제약조건 위반이 발생해 롤백되었고, 정상적으로 하나의 정보만 들어온 것을 볼 수 있었다.
해결하고자 하는 상황의 동시성 문제는 해결된 것을 볼 수 있지만, 아래와 같은 문제(?)가 남아있다고 생각했다.
- DB 레벨에서 발생한 예외를 잡아 어플리케이션 예외로 다시 던저야하는 번거로움
- Spring의 DataViolationException 예외가 Unique key, Foreign key, Not Null 등 정확히 어떤 문제인지 알 수 없음.
이러한 이유로, 유니크 제약조건 방식이 아닌 @Version 기술을 통한 낙관적 락 방식을 사용해보기로 했다.
낙관적 락 사용
낙관적 락은 DB 자체에 락을 거는 것이 아닌, 어플리케이션 레벨에서 처리하는 것이므로 성능 측면에서도 저하가 거의 없다고 생각했다.
또한, 읽음 확인 엔티티와 공지사항 엔티티는 FK 연관관계가 없는 간접 참조를 하고있어, 데드락 발생 확률도 거의 없을 거같다 생각했다.
FK 연관관계 시 데드락 발생에 대해 최근 포스터에서 다룬적 있었는데, 궁금할 시 참고 바랍니다.
JPA의 낙관적 락을 사용하기 위해 공지사항 엔티티에 @Version을 추가했다.
기존 비지니스 로직에서, 공지사항 검색 시 낙관적 락이 사용되도록 @Lock(LockModeType.OPTIMISTIC)를 추가했다.
그럼 실제 테스트를 진행해보자. 앞서 유니크 제약조건 테스트와 동일하고 발생하는 예외의 종류만 다르다.
결과를 보게 되면, 9개의 요청에 대해서는 ObjectOptimisticLockingFailureException 예외가 발생한 것을 볼 수 있고,
정상적으로 하나의 정보만 insert된 것을 볼 수 있다.
그럼 초기에 동시성 문제가 발생한 상황의 시퀀스 흐름과 어떤점이 다를까?
시퀀스 | Tx1 | Tx2 |
1 | 봉사 공지사항 A 찾음, version==0 | |
2 | 봉사 공지사항 A 찾음, version==0 | |
3 | 공지사항 A 읽음 확인이 존재하지 않는가? True |
|
4 | 공지사항 A 읽음 확인이 존재하지 않는가? True |
|
5 | 공지사항 A 읽음 수 증가, 읽음 저장 | |
6 | 공지사항 A 읽음 수 증가, 읽음 저장 | |
7 | version==0? True, Tx1 commit, version+=1(update) |
|
8 | version==0? False, Tx2 rollback |
트랜잭션 커밋 전, version 값이 일치하는지 검증을 통해 동시성 문제를 해결하게 된다.
마지막으로, 비지니스 로직에서 발생한 ObjectOptimisticLockingFailureException 예외를
@RestControllerAdvice를 사용함으써, 예외에 대한 관심사항도 분리하도록 했다.
마치며
좋아요 기능 등이 있는 서비스를 사용하면서 궁금했던 부분을 실제 프로젝트에 테스트 해보고 해결할 수 있는 좋은 경험을 한거 같다.
이제 정식 배포가 얼마 남지 않았는데, 배포 및 운영하면 트러블 슈팅이 발생한다면 꾸준히 기록해봐야겠다.
참고 자료
'JAVA > Spring & Java 학습 기록' 카테고리의 다른 글
퍼사드 패턴을 도입해 도메인 간 의존성을 줄이다. (0) | 2023.08.13 |
---|---|
Spring Rest docs 적용기 (0) | 2023.08.08 |
동시성 이슈와 데드락 문제를 비관적 락를 이용해 해결하다. (0) | 2023.06.28 |
스프링의 예외 누수 문제 해결 변천사 (0) | 2023.06.25 |
스프링 인터셉터를 이용한 권한 검증 분리하기 (0) | 2023.06.14 |