프로젝트 진행 중 동시성 문제가 발생하여 정리해보았다.
이 프로젝트에서는 여러 기기에 같은 계정으로 동시 로그인이 가능하도록 정책이 정해져 있다.
동시성에 대해 생각해보지 않고 있다가 동시성에 대한 강의를 들으며 현재 프로젝트에는 동시성 문제가 없는지 확인하다가 발견하였고 이를 해결한 내용을 정리해보겠다.
현상
다른 기기에서 같은 회원 계정으로 같은 상품을 동시에 좋아요 클릭 할 경우, 좋아요가 중복으로 저장되는 이슈가 발생하였다. 중복 저장되어 데이터 정합성에 문제가 발생하였다.

원인 및 해결방안
기존에 이 데이터를 저장하던 favorite_Item 테이블은 로그성으로 데이터를 쌓고 있었다. 통계 작업 설계 중 좋아요에 대한 로그성 테이블이 추가되었는데, 이 후에 이 테이블에 대한 정책이 변경되었어야 하는데 그대로 유지되고 있었다. 로그성으로 쌓이며 데이터 정합성이 어긋나게 되어버렸다..
새로운 로그성 테이블이 존재하므로 이 테이블의 경우 유저와 상품을 기준으로 unique로 잡고 1개만 데이터가 쌓고 deleted 컬럼으로 좋아요에 대한 정보를 업데이트하는 방식으로 테이블을 변경하였다.
+)이번 좋아요 동시성의 경우 테이블 변경이 가능하고 필요한 경우 였기에 간단하게 해결할 수 있었는데, 해결방법을 찾으면서 공부한 동시성 내용도 정리해보자한다.
동시성 해결 방안
1. 낙관적 락(Optimistic Lock)
- DB Level 에서 동시성을 처리하는것이 아닌 Application Level 에서 처리
- 충돌이 발생하지 않을 것이라 가정하고 Lock을 거는 방식
- 트랜잭션을 commit 하는 시점에 충돌을 알 수 있음
- version(hachcode,timestamp)과 같은 별도의 컬럼을 추가하여 충돌적인 업데이트를 막는다.
- 롤백 - 충돌을 해결하려면 개발자가 수동으로 롤백처리 해야함.
- 최초 하나의 요청만 성공하고 나머지 요청들은 ObjectOptimisticLockingFailureException 예외가 발생한다
ex) JPA의 버전관리 기능 - @Version
- jpa의 낙관적 락을 사용하기 위해서는 @Version을 사용해서 버전 관리 기능을 추가 해야한다.
@Version을 적용할 수 있는 데이터 타입은 아래와 같다.
- Long, long
- Integer, int
- Short, short
- Timestamp
- 엔티티에 @Version을 위한 필드를 추가하면, 엔티티를 수정할때 마다 버전이 하나씩 자동으로 증가한다. 그리고 엔티티를 수정할 때 조회 시점의 버전과, 수정 시점의 버전이 다르면 예외가 발생한다. 이런 매커니즘때문에 최초 커밋만 인정되는 방식을 구현 할 수 있으므로, 두 번의 갱신 분실 문제를 방지할 수 있다.
- 추천하는 전략은 READ COMMITTED 격리 수준 + 낙관적 버전 관리이다.
2. 비관적 락(Pessimistic Lock)
- DB Level 동시성을 처리
- 충돌이 발생할것이라 가정하고 우선 DB에 Lock을 거는 방식
- 데이터를 수정하는 즉시 충돌을 알 수 있음
- 트랜젝셔널이 시작될 때 shared lock 또는 Exclusive lock을 걸고 시작하는 방법
- Shared Lock을 걸게 되면 write를 하기위해서는 Exclucive Lock을 얻어야하는데 Shared Lock이 다른 트랜잭션에 의해서 걸려 있으면 해당 Lock을 얻지 못해서 업데이트를 할 수 없습니다. 수정을 하기 위해서는 해당 트랜잭션을 제외한 모든 트랜잭션이 종료(commit) 되어야합니다.
- 롤백 - 하나의 트랜젝션으로 묶여있기 때문에 database단에서 전체 rollback이 일어난다.
ex) JPA Lock 적용 범위
@Lock을 적용 가능한 범위는 아래와 같다.
- EntityManager.lock() , EntityManager.find(), EntityManager.refresh()
- Query.setLockMode()
- @NamedQuery
Querydsl과 같은 쿼리빌더로 작성한 코드로는 Lock을 사용할 수 없는건가?
-> Querydsl에서는 setLockMode(LockModeType lockMode) 메서드를 지원해준다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
- 레포지토리 수준의 비관적락은 위 어노테이션으로 설정.
3. 네임드 락
4. Redis Redisson 라이브러리 활용
언제 어느 락이 좋은가?
낙관적 락
- 실시간으로 변화하는 데이터가 중요할 때
- 트랜잭션 충돌이 자주 발생하지 않을 때
비관적 락
- 실시간일 필요없이 어느정도 시간의 텀을 줘도 될 때
- 트랜잭션 충돌이 자주 발생할 때
마치며
낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)은 싱글 DB 환경인 경우에만 적용 가능한 개념이다. 샤딩 또는 Replication 등을 통해 DB가 분산되어있는 환경이라면 적용할 수 없고 한다.
다음에는 분산 DB 환경에서 쓸 분산 락에 대해 공부해봐야겠다.