진짜 개발자
본문 바로가기

FrameWork/Spring MVC

Spring MVC - Spring MVC 동기화와 JPA 잠금기법

728x90
Spring MVC 동시성 테스트

이번 포스팅에서는 동기화 관련 문제를 다루어 보도록 하겠습니다.

우선, 동기화에 대해 알아보고, Spring MVC를 이용시 발생할 수 있는 문제점을 보도록 하겠습니다.

마지막으로, 이를 해결하는 방법도 알아보도록 하겠습니다.

 

 만약! 현재 Spring MVC 프로젝트를 진행중이지만, Thread를 별도로 생성해서 공유 자원에 접근하는 것이 아닌데 왜, 동기화 문제가 발생하는지를 궁금해하신다면, Servlet에 대해 다시 공부를 해보셔야 합니다.

 

1. 동기화

여러 스레드간 공유되는 자원은 항상 동기화 관련 처리를 해주어야합니다.

 

자바에서는 객체의 멤버변수가 동기화 대상 데이터가 될 수 있습니다. JVM에서, 객체는 Heap 영역에 할당되고, 이 Heap 영역을 Thread들이 공유하기 때문입니다.

 

예를들어, 위와 같은 Counter 클래스가 있다고 해보겠습니다.

 

이제 위와 같이 2개의 스레드를 이용해서, 각각 c.increase()c.decrease()를 20만번씩 호출해보도록 하겠습니다. 결과가 어떨것 같으신가요?

 

0에서부터 시작했고, +1을 20만번, 그리고 -1을 20만번 호출했으니 0 + 200000 - 200000 = 0이 결과로 나와야 하지 않을까요?

 

어? 결과가 2가 출력되었네요?

 

다시 한번 돌려볼까요? 이번에는 -1이 출력되었습니다. 결과가 계속 바뀌네요?

 

 

이유

실제로 java의 ++--는 원자적으로 실행이 되지 않습니다. 위와 같이 3단계로 나누어 실행이 되는데요. 아래에서 2개의 thread를 이용해 ++를 처리하는 과정을 통해서 문제점을 조금 더 자세히 알아보도록 하겠습니다.

 

count는 Thread간 공유되는 자원이며, 초기값은 0입니다.

 

Thread1에게 CPU가 먼저 할당되어, load count가 실행되어 count값을 가져온 상황에서, Thread2에게 CPU가 할당이 되었습니다. 지금 thread1이 가지고 있는 count의 값은 0입니다.

 

Thread2가 CPU를 점유하고 있는 상태에서, count를 가져와 1을 증가시킨 뒤, 이 값을 저장합니다. 이때 count에 저장된 값은 1입니다.

 

Thread2의 실행이 끝나고, Thread1에게 다시 CPU가 할당되고, 이때 CPU는 이전에 마지막으로 실행한 명령어의 다음 명령어 부터 실행하게 됩니다. 이때, Thread1이 가지고 있는 count의 값은 0입니다. 이 값에 +1을 합니다. 마지막으로, Thread1은 count에 1을 저장합니다.

 

최종적으로 count에 저장된 값은 1이 됩니다.

이 때문에, 스레드간에 공유되는 자원은 동기화를 필수적으로 해주어야 합니다.

 

 

 

 

2. Spring MVC에서의 동기화 문제

이번에는 Spring MVC를 이용할 때의, 동기화 문제를 알아보도록 하겠습니다.

 

만약! 현재 Spring MVC 프로젝트를 진행중이지만, Thread를 별도로 생성해서 공유 자원에 접근하는 것이 아닌데 왜, 동기화 문제가 발생하는지를 궁금해하신다면, Servlet에 대해 다시 공부를 해보셔야 합니다.

 

기본적으로 Spring MVC에서 사용자로부터 요청을 받게 되면, Container에서 Thread를 생성하고, 이에 대한 처리를 한뒤 응답을 주는 구조이기 때문에, 기본적으로 멀티 스레드 환경이라고 생각을 하셔야 합니다.

 

 

2.1 시나리오

  1. 게시판에 글이 하나 존재합니다.
  2. 게시판의 글 조회시 조회수가 1씩 증가합니다.
  3. 수백명의 사람들이 동시에 수차례 글을 조회합니다.
  4. 조회수가 올바르게 올라갔는지 확인합니다.

물론 조회수가 중요한 도메인이 아니라면, 어느정도 조회수 데이터의 일관성이 깨지더라도 크게 지장이 없지만, 유튜브와 같이 조회수가 수입과 직결되는 도메인이라고 가정하고 문제를 해결해 보도록 하겠습니다.

 

 

2.2 구현

Entity

실제 게시판이라면 성능의 문제로 위와 같이 조회수 필드를 글에 같이 넣지는 않겠지만, 간단한 예제를 위해서, 우선 위와 같이 필드를 넣도록 하겠습니다.

 

Service

 

Rest Controller

write로 요청시 임의로 글을 하나 작성하도록 하고, board로 요청시 해당글을 조회하도록 하겠습니다.

 

 

2.3 테스트

테스트에는 테스트 코드를 사용해보기도 하고, JMeter를 이용해보기도 할것 입니다.

 

 

Junit을 이용한 테스트

우선 코드단에서 테스트를 해보도록 하겠습니다.

 

