JPA에서는 데이터베이스에서 조회된 엔티티를 영속성 컨텍스트에 저장합니다. 이후 동일한 엔티티를 조회하는 요청이 들어온다면, 데이터베이스에서 같은 쿼리를 사용하여 엔티티를 조회 후 반환하는 것이 아닌 영속성 컨텍스트 내에 저장되어있는 엔티티를 찾아 반환해주기 때문에, 데이터베이스의 부화를 줄일 수 있고 1차 캐시 역할을 하기 때문에 더 빠른 조회 성능을 기대할 수 있습니다.
JPA는 엔티티의 정보를 수정(UPDATE)할 때 더티 체킹이라는 방식을 사용합니다. 더티 체킹이란 직접 데이터베이스 객체를 조회하여 정보를 수정하는 것이 아닌 영속성 컨텍스트에서 관리하고있는 엔티티에 발생하는 변경 사항을 감지하는 것입니다.
이 방식은 별도의 UPDATE 쿼리를 작성하지 않아도 단순히 애플리케이션에서 값을 변경하여 적용할 수 있기 때문에 수정 작업에 작성되는 코드의 수를 획기적으로 줄일 수 있습니다.
하지만 이러한 방식이 장점만 있는 것은 아닙니다. 영속성 컨텍스트는 JPA의 엔티티 매니저를 통해 할당받게 됩니다. 여기서 엔티티 매니저는 스레드 간 공유할 수 있는 자원이 아닌 독립된 스레드 환경에서 활동하는 객체입니다. 따라서 영속성 컨텍스트도 각자 독립적으로 작동한다고 할 수 있습니다.
이럴 경우 발생하는 문제가 무었일까요? 게시물의 조회수를 업데이트하는 기능이 있다고 생각해봅시다. 이때 우리는 JPA의 더티 체킹을 활용하여 Post.updateReadCount()와 같은 간단한 메소드로 조회수를 변경 할 수 있겠습니다만, 여러명의 사용자가 동시에 하나의 게시물을 조회한다고 생각해 봅시다. 각자 독립적으로 생성된 영속성 컨텍스트 내의 조회수 정보는 서로 다를 수도 있고, A가 반영한 조회수 정보가 B에게서는 반영되지 않은 상태로 영속성 컨텍스트에 존재 할 수 있습니다.
실제로는 어떻게 동작하는지 간단하게 테스트를 진행해보도록 하겠습니다.
테스트도구 Jmeter를 활용하여 간단하게 테스트 해보겠습니다. 200개의 가상의 쓰레드(사용자)를 생성하여 각각의 쓰레드에서 동일한 게시물을 4번 총 800번의 조회가 발생하도록 해보겠습니다.
실행 결과는 어떨까요?
800개의 요청에 한참 모자란 213개의 조회수가 반영되어있는 것을 볼 수 있습니다.
이처럼 더티 체킹에 의존한 엔티티 변경은 동시성 문제를 야기할 수 있습니다. 만약 계좌 잔고와 같은 데이터에 이런 문제가 발생한다면 엄청 심각한 상황을 초래할 수도 있겠습니다.
JPA의 동시성 문제 해결을 위해서는 어떤 방법이 있을까요.
우선 수정 작업을 더티 체킹에 의존하지 않고 직접 쿼리를 작성하여 변경해보겠습니다.
엔티티 필드값 수정이 아닌 PK를 사용하여 직접 조회수를 UPDATE해주도록 하겠습니다.
동일하게 테스트를 실행한 결과 800개의 요청과 조회수가 정상적으로 반영된 모습입니다.
지금과 같이 1개의 데이터베이스를 1개의 애플리케이션에서 바라보고있는 간단한 구조의 프로젝트에서는 위와 같이 직접 UPDATE문을 작성해주는 방법으로도 충분한 해결 방법이 될 수 있습니다.
하지만 여러 개의 애플리케이션이 1개의 데이터베이스를 바라보고있는 MSA 구조의 프로젝트에서도 이와 같은 방식이 정상적으로 작동할까요. 그러한 상황에서라면 앞서 살펴보았던 영속성 컨텍스트 차원에서 동시성 문제가 아닌 또 다른 차원에서의 동시성 문제가 발생할 수 있습니다.
궁극적으로 데이터베이스의 데이터 삽입, 삭제, 갱신등에서 발생하는 동시성 문제를 해결하기 위해서는 데이터베이스에서 제공하는 LOCK기능을 사용하여야합니다.
이후 포스팅에서는 JPA에서 제공하는 어노테이션을 활용하여 데이터 갱신 시 데이터베이스에 LOCK을 걸고 트랜잭션을 처리하는 방법에 대해서 알아보록하겠습니다.