이번 포스팅에서는, 일반적인 조회수 기능의 성능 향상을 고려해보는 시간을 갖도록 하겠습니다.
1. 조회수 기능 살펴보기
조회수는 어떤 글을 몇명의 사용자가 보았는지를 알려주는 것입니다. 방문자 수 또한 크게 보면 조회수와 같습니다.
언뜻보면, 조회수는 상당히 간단한 기능이기 때문에, 최적화가 필요할까? 성능향상이 필요할까? 싶기도합니다. 하지만 아주 작은 리소스를 소모하는 부분들일지라도, 잘못된 설계를 했고, 수많은 사용자들이 이용하다보면 매우 큰 문제로 이어지기도 합니다.
또한, 이렇게 작은 부분부터 하나 둘씩 성능 고려를 해두면, 나중에 정말 비즈니스상 크리티컬한 성능 문제가 야기 되었을때, 많은 도움이 되지 않을까요?
1.1 조회수 성능 최적화가 필요한 이유 및 필요한 부분
데이터베이스 잠금
첫번째로 데이터베이스 잠금입니다. Mysql을 예로 들자면, 별도 지정을 하지 않는 경우 스토리지 엔진으로 InnoDB
를 사용하게 되는데요, InnoDB는 UPDATE시, 조건(WHERE)절에 따라 인덱스를 잠그고 업데이트를 진행하게 됩니다. 당연히, 잠긴 상황에서 또 다른 조회 요청이 들어왔을때, 트랜잭션은 기다리게 됩니다.
게시판 입장에서 생각을 해본다면, 아래와 같습니다.
1번 회원
이1번 글 조회
요청을 합니다.- 조회수 UPDATE를 위해서,
1번 글
의 index가 잠깁니다. - 이때,
2번 회원
이1번 글 조회
요청을 거의 동시에 합니다. 하지만,1번 글
은 잠겨있으므로, 기다리게 됩니다. 1번 회원
에 의한 조회수 UPDATE 처리가 완료되고, 응답을 완료합니다.2번 회원
의 조회수 UPDATE 처리가 완료되고 응답을 완료합니다.
이 처럼, 글 조회시 조회수를 위한 UPDATE QUERY 때문에, 다른 사용자가 글을 조회하는 시간이 늦어질 수도 있습니다.
데이터베이스 QUERY CALL 수
보통의 커뮤니티
의 경우, 조회시 간단한 중복 검사를 하고 조회수를 증가시킵니다.
간단하게 처리한다면, 글 조회시 매번 글 조회 QUERY 1회 + 글 조회수 증가 QUERY 1회
총 2번의 QUERY가 DB로 전달됩니다.
조회수 기능이 없을때에 비해서, 쿼리가 2배 증가한 셈입니다. 트래픽이 적다면 상관 없겠지만, 시간당 1개의 글에 약 10000번
의 조회가 일어난다면, 하루 24시간 * 10000번 * 글의 개수
로 따진다면, 글의 개수가 1000개만되더라도, 2억 4천번의 추가 QUERY
가 발생하는 것입니다. (물론 트래픽이 몰리는 시간대가 따로 있긴합니다.)
사실 이렇게, 실시간 조회수가 중요하지 않다면, 조회수 UPDATE QUERY
를 글 조회시마다, 매번 같이 보낼 필요가 없을 수도 있습니다. 따라서 QUERY CALL수를 줄이기 위한 방법을 비즈니스를 고려해서 도모해 볼 수 있습니다.
1.2 성능 최적화를 위한 고려사항
사실 최적화 방법은, 비즈니스 측면에서도 고려가 필요합니다.
실시간 조회수가 중요한 경우
이를 테면, 커뮤니티
등의 경우는, 실시간 조회수에 민감한 경우들이 있습니다. 왜냐면 그 시간에 인기가 있는 글을 보여주어야 하기 때문입니다.
정확한 조회수가 중요한 경우
하지만, 유튜브
는 이러한 조회수가 수입
과 직결되기 때문에, 조회수를 증가시킬때, 여러 검증들(중복 조회, 특정시간 이상을 시청했는지 등)을 거치게 되고, 상대적으로 느리게 조회수 증가 처리가 되어집니다.
2. 성능 최적화
2.1 테이블 나누기 (X)
조회수를 위한 UPDATE 때문에 글의 INDEX가 잠기는게 문제라면, 조회수 필드
와 글 관련 필드
를 별도 테이블로 나누어 보는것은 어떨까요?
아쉽게도 이 방법은 다른 처리(글을 다 읽으시면 이해할 수 있습니다.)를 하지 않는다면, 결국 똑같이 성능저하가 일어납니다.
테이블을 나누어 처리한다고 했을때에도 결국, 글 조회와, 조회수 UPDATE가 모두 이루어져야 한 요청이 정상적으로 처리된것이기 때문에, 이를 하나의 트랜잭션에서 구현하게 됩니다. 결국 같은 일을 나누어 처리하는것 밖에 안되는것이죠. 오히려 성능이 느려질 것입니다.
Jmeter
를 이용해 테이블을 나누기 전과 후로, 100개의 스레드로 2000번의 요청 즉, 총 20만번의 요청을 해본 결과는 아래와 같습니다.
테이블 나누기전의 처리량입니다.
테이블을 나눈뒤의 처리량입니다. 오히려, 성능이 저하 되었음을 볼 수 있습니다.
2.2 Query를 모아서 처리해볼까
사실 실시간 조회수가 중요한 비즈니스가 아니라면, 조회수를 올리는 일은, 반드시 실시간으로 일어날 필요는 없습니다. 또한, 유튜브와 같이 수입과 직결되는것이 아니라면, 일부 조회수가 누락이 되더라도 실제로 크게 문제가 되지는 않습니다.
따라서, 글 조회시 이 글이 조회되었음을 메모리 어딘가에 기록해두고, 특정 시간마다, UPDATE 쿼리를 한번 전송하는 것으로 큰 성능향상을 도모해볼 수 있을것 같습니다.
xxxxxxxxxx
@Service
public class BoardService {
private final BoardRepository boardRepository;
private int tempHit;
public BoardDetail read(Long boardId) {
Board board = boardRepository.findById(boardId)
.get();
tempHit += 1;
if (tempHit > 10000) {
board.increaseViewCount(tempHit);
tempHit = 0;
}
return new BoardDetail(board.getBoardId(), board.getTitle(), board.getContent(), board.getHit());
}
위의 코드는, 테스트를 위해서, 글 조회 요청마다 tempHit를 증가시키고, 10000번 조회가 이루어지면 조회수를 업데이트 하도록 작성한 코드입니다.
실제 프로덕트에서는 위와 같이 코드를 짜면 안되고 시간마다 업데이트를 꼭 하도록 해야합니다. 인기가 없는 글의 경우, 10000번의 조회가 이루어지지 않는다면, 언제 조회수 업데이트가 이루어질지도 모르며, 위와 같이 코드를 짠다면, 조회수가 항상 10000단위로만 증가하게 되어버립니다. 또 Bean은 여러 스레드에서 공유되어 사용되므로 위와 같이 공유되는 멤버변수를 두면 안됩니다. 등등 여러 문제가 많은 코드이지만, 우선 테스트를위해 간단히 사용하도록 하겠습니다.
마찬가지로 Jmeter를 이용해 테스트를 진행합니다. 오! 확실히 처리량이 증가되었음을 확인할 수 있습니다.
CQRS
CQRS는 커맨드(데이터 변경 처리)와 쿼리(데이터 조회 처리)를 분리 시켜, 도메인의 복잡도를 줄이고, 각각의 성능향상을 도모할 수 있도록 해주는 하나의 프로그래밍 원칙입니다.
xxxxxxxxxx
public class BoardService {
...
public BoardDetail read(Long boardId) {
Board board = boardRepository.findById(boardId)
.get();
// 조회수 증가 이벤트 발행
rabbitTemplate.convertAndSend("amq.topic", "board.increaseHit", boardId);
return new BoardDetail(board.getBoardId(), board.getTitle(), board.getContent(), board.getHit());
}
}
위와 같이, 글 조회 요청이 들어온 경우 글 조회를 위한 데이터만을 반환하고, 글 조회 이벤트를 발행하는 것으로 처리를 끝냅니다. 이렇게하면, 조회수 증가를 위한 별도 처리 로직이 전혀 들어가지 않기 때문에, 코드가 더욱 명확해지고, 글 조회 처리만을 하기 때문에 성능향상을 도모할 수도 있습니다.
무엇보다 중요한 것은 게시글을 읽을 때, `조회 전용 세션으로 쿼리`를 보내기 때문에 조회수를 증가시키기 위해 테이블의 인덱스에 잠금이 걸리더라도, 조회시에는 잠금을 기다릴 필요가 없게 됩니다.
'Application Knowhow > Server' 카테고리의 다른 글
ApplicationKnowhow/Server - API 성능 개선기 2 (Query 개선) (0) | 2021.11.09 |
---|---|
ApplicationKnowhow/Server - API 성능 개선기 1 (JPA 성능 개선(kotlin)) (0) | 2021.11.09 |
ApplicationKnowhow/Server - 사용자 입력 값 검증방법과 노하우 (0) | 2020.07.01 |
ApplicationKnowhow/Server - 공인 IP없이 외부에서 접속 가능하게 만들기(Ngrok란?) (0) | 2020.06.15 |
ApplicationKnowhow/Server - Image와 Json을 함께 서버에 업로드하는 대표적 3가지 방법 (0) | 2020.04.16 |