문제 인식
커뮤니티 관련 서비스를 구현 중에, 다음과 같은 요구사항이 존재하였다.
"게시글 테이블이 존재하며, 클라이언트에서는 이를 최신순, 조회수 수, 좋아요 수 세가지 정렬 기준 중 하나를 통해 조회를 할 수 있습니다."
여기서 게시글을 최신순으로 조회하는 것은 간단하게 해결된다. 게시글 테이블에 updatedAt 칼럼을 추가하고, 데이터가 생성/수정 될 때마디 이를 업데이트 한 뒤, 이를 기준으로 데이터를 정렬하여 가져오면 된다.
조회수 수 / 좋아요 수의 경우 어떤식으로 데이터베이스를 설계해야 할까? 두가지 기능이 비슷하기 때문에, 조회수 기준으로 이를 어떻게 구현할지만 생각해보자.
우선 게시글의 조회수를 관리할 수 있는 테이블이 필요하다. 게시글 조회수를 관리하는 테이블에선 유저의 id 와 게시글의 id 를 복합키, 혹은 유니크한 필드 조합으로 관리하면 된다. 게시글의 조회수가 필요할 때는 이 테이블을 뒤지며 해당하는 게시글의 id 를 갖는 인스턴스 개수를 세면 된다.
순조롭다고 생각하며 게시글 목록을 조회하는 비지니스 로직을 구현하던 와중, 이러한 접근법에는 큰 문제가 존재한다는 것을 알아차렸다. 조회수 순으로 게시글을 조회하려면, 각 게시글의 조회수를 알아야 하는 것인데, 그렇다면 매 요청마다 모든 포스트의 조회수를 계산해야 한다는 것이다. 즉, 페이지네이션이 구현되어 20개의 게시글을 조회할 때에도, 1000개, 10000개의 게시글의 조회수를 매번 계산해야 할 것이다.
해결 방안
이를 개선하려면 글의 주제인 Denormalization 을 활용해야 하는 것이다.
Denormalization: Duplicate information is stored in multiple places, increasing data redundancy but making read queries simpler and faster.
간단히 말해, 여러 장소에 같은 값을 나타내는 정보가 존재하도록 두는 것이다. 부연 설명으로, 데이터 중복성은 증가하지만 읽기 쿼리는 더 간단하고 빨라진다고 써있다. 읽기 작업이 많고, 데이터 중복성이 크리티컬하지 않은 경우에 이를 적용하면 좋을 것 같다.
이를 적용하여, 게시글 조회수를 관리하는 테이블은 유지하되 (단순히 클릭에 따른 조회수라면 상관없겠지만, 유저 unique 한 조회수의 경우엔 필요하다), 게시글 테이블에 조회수 column 을 추가한다. 이렇게 되면, updatedAt 처럼 조회수 수 기준으로 빠르게 조회가 가능하다.
게시글 테이블에 조회수 수를 나타내는 viewCount 칼럼을 추가했다고 생각해보자. 이제 이 viewCount 값을 업데이트 해주어야 할텐데, 여기에도 여러가지 방법이 나뉘어 진다. 단순히 유저가 게시글을 클릭할 때마다 이 값을 올려주는 방법이 있을 것이다. 이경우 동시에 여러명의 유저가 게시글을 클릭했다면 race condition 이 발생하고, lock / unlock 메커니즘이 동작하느라 병목현상이 발생할 수 있다.
병목 현상을 어떻게 해결할 수 있을까?
배치 업데이트를 활용하는 방법이 존재한다. 유저가 게시글을 보자마자 조회수를 올리지는 않고, 일정한 주기로 게시글 조회수 테이블의 데이터를 확인하며 모든 게시글의 viewCount 를 올려주는 방식이다. 병목현상은 확실히 해결되겠지만, 반응성은 떨어질 것이다. 다만, 조회수는 반응성이 크게 중요하지 않아 괜찮을 수도 있을 것 같다.
다음으로, 메세지 큐를 활용하는 방법이 있다. 유저가 게시글을 클릭했을 때, 비동기 방식으로 유저에게는 바로 데이터를 돌려주고, 조회수를 업데이트 해야한다는 메세지를 큐에 넣어두는 것이다. 결론적으로 데이터베이스는 순차적으로 업데이트 되겠지만, 유저는 빠르게 게시글을 받아볼 수 있다. 조회수를 업데이트 하는 것이 게시글을 수정하는 더 중요한 기능을 막을 수도 있을 것 같지만, 이는 우선순위를 설정하는 방식으로 해결될 수 있지 않을까 싶다.
캐싱 시스템 도 한가지 방법이다. 유저가 게시글을 읽었을 때 데이터 베이스에 바로 값을 업데이트하는 것이 아니라, 캐시에 값을 업데이트 한다. 이후 주기적으로 또는 특정 트리거에 따라 캐시 카운트로 데이터베이스를 업데이트 한다. 배치 업데이트 방식과 다소 유사하지만, 여러 커넥션에서 캐시값을 함께 공유하여 사용하며 매번 데이터베이스를 건들지 않기 때문에 보다 효율적일 수 있다.
결론
결론적으로 Denormalization 을 활용하되, 단순성/복잡성, 트래픽의 양, 해당 요청의 읽기/쓰기 비율에 따라 적절한 방법을 활용하면 될 것 같다. 현재 진행중인 프로젝트에선 예상되는 초기 유저가 많지 않기 때문에, 우선은 단순히 게시글을 조회할 때마다 viewCount 값을 올려주는 방법을 활용해도 될 것 같다.
추가적으로, 진행하고 있는 프로젝트는 node.js 기반, 싱글 스레드로 동작하기 때문에 transaction 만 잘 활용한다면 race condition 이 크게 문제가 되지 않을 수는 있을 것 같다. 하지만, 서버를 여러개 띄울 수도 있기 때문에 이에 대한 고려는 해두는게 좋은 것 같다.