Thread 수는 100으로 정하고 이것을 이용해 글 읽기 요청을 동시에 하도록 할 것입니다. (CountDownLatch는 thread가 모두 종료되기를 기다리도록 도와주는 역할을 합니다., thread의 join() 메소드와 비슷한 역할)

 

100번의 글 읽기 요청을 했으니 조회수는 100이어야 할 것입니다.

 

역시 동기화 처리를 해주지 않았기 때문에, 올바른 값이 나오지 않습니다.

 

 

 

Jmeter를 이용한 테스트

https://galid1.tistory.com/373 Jmeter 설치

https://galid1.tistory.com/374 Jmeter 간단한 사용법

 

우선 브라우저에서 /write로 요청을 보내, 임의의 글을 작성하도록 합니다.

 

글이 작성되었음을 확인합니다. VIEW_COUNT는 현재 0입니다.

 

100개의 스레드로 100번 요청하도록 해보겠습니다. 실행 이후, 조회수는 100 * 10010000이 되어야겠죠?

 

??? 결과가 우리가 예상한것과 너무 다르게 나왔습니다. 이 처럼 Spring MVC에서도, 멀티스레딩 환경이기 때문에, 공유자원에 대한 동기화 처리가 필요합니다.

 

 

 

3. 동기화 해결

잠금종류

우선 JPA의 잠금기법을 알아보기전, 잠금의 종류에 대해 알아보도록 하겠습니다.

 

Optimistic Locking (낙관적 잠금)

이는, 버전을 이용한 잠금 기법으로, 데이터 갱신시 스레드에 의해 경합이 발생하지 않을거라고 생각을 하고 잠금처리를 하는 기법입니다.

우선 데이터에 version이라는 개념이 추가되고, 공유 대상의 데이터를 가져올 때 버전을 확인합니다. 이후, 알맞은 처리를 한뒤, 저장시에 자신이 가져온 version과 같은지를 확인합니다. 이때 version이 다르다면 에러를 발생시킵니다.

거의 충돌 감지? 와 같은 개념인것 같습니다. 이 기법은, 회원정보 수정등과 같이 동시에 많은 처리가 이루어지지 않을거라고 예상되는 곳에서 사용하는것이 좋아보입니다.

 

Pessmistic Locking(비관적 잠금)

이 방법은, 비관적으로 즉, 무조건 데이터 경합이 발생할 것이라고 생각하고 먼저 데이터에 대한 잠금을 걸도록 하는 잠금 방식입니다.

이 방법은 주문시의 재고량 관리와 같이 많은 사람들이 동시에 변경을 요청하는 작업이 일어나는 곳에 사용해야하는 방법입니다.

 

 

3.1 JPA 잠금기법

앞서 말씀드렸듯이, 현재 우리의 서버에서 에러가 발생하는 원인은, 동기화 처리가 되지 않았기 때문입니다. 조금 더 자세히 말씀드리자면, 공유 자원인 Board Entity의 viewCount에 대한 동기화가 되지 않았기 때문입니다.

 

암시적 잠금

코드로 명시적으로 잠금을 하지 않아도 JPA에서 자동으로 잠금이 발생하는 것을 의미합니다.

Entity에 @Version이 부여된 필드가 존재하거나, @OptimisticLocking어노테이션이 부여되어 있다면, 이 Entity를 조회하는 경우 자동으로 각각에 맞는 잠금처리가 이루어 집니다.

 

명시적 잠금

의도적으로 잠금을 실행하는 것을 명시적잠금이라고 합니다.

EntityManager를 이용해 Entity조회시 LockMode를 지정하거나, createQuery의 LockMode를 set하거나, select for update를 이용해서직접 잠금을 지정할 수 있습니다.

 

 

 

3.2 잠금 기법을 이용해 조회수 동기화 하기

낙관적 잠금을 이용해보자

우선 낙관적 잠금을 이용해서 조회수 데이터의 일관성이 깨지는것을 막아보도록 하겠습니다.

 

앞서 말씀드렸듯이, 동기화가 필요한 필드에 @Version을 부여하면, 낙관적 잠금이 암시적으로 설정됩니다.

 

테스트 실행

낙관적 잠금을 걸고나서, 이와 같은 에러가 발생할 것이라고 예측이 되셨어야 합니다.

낙관적 잠금은, 여러 스레드가 동시에 한 데이터를 변경하려 할때 우선은 데이터를 반환해주고, 이를 저장하는 시점에 다른 스레드에 의한 데이터의 변경이 있으면 이를 덮어 쓰지 못하도록, 에러를 발생시키기 때문입니다.

우리의 테스트는, 100개의 스레드를 이용해 조회수를 동시에 변경하도록 했으니 이때, 낙관적 잠금을 이용한다면, 당연히 변경 오류가 발생할 것 입니다.

 

 

비관적 잠금을 이용해보자

마찬가지로 암시적 잠금 기법을 사용할 것입니다. BoardRepository에, 만약 변경을 위해 글을 조회하는 경우에는, 위의 @Lock(LockModeType.PESSIMISTIC_READ)가 부여된 find 메소드를 이용하도록 할 것입니다.

 

BoardService의 read() 메소드중 위의, board를 조회하는 곳에서 비관적 잠금 설정이 된 find 메소드를 사용하도록 변경합니다.

 

테스트 실행

성공입니다...

 

 

Jmeter 테스트

100개의 스레드로 10번의 요청을 하도록 하겠습니다.

 

성공입니다 !