💢 문제
현재 우리의 메인 화면에는 조회수 순으로 작품을 나열하는 것이 있다.
그렇다면 조회수가 중요해지는데 조회수는 조금만 생각해도 동시성 문제가 발생 할 수 있다.
쇼핑몰의 재고값을 다루는 동시성보다는 중요도가 떨어질수는 있어도 중요한 문제이기 때문에 그냥 대충 넘기는 것은 좋지 않다고 생각했다
그래서 이번에도 동시성 문제를 해결해보고자 한다.
우선 문제를 봐야한다 기본적인 동작이다.
work의 값을 가져오고 직접 그 값을 1 증가시킨 후에 save로 저장한다.
테스트
테스트를 진행하는 방법은 매우 간단하다. JMeter를 통해 1000번의 요청을 하는 것이다.
당연히 결과는 view가 1천번 올라가는 것이다.
에러가 없고 TPS는 4.8이라는 형편없는 결과를 내뱉었다
역시 동시성 문제가 발생하면서 겨우 106정도의 값만 증가했다
이유를 살펴보면 1천개의 요청이 비슷한 타이밍에 view를 read하고 증가하는데 1로 업데이트하고 저장하는데 다른 쓰레드도 이미 0으로 읽었기 때문에 굉장히 대부분의 쓰레드가 1로 업데이트 했다는 것이다
106이 된 이유는 어떤 쓰레드는 너무 늦게 읽어서 증가된 값을 읽었고 그것을 업데이트한 모습이다.
이를 해결하기 위해 방법을 찾아야 한다
💬 1번 해결책. 비관적 락 사용
역시 동시성 하면 DB의 Lock을 이용한 해결방식이 가장 먼저 떠오른다.
한번 했던것이라 그런지 더 친근하게 느껴진다.
그리고 낙관적 락을 사용하지 않는 이유는 역시 충돌할 가능성이 매우매우매우 많고 충돌한다면 오히려 안좋은 성능을 보일 것이 확실하기 때문이다.
코드를 작성하는것 자체는 매우 간단하다 상품을 가져오는 findById에 Lock을 거는 것이다.
하지만 이건 해결책이 아니라고 생각한다
비관적 락을 사용하려면 DB를 다루는 repository에서 특정 작품을 가져오는 곳에서 쿼리문을 통해 Lock을 다루게 되는데 상품을 가져오는것 하나만으로 작품 테이블 전체를 Lock으로 막아버리고 다른 쓰레드가 사용하지 못하도록 하는건 정말 별로라는 생각이 들었기 때문이다.
💤 테이블 추가
그렇다면 비관적 락을 아예 포기해야할까? 좀 다른 생각을 해보았다.
각 작품의 조회수에 관련된 테이블을 하나 만드는 것이다. 그리고 해당 테이블을 접하는 로직만 Lock을 걸어 해결하는 것이다
이건 굳이 로직으로 작성하지 않았다.
- 결국 작품을 가져오면서 Lock이 걸린다 로직이 그저 카운트를 올리는거라 괜찮겠지만 수많은 요청이 쌓이면 결과는 비슷할 것이라고 생각한다
- 조회수를 기준으로 검색하기 때문에 join쿼리가 필요해지면서 쿼리의 성능까지 저하되는것을 우려했다
위에 두개의 이유로 별로 도입하고 싶지 않아졌다.
💬 2번 해결책. @Modifying 사용하기
사실 조회수 올리는 것은 매우 간단한 쿼리이고 단순하다 그래서 간단한 쿼리를 repository에 붙여서 사용하면 된다.
이때 @Query를 통해서 직접 Update쿼리를 작성해 줄 것인데 JPA는 그냥 update, insert, delete와 같은 쿼리를 작성하면 알아듣지 못한다. 심지어 JPA는 Query를 모두 select로 인식한다.
그래서 @Modifying을 통해 인식하도록 하는데 변경이 일어나는 쿼리를 함께 사용해서 JPA에서 변경 감지와 관련된 처리를 생략하고 더 효율적인 실행이 가능하도록 하는 것이다.
즉, 벌크 연산이 가능하다
벌크 연산이란 Update, Delete, Insert와 같은 쿼리를 작성하여 대량의 데이터를 한번에 처리하도록 하는 작업이다
즉, JPA는 @Modifying과 같은 벌크 연산을 하도록 하는 것은 더티 체킹을 하지 않고 여러 데이터에 변경 쿼리를 직접 날리는 것이다.
그리고 @Modifying은 Transactional과 함께 사용되어야 한다
혹여 문제가 생기더라도 롤백 기능을 통해 다시 돌려야하는 것은 똑같다
역시나 쿼리만 보면 굉장히 간단하다.
Modifying이 이해가 가지 않는 사람들을 위해 예를 들어 설명해보자면
더티 체킹을 통해 변경
우리가 만약 그냥 데이터를 변경하면 1차 캐시에 값을 최초에 가져왔는데 그때 값이 0이라면 그것을 update할 것이다. 근데 그것이 1000개 중 대부분이 0을 1로 업데이트하는 더티체킹을 하게 되는 것이다. 그리고 로직이 끝나는 시점에서 DB에 실제로 반영된다는 것을 알 수 있다.
@Modifying을 통해 변경
하지만 Modifying을 사용하면 더티 체킹과 상관없이 update쿼리가 작성된다. 그럼 1천개의 update쿼리를 작성하게 되는 것이다. 읽는 시점과 무관하게 말이다.
그리고 커밋이 되는 시점에서는 view를 + 1씩 한다.
엥? 근데 잠깐 생각해보면 그럼 DB는? DB에서의 쿼리는 문제가 없나? 결국 DB의 update쿼리도 read라는 행위를 하긴 할텐데??
그것은 데이터의 트랜잭션은 원자적으로 작성되기 때문에 상관없다. 기본적으로 RDBMS는 특정 격리 수준으로 동시성을 제어한다. 데이터의 일관성을 위해 알아서 검색한 레코드의 Lock을 설정하고 동시성을 제어한다
🔩 테스트
비관적 락을 사용하는 것은 맘에 들지 않지만 그래도 테스트는 진행되어야 한다. 두개를 비교해보고 싶기 때문이다.
전체 ( 평균 응답 시간, TPS )
비관적 락
@Modifying
전체적인 능력치가 완전히 다르다는 것을 보여준다 동시성을 똑같이 해결해주지만 성능은 전혀 다르다는 것을 확인할 수 있다.
시간에 따른 응답 속도 - 그래프
비관적 락
@Modifying
TPS - 그래프
비관적 락
@Modifying
결과
결과는 물론 두 개다 안정적으로 해결된 모습을 볼 수 있었다
비관적 락
@Modifying
위의 그래프와 전체 표를 보면 알 수 있지만 훨씬 더 나은 성능은 @Modifying이 보여주는 것을 확인할 수 있다
그럼 @Modifying을 사용하면 되지 왜 굳이 비관적 락을 걸어서 사용하는 걸까?
@Modifying은 주의점이 있다.
JPA의 영속성 컨텍스트는 1차 캐시를 이용한다는 것을 들어본적이 있을 것이다. 거기에 한번 부른 엔티티를 저장하고 나중에 또 다시 사용할때 다시 사용함으로써 DB의 접근량을 줄이는데 벌크연산은 1차 캐시는 확인하지 않는다.
즉, 엔티티의 변경 사항 같은것은 신경쓰지 않는것이다.
만약 로직에서 더티체킹 같은 것으로 엔티티가 변경되었지만 아직 커밋되지 않아서 변경 감지 쿼리가 날아가지 않았다면 벌크 연산을 하는 Modifying은 문제가 발생 할 수 있다는 것이다.
그래서 신중히 사용하는 것이 좋다. 물론 Modifying을 덕지덕지 붙여서 사용할 수 있겠지만 그것이 마냥 좋아보이진 않고 심지어 다른 사람이 코드를 이해하려면 이곳저곳 돌아다니며 이해해야 한다
그래서 Modifying을 사용하는 경우를 확인해보면 간단한 업데이트를 자주 수행하는 경우 많이 사용한다.
반면에 비관적 락은 해당 데이터의 접근 자체를 막게되면서 여러가지 연산을 하고 한번에 커밋하면된다. 그래서 상황을 잘 고려하여 작성하면 더욱 좋은 성능 혹은 좋은 코드를 작성할 수 있을것이라고 생각한다.
최근에 동시성 문제를 해결하면서 동시성 = 비관적 락 or 분산 락을 활용하자고 생각했었는데 Modifying으로 해결하는 방법도 있다니 정말 대단하다...
'프로젝트 > 스위프 프로젝트(Lit Map)' 카테고리의 다른 글
🎋 쿼리 튜닝을 통한 응답 속도 개선 ( feat. QueryDSL, JPQL ) (0) | 2024.07.21 |
---|---|
🎋 최신 업데이트 된 작품들 가져오기 (Native Query의 UnionALL) (0) | 2024.07.19 |