JAVA/JPA 학습기록

[JPA] 프로젝트 동시성 이슈 해결을 위해 낙관적 락을 걸어보았다

kth990303 2022. 10. 29. 15:55
반응형

롤링페이퍼 기반 서비스 플랫폼을 개발하던 중 아래 버그를 발견했다.

???

대체 어떤 일이 일어난다는 거야...

하는 마음으로 같은 메시지를 막 광클했을 때 위와 같이 `에러가 발생했어요` 페이지가 뜨는 것을 발견했다.

 

해당 메시지에 좋아요를 광클하거나, 같은 계정으로 PC/모바일 등 다중 환경 접속 후 좋아요를 여러번 동시에 누르면 발생하는 버그였다.

 

즉, 동시성 이슈로 발생한 문제였다!

 

우리 서비스에서는 한 명의 회원이 같은 메시지에 좋아요를 2개 이상 누를 수 없다. 

인스타나 페이스북처럼 좋아요가 눌러진 상태에서 좋아요를 또 누르면 좋아요 취소가 작동돼야 한다.

같은 message_id(메시지)에 같은 member_id(회원)의 좋아요가 2개 들어간 버그

하지만 위와 같이 광클하는 경우에는 그 찰나에 같은 회원의 메시지 좋아요가 2개 들어가버린 것을 확인할 수 있었다.

 

상황을 그림으로 정리해보자면 아래와 같다.

우리 플랫폼 백엔드 환경은 MySQL 8.0 (innoDB)이며, 트랜잭션 격리레벨은 디폴트로 Repeatable Read이다.

 

메시지 A 를 찾는 select 과정은 참고로 select ... for update가 아닌 진짜 단순 select ... from이다. 따라서 해당 레코드에 X-Lock을 얻는 과정이 아닌, 아예 락을 얻는 과정이 발생하지 않는다. 

따라서 트랜잭션 2(이하 Tx2) 에서 메시지 A를 select하는 과정 역시 select ... for update가 아닌 단순 select이므로 Tx1에서 락을 얻든 얻지 않든 접근이 가능하다. 

 

위 내용을 요약하자면 충분히 동시 접근이 가능한 쿼리라는 것.

따라서 해당 메시지를 광클하는 경우에 select로 얻은 다음, 좋아요 기록이 존재하지 않는가? 과정을 거칠 때 두 트랜잭션에서 모두 true가 나오게 돼 DB에 둘 다 저장되게 된다. 


낙관적 락과 비관적 락

그렇다면 해당 동시성 이슈 문제를 해결하기 위해 어떤 락을 걸어주어야 할까?

우리 프로젝트의 경우 Repeatable Read 환경이기 때문에 비관적 락(Pessimistic Lock) 또한 충분히 고민해볼 수 있는 환경이었다. 아예 트랜잭션을 시작할 때 messageRepository.findById 메서드에 아래 옵션을 건다고 하자.

@Lock(LockModeType.PESSIMISTIC_WRITE)

이렇게 할 경우 findById로 메시지를 찾을 때 select ... from이 아닌 select ... for update 로 되고, 따라서 X-Lock을 얻는다. Tx2(다른 트랜잭션)에서 message.findById를 하기 위해선 Tx1이 끝날 때까지 대기해야 된다. 이렇게 트랜잭션에서 충돌이 날 것이라 가정하고 미리 락을 걸어버리는 비관적 락 방법으로 위 문제를 해결할 수도 있다.

하지만 비관적 락은 지나치게 고립성이 높아 성능이 많이 저하된다는 단점이 존재한다. 

 

 

위 이슈의 경우 트랜잭션에서 충돌이 나지 않을 것이라 가정하고 애플리케이션에서 제공해주는 기법인 낙관적 락으로도 충분히 해결할 수 있다고 판단했다. 낙관적 락을 걸게 되면 Tx1에서 해당 Version을 업데이트하면서 커밋을 하기 때문에 Tx2에서 동시에 작업을 할 경우 커밋을 할 때 Version이 다르면 롤백되어 DB에 같은 좋아요가 2개 이상 쌓이지 않고 하나만 save되게 된다. 따라서 낙관적 락으로도 해당 이슈를 충분히 해결할 수 있다. 또한, DB에 락을 거는 것이 아닌, 애플리케이션 단에서 처리하는 것이므로 성능저하도 비관적락 방법에 비해 거의 일어나지 않는다고 판단했다.

