저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

조회수, 좋아요. 이 두 값은 전형적인 카운트이다. 글을 읽거나 좋아요 버튼을 누를 때 마다 값이 1씩 증가하는 특징을 갖는다. 예를 들어, 게시글의 경우 다음과 같이 '조회' 수와 '좋아요' 수를 갖는데, 이들 값은 사용자가 글을 조회하거나 '좋아요'를 할 때 마다 1씩 증가하게 된다.


public class Article {

    ...

    private int viewCount;

    private int likeCount;

    ...

}


카운트와 캐시


이제 서비스에 있어서 캐시는 기본이 된 듯 하다. 급증하는 트래픽을 장비로만 막는데는 한계가 있기 때문에, 캐시는 반드시 고려해야 하는 대상이다. 이 캐시를 적용할 때 개발자를 짜증나게 만드는 것이 있는데, 그것은 바로 카운트 방식의 값이다.


예를 들어, 게시판에서 하나의 게시글을 Article 이란 모델로 표현했다고 할 경우, 게시글은 읽기만 해도 증가하기 때문에 Article 및 Article의 List를 담고 있는 캐시는 글의 조회수가 증가될 때마다 캐시에서 제거되어야 한다. (또는 백엔드에서 다시 값을 읽어와 캐시에 넣어야 한다.) 조회수 증가가 크지 않은 사이트는 이렇게 하더라도 전혀 문제가 되지 않겠지만, 캐시를 도입하는 이유는 트래픽이 많아졌기 때문이다. 그런데, 트래픽이 많아졌다는 것은 특정 게시글에 대한 조회 요청이 많다는 것을 의미하고, 이는 조회수의 증가가 발생한다는 것을 의미한다. 조회수의 증가는 캐시에 보관된 게시글 정보가 더 이상 유효하지 않다는 것을 의미하므로 캐시에서 게시글 정보 또는 관련 게시글 목록 정보가 캐시에서 제거된다는 것을 뜻한다. (조회수에 민감하지 않다면 캐시에 보관된 데이터를 일정 시간 동안 사용할 수 있겠지만, 실제로 고객들은 조회수, 좋아요 등의 숫자에 매우 민감하기 때문에 제대로 반영해 주어야한다.) 캐시에서 제거되었으므로 결국 DB에서 다시 읽어와 캐시에 담게 된다. 하지만, 조회수가 증가하기 때문에 결국 다시 캐시에서 제거되고 DB에서 읽어와 캐시에 담는 과정이 반복될 것이다.


트래픽이 증가해서 캐시를 적용했는데, 카운트 증가 때문에 캐시 효과가 없어지는 어처구니 없는 상황이 발생하는 것이다. 사실, 게시글의 내용과 카운트 값은 변경의 빈번도가 완전히 다르다. 제목과 같은 내용은 변화 빈도가 낮은 반면에 카운트 값은 특정 기간 동안에 빈번도가 굉장히 높다.


카운트의 분리


따라서, 특정 컨텐츠에 대한 캐시 효과를 극대화하려면 카운트 류의 값을 별도로 분리해주어야 한다고 생각한다. 예를 들어, 게시글의 경우 다음과 같이 두 개의 모델로 분리한다.



즉, 내용을 담고 있는 Article과 해당 Article의 카운트 값을 갖는 ArticleCount로 구분할 수 있을 것이다. ArticleCount의 id는 Article과 동일한 id를 가질 것이다.


Article과 ArticleCount가 분리되었으니, 이 두 객체를 다루는 서비스도 분리된다.


public interface ArticleService {

    public Article getArticle(Long id);

}


public interface ArticleCountService {

    public ArticleCount getArticleCount(Long id);

    public void increaseViewCount(Long id);

}


조립하기


완전한 게시글 읽기 기능은 Article과 ArticleCount를 필요로 하므로, 다음과 같이 두 서비스를 사용해서 구현한다.


public class ReadArticleService {

    public ArticleDto readArticle(Long id) {

        Article article = articleService.getArticle(id);

        articleCountService.increaseViewCount(article.getId());

        ArticleCount count = articleCountService.getArticleCount(article.getId());

        return new ArticleDto(article, count);

    }

    ...

}


public class ArticleDto {

    private Article article;

    private ArticleCount count;


    public ArticleDto(Article article, ArticleCount count) {

        this.article = article;

        this.count = count;

    }


    public Long getTitle() {

        return article.getTitle();

    }

    public int getViewCount() {

        return count.getViewCount();

    }

    ...

}


AritcleDto는 Article과 ArticleCount로부터 값을 읽어오는 getTitle(), getViewCount() 등의 메서드를 제공한다.


캐시 분리 적용


이제 ArticleService의 getArticle()에 캐시 기능을 적용하자. 


public class ReadArticleService {

    public ArticleDto readArticle(Long id) {

        Article article = articleService.getArticle(id); // 캐시에서 읽어 옴

        articleCountService.increaseViewCount(article.getId()); // DB에서 반영

        ArticleCount count = articleCountService.getArticleCount(article.getId()); // DB에서 읽어 옴

        return new ArticleDto(article, count);

    }


ArticleService.getArticle()은 이제 캐시에서 Article 객체를 가져온다. 반면 ArticleCount는 항상 DB에서 가져온다. 이제 조회수와 같은 카운트 값이 빈번하게 변경하더라도 Article 객체가 캐시에서 제거되지 않으므로, Article에 대한 캐시 히트율이 높아질 것이다.


지금까지는 그냥 생각일 뿐, 실제로 조회수가 급격하게 증가하는 상황에서 성능이 어떻게 될지는 아직 테스트 해 보지 못했다. 이건 나중에 기회가 되면 한 번 해 보기로 하자.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. bluepoet 2012.11.29 16:12 신고  댓글주소  수정/삭제  댓글쓰기

    색다른 아이디어의 글 잘 읽었습니다.

    카운트에 대한 객체를 따로 빼내고, 글 정보 자체를 캐시한다음

    해당 글의 카운트 정보만 업데이트하고 그 정보만 DB에서 읽어온다면

    DB 인덱스 설계에 따라 달라지긴 하겠지만, Article에 대한 글정보를 업데이트하고

    가져오는 것보단 비용이 덜하지 않을까 생각됩니다.

    다만, 리스트를 뿌릴 때 Article 하나에서 가져오는 것과

    Article과 ArticleCount를 조인해서 가져오는 것과의 비용은 잘 따져서

    선택해야 될 것 같습니다.

    스트레스 테스트 하시면 결과도 공유해주시면 좋을 것 같네요