본문 바로가기

카테고리 없음

목록 페이징 조회 시 좋아요 수를 보여주는 구현 전략

1. 문제 상황

미니홈 목록 조회 시 각 미니홈의 좋아요 수를 함께 보여줘야 한다. 따라서 미니홈 테이블과 좋아요 테이블 간 조인이 발생한다.

 

또한 좋아요 순으로 정렬 조회할 경우, 미니홈 테이블과 좋아요 테이블의 모든 데이터에 대해 조인, 그룹화, 정렬이 발생한다. 다음 코드는 좋아요 순 정렬 조회 코드이다.

@Repository
@RequiredArgsConstructor
public class MinihomeEntityRepository implements MinihomeRepository {

    private final MinihomeJpaRepository minihomeJpaRepository;
    private final LikeJpaRepository likeJpaRepository;

    @Override
    public Slice<Minihome> findAllByLikeCount(Pageable pageable) {
        return minihomeJpaRepository.findOrderByLikeCount(pageable)
                .map(minihomeEntity -> {
                    Long likeCount = likeJpaRepository.countByMinihomeId(minihomeEntity.getId());
                    return new Minihome(minihomeEntity.getId(), minihomeEntity.getUserId(), minihomeEntity.getTotalVisitorCnt(), likeCount);
                });
    }
}
@Repository
public interface MinihomeJpaRepository extends JpaRepository<MinihomeEntity, Long> {

    @Query("select m from MinihomeEntity m " +
            "left join LikeEntity l on l.minihomeId = m.id " +
            "group by m " +
            "order by count(l) desc")
    Slice<MinihomeEntity> findOrderByLikeCount(Pageable pageable);
}

 

이로 인해 다수의 조회 요청 시 성능 저하가 우려되어 이 부분을 개선하고자 한다.

2. 해결 방안

2-1. 미니홈 테이블에 좋아요 수 필드 추가

첫 번째로 미니홈 테이블에 좋아요 수 필드를 추가하는 방법을 생각했다. 미니홈 테이블에 좋아요 수 필드를 추가하여, 좋아요 순 정렬 조회 시 조인이 발생하지 않고 Pageable 객체를 통한 자동 페이징 처리가 가능하다.


그러나 좋아요 수 외로 다양한 요구사항이 추가될 때마다 미니홈 테이블에 해당 데이터 필드를 추가하는 것은 복잡성 증대된다. 그리고 미니홈이라는 데이터가 좋아요 수에 대해 반드시 알아야 하는가? 라고 생각하면 그렇지 않다고 생각한다. 좋아요 데이터는 미니홈 id와 같이 미니홈에 대해 알고 있어야 하지만, 미니홈은 자신의 좋아요 수에 대해 굳이 알고 있을 필요가 없다고 생각했다. 따라서 해당 방식은 적용하지 않았다.

2-2. 좋아요 수를 별도의 메타 데이터로 관리

두 번째 방법으로는 좋아요 수를 별도의 메타 데이터로 관리하는 방법이다. 즉, 좋아요 요청 처리시 다음과 같이 likes 테이블에 INSERT하는 것과 동시에, 비동기로 미니홈 메타 데이터 테이블의 좋아요 수 필드도 1 증가시키는 것이다.

@Service
@RequiredArgsConstructor
public class MinihomeService {

    private final UserRepository userRepository;
    private final LikeRepository likeRepository;

    public void like(User minihomeUser, User currentUser) {
        Minihome minihome = minihomeRepository.findByUserId(minihomeUser.getId());
        Optional<Like> optionalLike = likeRepository.findByMinihomeIdAndAndUserId(minihome.getId(), currentUser.getId());
        if (optionalLike.isEmpty()) {
            likeRepository.save(new Like(null, minihome.getId(), currentUser.getId()));
            minihomeRepository.increaseLikeCount(minihome.getId());
        } else {
            likeRepository.delete(optionalLike.get());
            minihomeRepository.decreaseLikeCount(minihome.getId());
        }
    }
@Repository
@RequiredArgsConstructor
public class MinihomeEntityRepository implements MinihomeRepository {

    private final MinihomeJpaRepository minihomeJpaRepository;
    private final MinihomeMetaJpaRepository minihomeMetaJpaRepository;

    @Override
    @Async
    @Transactional
    public void increaseLikeCount(Long minihomeId) {
        minihomeMetaJpaRepository.increaseLikeCount(minihomeId);
    }

    @Override
    @Async
    @Transactional
    public void decreaseLikeCount(Long minihomeId) {
        minihomeMetaJpaRepository.decreaseLikeCount(minihomeId);
    }
}

 

이때 메타 데이터의 좋아요 수를 1 증감시키는 것은 다음 쿼리를 통해 실행되므로 원자성이 보장된다. 따라서 메타 데이터의 좋아요 수 필드에 대해 경합이 발생하지 않는다.

@Repository
public interface MinihomeMetaJpaRepository extends JpaRepository<MinihomeMetaEntity, Long> {

    @Modifying
    @Query("update MinihomeMetaEntity m " +
            "set m.likeCount = m.likeCount + 1 " +
            "where m.id = :id")
    void increaseLikeCount(@Param("id") Long minihomeId);

    @Modifying
    @Query("update MinihomeMetaEntity m " +
            "set m.likeCount = m.likeCount - 1 " +
            "where m.id = :id")
    void decreaseLikeCount(@Param("id") Long minihomeId);
}

 

그리고 미니홈 좋아요 순 페이징 조회 시 메타데이터를 통해 조회한다.

@Repository
@RequiredArgsConstructor
public class MinihomeEntityRepository implements MinihomeRepository {

    private final MinihomeJpaRepository minihomeJpaRepository;
    private final MinihomeMetaJpaRepository minihomeMetaJpaRepository;
    private final LikeJpaRepository likeJpaRepository;
    
    @Override
    public Slice<Minihome> findAllByLikeCount(Pageable pageable) {
        return minihomeMetaJpaRepository.findAllBy(pageable)
                .map(minihomeMetaEntity -> {
                    MinihomeEntity minihomeEntity = minihomeJpaRepository.findById(minihomeMetaEntity.getMinihomeId())
                            .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND_MINIHOME));
                    return new Minihome(minihomeEntity.getId(), minihomeEntity.getUserId(), minihomeEntity.getTotalVisitorCnt(), minihomeMetaEntity.getLikeCount());
                });
    }
}

3. 결과

1,000개의 미니홈 데이터와 10,000개의 좋아요 데이터 삽입 후, JMeter를 통해 좋아요 순 미니홈 페이징 조회 API를 1,000번 호출하여 테스트를 진행해보았다. 테스트 결과, 기존 방식은 Throughput이 31.9/sec이 나왔고, 메타 데이터를 적용한 방식은 119.8/sec이 나오며 더 빠른 성능을 보였다.

기존 방식(조인+그룹화+정렬 발생)
메타 데이터를 통해 미니홈 좋아요 순 정렬 조회