JPA에서의 랜덤 레코드 조회 방법

박양원·2024년 2월 21일
1

Trouble Shooting

목록 보기
9/16
post-thumbnail

해당 포스팅은 사이드 프로젝트 진행 중 겪은 크고 작은 이슈들에 대한 기록입니다.

읽기에 앞서

  • 프로젝트를 진행하는 도중 무작위 랜덤 조회 기능이 필요하여 어떤 방식으로 사용하는지에 대해 공부한 내용을 정리한 포스팅

SQL에서의 랜덤 조회 방식

  • 이 포스팅에서의 원하는 내용이 아니므로 간단하게 정리하고 넘어간다.
  • 현재 DB로 사용하고 있는 MariaDB의 경우, 랜덤으로 필드를 조회하는 쿼리는 아래와 같다.
  • 이는 인덱스를 사용하지 못하여 데이터가 방대해질 수록 성능의 저하가 심한 듯 하다.
# 참고 블로그 [https://creds.tistory.com/158]

# 랜덤 데이터 조회
SELECT * FROM [TABLE] ORDER BY RAND();

# 랜덤으로 N개의 데이터 조회
SELECT * FROM [TABLE] ORDER BY RAND() LIMIT 1;

# ORDER BY 2번 사용 시
# DESC를 먼저 수행 후, 중복 데이터 중 하나를 랜덤으로 뽑아서 사용
SELECT * FROM [TABLE] ORDER BY COLUMN1 DESC, RAND() LIMIT 1;

JPA에서는?

그렇다면 방법이 없을까..?

  • ORDER BY RAND()의 성능 이슈 문제도 있고, 네이티브 쿼리를 사용하고 싶지 않아 방법을 계속 찾던 중, 유사한 방식의 조회법이 있다는 것을 아래의 게시글을 통해 알게 되었다.
  • StackOverFlow 참고
  • 위의 게시글을 참고하면 JPA의 페이징 기법을 활용하여 마치 랜덤으로 조회하는 듯한 효과를 낼 수 있다!

페이징을 활용하여 랜덤 조회를 구현해보자

  • 위의 게시글은 JPA를 바탕으로 구현돠었으나, 난 프로젝트의 환경에 맞게 QueryDSL을 활용하여 필요한 기능을 구현하고자 한다.
  • (페이징에 관한 포스팅은 추후에 작성 예정)
  • 구현하고자 하는 기능 : 특정 범위 내의 7개 매장 정보를 랜덤으로 조회

Repository 구현체

/*
	특정 범위 상위 7개 조회
*/
@Override
public List<SimpleSearchStoreDTO> searchTop7Random(Polygon<G2D> polygon, Pageable pageable) {
    final int dist = 3;
    
    return queryFactory
                .select(Projections.constructor(SimpleSearchStoreDTO.class,
                        store.id, store.name, store.lon, store.lat, store.phoneNumber,
                        store.status, store.businessTime, store.address, store.ratingTotal, store.file.uploadImageUrl
                )).distinct()
                .from(store)
                .where(stContains(polygon), stDistance(polygon).loe(dist))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
}

/*
	특정 범위 개수 조회
*/
@Override
public Long countStoreInPolygon(Polygon<G2D> polygon) {
    return queryFactory
        .select(store.count())
        .from(store)
        .where(store.file.uploadImageUrl.isNotNull())
        .where(stContains(polygon), stDistance(polygon).loe(3))
        .fetchOne();
}
  • QueryDSL의 사용법은 별도로 설명하지 않는다.

Service

public List<SimpleSearchStoreDTO> searchTop7Random(double lat, double lon, double dist) {
    final int size = 7; 			// 한 페이지 당 데이터 개수
    
    /*
    	범위 생성
    */ 
    Polygon<G2D> polygon = getPolygon(lat, lon, dist);

    /*
    	특정 범위 개수 조회
    */ 
    Long count = storeRepository.countStoreInPolygon(polygon);

    /*
    	참고 블로그 - https://mine-it-record.tistory.com/141
    	Math.random()을 활용하여 난수를 생성
   		+1을 해주면 0 ~ (count / n)까지의 값이 선택됨
   		아니면 0 ~ (count / n) - 1까지의 값이 선택됨
    */
    int pageNum = count % n != 0
                    ? (int) (Math.random() * (count / size + 1))
                    : (int) (Math.random() * (count / size));

    /*
    	페이징 정보를 넘겨줌
    	idx쪽의 n개 데이터를 세팅함
    */
    PageRequest pageRequest = PageRequest.of(pageNum, size);

    return storeRepository.searchTop7Random(polygon, pageRequest);
}
  • 다소 헷갈릴 수 있는 부분이 있는데 바로 idx의 값의 연산이다.
  • count가 n으로 나누어 떨어지지 않는 경우, 나머지가 존재한다는 의미이다.
  • 나머지 데이터들 또한 하나의 페이지에 담아줘야 하므로 페이지가 하나 더 필요해진다. 따라서 +1을 하여 난수 생성 범위를 1 늘려준다.

어떤 방식인가?

  1. 전체 데이터의 개수를 구하고 이를 원하는 데이터 크기만큼 나누어 전체 페이지 개수를 구함
  2. Math.random()을 활용한 난수 생성을 이용하여 하나의 페이지 넘버를 구함
  3. 이를 PageRequest에 사이즈와 함께 담아서 넘겨줌

결론

  • 흡사 책을 들고 아무런 페이지나 열어보는 경우와 비슷합니다.
  • 데이터를 랜덤 조회한다기 보단 추출한 데이터들의 페이지를 랜덤 조회하는 것입니다..
  • 현재 제 프로젝트에선 완전한 랜덤 조회 기능은 필요없기에 위의 방식에 만족하고 알맞은 용도로 잘 사용하고 있으나, 100% 완벽하게 랜덤 데이터를 조회하는 방법이 아닙니다. 해당 방법을 구현하고 하는 경우라면 네이티브 쿼리나 jdbcTemplate을 사용해야 하는 것 같습니다.

0개의 댓글