Question 클래스는 질문 게시글에 대한 엔티티 클래스이다.
프런트의 요청으로 조회수를 추가할 일이 생겼고, 우선 다음과 같이 조회수 필드를 추가했다.
Question.java
public class Question extends BaseTimeEntity {
//다른 필드
@ColumnDefault("0L")
private long viewCount;
// 다른 필드
}
고려해 볼 만한 사항
- 우선 래퍼 클래스로 선언할지, 기본 타입으로 선언할지 고민했으나, 아무래도 조회수의 경우 기본값이 null이면 안된다고 생각해서
기본 타입으로 선언했다. - 동시성 문제가 발생할 여지가 있다. 조회수 증가 메서드는 게시글 조회할 때 실행되는 메서드이므로, 같은 시점에 같은 게시글에 대한 요청이 들어올 경우, 만약 특정 게시글의 대한 조회수가 0이라고 하면, 첫 번째 요청이 1, 두 번째 요청이 1에서 2로 올라가는 순차적으로 증가가 되어야 한다.
문제 확인하기
우선, 동시성 문제가 없다고 가정해서 엔티티에 메서드를 다음과 같이 만들었다.
public void increaseViewCount() {
viewCount++;
}
이제, JMeter로 테스트해 보자.
우선, 스레드 요청을 500개로 늘려서 테스트를 해보자.

그리고
다음과 같은 경로로 요청을 보낸다

우선, 동시성 문제가 없다고 가정하면 id가 14인 문제는 조회수가 500이 되어야 한다.
결과를 확인해 보면,

viewCount의 조회수가 500을 기대했으나 323밖에 오르지 않았다는 것을 확인할 수 있었다.
아마 JPA의 트랜잭션 내에서 읽기 -> 쓰기 -> 변경 감지의 순서로 흘러가기 때문에, 여러 트랜잭션이 동시에 읽기를 하여 동시에 쓰기를 했기 때문인 것 같다.
이제, 문제를 확인했으니 해결하기 위해 로직을 리팩토링 해보자.
리팩토링
요청 시 조회수를 1만큼 증가하는 간단한 로직이기 때문에, 쿼리문으로 해결할 수 있다.
Repository에 다음과 같은 메서드를 추가한다.
@Modifying
@Query("UPDATE Question q SET q.viewCount = q.viewCount + 1 WHERE q.id = :questionId")
void increaseViewCount(@Param("questionId") Long questionId);
인자로 받은 questionId인 문제를 조회해서 조회수 칼럼을 1만큼 증가하는 쿼리문이다.
@Modifying : JpaRepository는 C, U , D의 경우 해당 어노테이션을 붙이지 않으면 에러가 발생한다.
기본적으로 RDBMS는 특정 격리 수준으로 동시성을 제어한다.
또한, 탐색에 사용된 인덱스가 Lock 걸릴 레코드를 결정한다.
위 쿼리는 PK로 탐색했기 때문에 단 하나의 레코드만 Lock 된다.
Service 코드는 다음과 같다.
@Transactional
public void increaseViewCount(Long questionId) {
if(!questionRepository.existsById(questionId)) {
throw new EntityNotFoundException();
}
questionRepository.increaseViewCount(questionId);
}
위와 같이 수정한 후 다시 테스트를 해보면

성공적으로 0인 조회수의 문제가 500으로 증가함을 확인할 수 있었다.
비관적 락 사용하기
DB에서 비관적 락으로 해결할 수도 있다.
위 엔티티 메서드 view에 다음과 같이 작성하고,
public void view(Long id) {
Question questions = questionRepository.findByIdForUpdate(id)
.orElseThrow(RuntimeException::new);
question.increaseViewCount();
}
questionRepository의 findByIdForUpdate에 @Lock 어노테이션을 선언한다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT q FROM Question q WHERE q.id = :questionId")
Optional<Question> findByIdForUpdate(@Param("questionId")Long questionId);
결과를 확인해 보면,

500번의 동시 요청에 대해 500만큼 조회수가 올랐다.
비관적 락이 성능에 미치는 영향
비관적 락은 성능이 저하되는 문제가 있기 때문에, 과한 옵션일 수도 있다.
비관적 락을 사용하여 동시성 문제를 해결했을때

UPDATE 문을 이용해서 동시성 문제를 해결했을 때

비관적 락이 UPDATE 문보다 느린 이유는 아래와 같다.
- UPDATE 문에서는 existsById()를 사용
- 비관적 Lock은 배타락을 사용하여 다른 트랜잭션의 읽기 자체를 막는다
'🍃Spring' 카테고리의 다른 글
| [Redis] Redis 캐시를 사용한 JWT 리프레시 토큰 관리하기 (0) | 2023.04.10 |
|---|---|
| [Spring Security] CORS 문제 해결하기 (0) | 2023.04.01 |
| [Spring] Spring AOP를 이용한 권한 체크 (0) | 2023.02.21 |
| [Spring] Business Exception 처리하기 (0) | 2023.02.21 |
| [Spring Boot] 스프링 부트 3 이상에서 Springdocs swagger 적용하기 (0) | 2023.02.19 |