해결된 질문
작성
·
3.7K
9
안녕하세요 영한님 JPA 책 부터 인프런의 여러 좋은 강의들 항상 잘 듣고 배우고 있습니다.
간혹 기본키를 숫자 형태로 max + 1로 하는 레거시한 프로젝트를 만나는 경우가 있는데요. 이 경우 동시성 이슈 관련하여 JPA에서 어떤 전략을 사용해야하는지 궁금합니다.
(Spring Data JPA 사용 중입니다)
구체적인 예시를 들면
* 사용자 - 식단카드라는 두 개의 테이블이 존재합니다.
* 식단카드에는 card_key, user_id를 복합키로하며 그 외 카드 식단 정보가 있습니다.
* 식단카드 생성시 user_id를 조건으로 검색하여 가장 높은 card_key를 가져와 + 1해서 card_key를 만들어 저장합니다.
이 때 클라이언트 단에서 비동기 처리로 여러 식단카드를 등록하려다보니 먼저 실행된 요청 쓰레드의 트랜잭션이 성공하고, 두번째 실행된 요청 쓰레드가 먼저 실행된 쓰레드랑 같은 최근값을 읽고 + 1해서 커밋하려니 중복된 키 오류가 발생합니다.
제가 시도해본 방법은 여러가지였는데, 모두 잘 안되더라고요.
MariaDB innoDB 엔진 사용하고요. 기본적으로 JPQL을 사용하여 max + 1 값을 가져오는 Repository 인터페이스 메서드 선언
시도 해본 방법
1. @Transactional isolation level을 SERIALIZBLE 로 변경
* 해결 되지 않음
2. 비관적 락 @Lock(LockModeType.PESSIMISTIC_WRITE)
* 해결 되지 않음
* 구글링 찾아보니 기본키 인덱스 이외의 다른 인덱스가 있을 경우 동작하지 않는다고 하는데, 정확한 공식 문서 내용을 찾아볼 수가 없네요.
3. JPQL NATIVE QUERY 사용하고 FOR UPDATE로 LOCK
* 해결 되지 않음
결국 Spring Retry 연결하여 실패시 계속 재시도하여 들어가게끔 처리는 했는데, 어거지로 해결한 느낌이라 계속 속에 남아있네요 ㅜㅜ
트랜잭션이나 LOCK에 대해서 공부해서 적용해봐도 안되는 이유를 모르겠습니다ㅜㅜ
답변 2
10
안녕하세요. 훈마로님
max+1은 데이터베이스 락으로 해결하기가 어렵습니다.
데이터베이스 락은 특정 row에 락을 거는 것인데, 생각해보면 max+1을 구하는 방식은 특정 row에 락을 걸어서 해결이 되는 것이 아니니까요.
그리고 특정 row를 변경하는 것이 아니라 새로운 row가 추가되는 것이지요.
이런 문제의 해결 방안은 다음과 같습니다.
[별도의 채번 테이블]
별도의 채번 테이블을 만들어서 사용하는 방식으로 문제를 해결해야 합니다. 채번 테이블의 row 값이 변경되기 때문에 문제를 좀 더 쉽게 해결할 수 있습니다. 다만 현재 요구사항을 만족하기는 어려울 수 있습니다.
[분산 락]
좀더 깔끔한 대안은 채번하는 로직 자체를 한번에 하나의 쓰레드만 동작하게 하는 방법입니다.
이 경우 서버가 여러대가 있어도 하나의 쓰레드만 동작해야 하기 때문에, 락을 동기화 하는 방법이 필요합니다. 따라서 분산 락을 사용해야 합니다.
분산 락 관련해서는 다음을 글들을 참고해주세요.
https://woowabros.github.io/experience/2019/05/30/mysql-user-level-lock.html
https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
[RDB에서 사용할 수 있는 단순한 방법]
별도의 락 테이블을 하나 만들고, 그곳에 유니크 제약조건을 걸어둡니다.
그리고 여기의 경우 max + 1 채번을 한 다음에 락 테이블에 max+1값을 유니크 제약조건이 걸린 컬럼의 값으로 저장합니다.
이렇게 하면 동시에 접근할 경우 두번째 조건은 유니크 제약 조건 때문에 실패합니다. 실패한 경우 n번 정도 retry 하도록 로직을 작성합니다.
도움이 되셨길 바래요.
5