본문 바로가기
프로젝트 기록/Spring

[Spring Data JPA] MariaDB에서의 동시성 이슈로 인한 갱신 이상 문제와 그 해결법(비관적 락, 낙관적 락)

by clean01 2024. 11. 20.

동시성 이슈란?

멀티 스레드로 동작하는 데이터베이스에서 동시에 2개 이상의 트랜잭션이 실행돼서 데이터의 정합성이 맞지 않게 되는 이슈를 의미합니다.

 

동시성 이슈를 확인해보자

MariaDB에서 정말 동시성 이슈가 생기는지를 확인해보기 위해 Spring Boot 프로젝트 하나를 만들었습니다.

 

우선 이렇게 2개의 엔티티를 만들어주었는데, 각각의 용도는 이렇습니다.

TestEntity: 초기에 count를 N개로 하고, 이 count를 감소시키며 갱신이상 문제가 생기는지 확인

RequestRecord: 요청이 들어올 때마다 하나씩 생성해서 해당 트랜잭션이 롤백된게 아니라는 것을 증명하기 위해 사용

 

그리고 count 감소 API를 위한 TestController와 TestService도 만들어줬습니다.

 

요청이 들어오면, 1번 TestEntity를 찾아 count를 1 감소시키고, 새로운 RequestRecord 데이터를 만들어 DB에 저장합니다.

이때, reqId는 TestEntity의 count 값이 됩니다.

 

Spring boot 프로젝트를 실행 시키면 아래와 같이 2개의 테이블이 잘 만들어진 것을 확인할 수 있고,

두번째 사진과 같이 test_entity 테이블에는 아이디가 1이고 count가 100인 레코드 하나를 미리 추가해줬습니다.

 

그럼 이제 동시에 decrease 요청을 보내보면 되겠죠?

jmeter로 100개의 스레드로 동시에 요청을 보내는 테스트를 만들어보겠습니다

 

실행시켜보면!

 

 

모든 요청이 잘 성공했습니다.

그럼 test_entity 데이터의 count 값은 0이 돼 있겠죠?

 

 

 

어라... 아닙니다..

카운트가 17개 밖에 줄지 않았습니다.

 

request_record의 데이터가 온전히 100개가 들어간 것을 보니 롤백된 트랜잭션도 없습니다.

 

count가 N인 상황에서 여러 개의 스레드가 N이라는 값을 읽고, 1을 감소시킨 N-1이라는 값을 덮어씌워서 갱신 이상 문제가 생긴 것인데요.

이를 그림으로 설명해보면 아래와 같습니다

 

동상이농 발표 자료로 설명 날먹하기

 

 

그렇다면 이런 문제는 어떻게 해결할 수 있을까요?

 

1. 비관적 락(DB 레코드에 진짜 락 걸기)

2. 낙관적 락(실제로는 락을 걸지 않고 버전을 관리함으로써 해결)

3. 레디스와 같은 싱글 스레드로 동작하는 서드파티에서 데이터 관리

 

오늘은 Spring 프레임워크와 DB 차원에서 해결할 수 있는 1, 2번 방식에 대해 알아보겠습니다.

 

비관적 락

비관적 락은 데이터베이스에 실제로 배타 락을 걸어 해결하는 방식인데요.

Mybatis와 같은 raw query를 쓰는 기술로 DB에 접근하고 있다면 "select ... for update" 구문을 통해 읽기 시점에 락을 걸 수 있습니다.

말 그대로, '지금부터 이 레코드 고치려고 읽는거니까 건드리지마!'라는 의미입니다.

 

JPA에서는 @Lock이라는 어노테이션을 repository의 메서드 위에 붙여서 배타락을 걸 수 있습니다.

 

 

package com.example.demo;

import jakarta.persistence.LockModeType;
import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.QueryHints;

import java.util.Optional;

public interface TestRepository extends JpaRepository<TestEntity, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "1000")})
    Optional<TestEntity> findById(Long id);
}

 

이렇게 메서드에 어노테이션만 붙이고 동일한 테스트를 실행해보면

 

 

이렇게 lost update 문제가 생기지 않고 카운트 값이 1씩 감소하는 것을 확인할 수 있습니다.

 

 

낙관적 락

낙관적 락은 실제로 락을 걸지 않고 동시성 문제를 해결합니다.

레코드를 고칠 때마다 version 값을 증가시키고, 커밋 시점에 조회 시의 버전 값과 커밋 시점의 버전 값을 확인하여 일치하지 않는다면 예외를 발생시켜서 해당 트랜잭션을 롤백 시킵니다.

 

낙관적 락은 Spring Data JPA에서는 @Version 이라는 어노테이션을 통해 구현할 수 있습니다.

아래와 같이 @Version 어노테이션을 단 버전 관리용 컬럼을 추가해서 버전을 관리하는 것입니다.

저는 아래와 같이 TestEntity에 버전 컬럼을 추가해줬습니다.

 

비관적 락을 걸기 위해 바꿨던 TestEntityRepository 코드는 원래대로 바꿔주고 jmeter 테스트를 실행했습니다.

 

 

 

100개의 요청 중 15개만 성공한 모습입니다.

85개의 요청은 커밋되는 시점에 버전이 맞지 않아 롤백 된 것 같습니다.

그럼 데이터베이스는 어떻게 되어 있을까요? 성공한 15개의 요청에서는 데이터 정합성이 잘 지켜졌을까요?

 

 

성공한 15개의 요청에 대해서는 카운트가 정확히 감소한 것을 확인할 수 있었습니다.

 

Spring Data JPA에서 비관적 / 낙관적 락을 통해 동시성 문제를 해결하는 방법과 테스트 과정에 대한 포스트를 마치겠습니다. :)

 

Reference

https://chaewsscode.tistory.com/181

 

[Spring Boot] JPA 동시성 이슈, 낙관적 락(Optimistic Lock)을 이용한 해결

이전에 썼던 상품 좋아요 생성 API를 구현하면서 "상품 좋아요 수"의 동시성 문제에 대해 고민하게 되었다. 👍 상품 좋아요 수회원이 상품 좋아요를 생성하면 해당 상품은 좋아요 개수가 1 증가

chaewsscode.tistory.com

https://wildeveloperetrain.tistory.com/128

 

SELECT FOR UPDATE / JPA를 사용한 비관적 잠금

'동시성(Concurrency)' 웹 서비스에서는 다수의 사용자들이 데이터베이스에 동시에 접근하는 경우가 빈번하게 발생합니다. 때문에 데이터의 일관성에 대한 처리가 필요한데요. 이를 '동시성(Concurren

wildeveloperetrain.tistory.com