티스토리 뷰

이번 포스팅에서는 좋아요 개수 조회 성능을 개선해보도록 하겠다. 모든 내용은 테코블 블로그를 참고하였습니다. 모든 코드는 깃허브에 있습니다. 

 

성능 개선 전

현재 상황은 질문글을 전체 조회할 때 발생하였다. 만약 질문글을 전체 조회할 때 아래와 같은 속성을 응답해야 된다는 요구사항이 왔다고 가정해보자. 

 

  • 질문글 PK
  • 질문글 상태
  • 제목
  • 카테고리
  • 댓글 개수
  • 좋아요 개수
  • 등록한 날짜

UI로 본다면 아래와 같은 모습이다. (예시 사진입니다.)

 

 

이 때, 각 질문글은 좋아요 개수를 가져오기 위해 좋아요 테이블과 조인하여 조회한다. 조인하는 이유는 단순히 좋아요만 누르는 것이 아닌 좋아요를 누르고 한번 더 누르면 삭제할 수 있어야 하기 때문이다.

 

각 질문글을 조회하는 쿼리는 다음과 같다. 나머지는 질문글 테이블에 있는 컬럼이다. 

 

 

이렇게 조인해서 가져올 때, 만약 100만개의 질문글을 전체조회 한다면 어떤 일이 벌어질까? 각 질문글 당 한번 조인하여 100만개가 각각 조인될 것이다. 이렇게 많은 사용자들이 질문글을 작성한다면 성능은 저하될 것이고 결국 속도가 느려질 것이다.

 

이 문제를 어떻게 해결해야 할까? 간단하다. 이 문제가 일어난 이유를 다시 한번 생각해보면, 좋아요 테이블과 조인을 한 것이 원인이다. 그렇다면 조인을 하지 않고 바로 개수를 알 수 있다면 성능을 개선할 수 있다.

 

성능 개선

먼저 질문글 Entity에 좋아요 개수 컬럼을 추가해주자. 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "QUESTION")
public class Question extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    ...

    private int likeCount;

    ...

    @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
    private final List<QuestionLike> questionLikes = new ArrayList<>();

    @Builder
    public Question(String title, String content, Category category) {
        this.title = title;
        this.content = content;
        this.category = category;
    }

    ...
    
}

 

그리고 좋아요 개수를 더하는 로직과 취소시키는 로직을 구현한다.

public void addLikeCount() {
        this.likeCount += 1;
    }

    public void downLikeCount() {
        this.likeCount -= 1;
    }

 

이제 간단하다. 요청했을 때, 이미 좋아요를 눌렀다면 취소시키는 로직을, 누르지 않았다면 더하는 로직을 호출하기만 하면 된다. 아래는 좋아요 로직의 일부다. 

public boolean like(Long id) {
        Question question = questionFacade.getQuestionById(id);
        
        ...
        
        if(questionLikeRepository.existsByQuestionAndUser(question, user)) {
            ...
            question.downLikeCount();
            return true;
        }
        QuestionLike questionLike = QuestionLike.builder()
                .question(question)
                .user(user)
                .build();

        question.addLikeCount();
        return true;
    }

 

위와 같이 수정한 후 질문글을 전체조회할 때 좋아요 개수 컬럼을 조회하면 끝이다.

 

기존 응답 DTO

@Getter
public class QuestionsResponseDto {
    private final Long id;
    private final QuestionStatus status;
    private final String title;
    private final Category category;
    private final int commentCount;
    private final int likeCount;
    private final LocalDateTime createdDate;

    @Builder
    public QuestionsResponseDto(Question question) {
        this.id = question.getId();
        this.status = question.getQuestionStatus();
        this.title = question.getTitle();
        this.category = question.getCategory();
        this.commentCount = question.getCommentsSize();
        this.likeCount = question.getQuestionLikes().size();
        this.createdDate = question.getCreatedDate();
    }
}

 

수정 후 응답 DTO

@Getter
public class QuestionsResponseDto {
    private final Long id;
    private final QuestionStatus status;
    private final String title;
    private final Category category;
    private final int commentCount;
    private final int likeCount;
    private final LocalDateTime createdDate;