@Lock(LockModeType.OPTIMISTIC)

 

NONE이 아닌 OPTIMISTIC 타입으로 설정해주어야 조회시에도 버전을 체크하고 확인하기 때문에 OPTIMISTIC 타입으로 진행해주었다.

 

또한, 메시지 엔티티에 아래와 같이 @Version을 추가해주었다.

@Version
private Long version;

이렇게 될 경우 아래처럼 진행된다.

 

 

락을 안걸고 해결하는 방법은 없을까?

사실 위 케이스의 경우 uniqueConstraints로 해결할 수도 있을 것이라 생각한다. 유니크 옵션을 걸어줌으로써 DB에 두 개 이상의 값이 들어가게 될 경우가 생긴다면 DataViolationException을 터뜨리면서 롤백될 것이기 때문.

 

하지만 아래와 같은 이유로 일단 낙관적 락으로 해결하는 방법을 선택했다.

 

  • DB 내에서 예외를 터뜨리면서 해당 예외를 잡아 애플리케이션 예외로 다시 넘겨주어야 한다는 번거로움
  • 낙관적 락을 걸었을 때 DB 자체에 락을 거는 게 아니므로 성능저하가 거의 없다는 점
  • 메시지 좋아요 엔티티는 연관관계 직접참조가 아닌 id로 간접참조를 해줌으로써 FK가 설정돼있지 않은 상태이다. 따라서 낙관적 락을 걸 때 데드락 확률이 거의 없을 것이라 판단했다.
  • 사실 그리고 낙관적 락을 도입해봄으로써 락에 대해 공부해보고 성장해보자는 의도도 좀 있었다 ㅋㅋ)

 

참고로 외래키를 걸면 데드락 발생 확률이 높아지는 이유는 아래 블로그 포스팅을 참고하자.

https://junghyungil.tistory.com/m/178

 

[MySQL] 외래키(Foreign Key)와 데드락(DeadLock)

안녕하세요. 오늘은 외래키(Foreign Key)를 사용할 때, 발생할 수 있는 데드락(Dead Lock)에 관해 글을 작성해보았습니다. 우선 외래키(Foreign Key)와 데드락(Dead Lock)란 무엇일까요? 데드락이란, 둘 이상

junghyungil.tistory.com

 

If a FOREIGN KEY constraint is defined on a table, any insert, update, or delete that requires the constraint condition to be checked sets shared record-level locks on the records that it looks at to check the constraint. InnoDB also sets these locks in the case where the constraint fails.

 

 

다만, 위 이유들이 uniqueConstraints를 도입하지 않아야 된다는 이유로 볼 수는 없다고 생각한다.

조금 더 프로젝트를 진행해보고 낙관적 락에 의한 트러블슈팅이 생길 경우에 다시 고려해볼만한 매력적인 이슈라 생각된다.

 

이번 기회에 동시성 이슈 및 락에 대해 조금이나마 공부할 수 있는 계기가 됐다.

어서 다양한 트러블슈팅을 통해 동시성 이슈 및 db lock에 대해 더 공부할 수 있음 좋겠다 ㅎㅎ


해당 관련 PR은 아래 링크에서 볼 수 있다.

https://github.com/woowacourse-teams/2022-nae-pyeon/pull/632

 

feat: 메세지 좋아요시 동시성 이슈를 해결한다. by asebn1 · Pull Request #632 · woowacourse-teams/2022-nae-pyeo

문제원인 시퀀스 Tx1 Tx2 1 메시지 A 찾음   2   메시지 A찾음 3 메세지 좋아요 기록 존재하지 않는가? true   4   메세지 좋아요 기록 존재하지 않는가? true 5 메세지 좋아요 저장   6   메세지 좋아요

github.com

 

참고

 

반응형