    @Builder
    public QuestionsResponseDto(Question question) {
        this.id = question.getId();
        this.status = question.getQuestionStatus();
        this.title = question.getTitle();
        this.category = question.getCategory();
        this.commentCount = question.getCommentsSize();
        this.likeCount = question.getLikeCount();
        this.createdDate = question.getCreatedDate();
    }
}

 

조회쿼리를 보면 더 이상 좋아요 테이블과 조인하지 않는 것을 볼 수 있다. 

 

 

하지만 이렇게 직접 좋아요 개수를 올려주고 빼는 것이 과연 좋을까? 좋아요 테이블의 데이터와 질문글 테이블의 좋아요 개수 데이터가 같다는 보장이 있나? 없다. 즉 두 테이블에서 따로 수정이 이루어지기 때문에 데이터가 맞지 않을 수 도 있다.

 

예를 들어보겠다. 한 질문글이 있다. A 사용자는 질문글 좋아요를 눌렀다. 그럼 좋아요 테이블에 데이터가 추가되고, 질문글의 like_count 데이터도 +1 시켜주어야 한다. 이 때, 해당 트랜잭션이 커밋되기 전에 B 사용자가 좋아요를 누르면 어떻게 될까? like_count +1 된 데이터에 다시 +1이 되는 것이 아니라 기존 like_count 데이터에 +1이 된다. 

 

다시 말해서 2명의 사용자가 좋아요를 눌렀다면 좋아요 개수가 2개가 되어야 하지만 1개가 된다. 이를 갱실 분실(Lost Update)라고 한다. 자세한 내용은 테크불 내용을 참고하자. 이를 해결해주기 위한 방법이 바로 Sync Schedule이다.

 

Sync Schedule

다른말로 궁극적 일관성이라고 말하기도 한다. 궁극적 일관성이란, 시간이 지나서 최종적으로 같은 데이터로 동기화하는 것을 말한다. 질문글의 좋아요 개수와 좋아요 테이블의 데이터가 달라도, 특정 주기를 정해서 좋아요 개수를 맞춰주는 것이다. 특정 주기가 지나기 전까진 데이터가 맞지 않을 수 있지만, 좋아요 개수를 제한되게 보여준다면 실제로는 다르지만 다르게 보이지 않는다. 

 

예를 들어 페이스북의 좋아요는 1의자리 숫자까지 나타내지 않는다. 따라서 좋아요 테이블의 데이터는 25041이고 게시글의 좋아요 개수는 25038이라도 사용자 입장에서는 모르는 것이다. 

 

 

물론 좋아요를 많이 받지 않는다면, 운이 좋을 때 좋아요를 눌러도 오르지 않는 이상한 경험을 할 수 있다.

 

 

그럼 한번 구현해보자. 먼저 Application에 아래 어노테이션을 설정한다.

@EnableScheduling

 

그리고 새벽 4시가 될 때마다 동기화를 해준다. 

    private static final String FOUR_A_M_CORN = "0 0 4 * * *";
    
    ...
    
    @Scheduled(cron = FOUR_A_M_CORN)
    public void syncLike() {
        questionRepository.findAll()
                .forEach(this::syncQuestionLike);
    }

    private void syncQuestionLike(Question question) {
        int likeCount = questionLikeRepository.findByQuestion(question).size();
        question.syncLikeCount(likeCount);
    }

 

질문글을 다 가져온 후 해당 질문글과 연관되어 있는 좋아요 테이블의 데이터와 질문글의 좋아요 개수를 초기화시켜준다.

    public void syncLikeCount(int likeCount) {
        this.likeCount = likeCount;
    }

 

이렇게 해줌으로써 좋아요의 성능을 개선할 수 있다. 여기서 조금 더 성능을 개선시킬 수 도 있다. 아까 페이스북의 좋아요 예시를 들었는데, 페이스북 좋아요는 1000개 이상부터 1의자리 숫자까지 보여주는 것이 아닌 1천 이라고 나타낸다. 이를 코드에 적용시킨다면, 1000개 이상부터는 Sync Schedule 방법을 사용하고, 1000개 이하라면 좋아요 테이블과 조인하여 보여주는 것도 방법이다. 하지만 Sync Schedule방법이 정답인 것은 아니다. 상황에 따라서 적절한 해결방법을 적용시키는 것이 좋다. 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday