JPA MySql Random 조회 (1) - order by rand()

JaeHun KimJaeHun Kim
3 min read

서문

요구사항 개발 중 데이터를 랜덤으로 조회할 필요가 생겼다.

DB는 MySql을 사용하고 있다. Mysql은 rand()를 통해 랜덤 기능을 제공하며 데이터를 랜덤하게 조회하기 위해선 공식문서에도 나와있듯이 SELECT * FROM tbl_name ORDER BY RAND(); 와 같이 사용하면 된다.

애플리케이션 서버에서 이를 어떻게 구현할 지가 고민이었고 Spring Boot 2.7.5, JPA, QueryDsl 을 사용중이다.

JPQL 작성

Mysql의 rand()는 jpql이나 hql에서 지원해주는 함수가 아니다. (Oracle 문서, Hibernate 문서)

하지만 Hibernate 문서에도 나와있듯이 각 DB의 Dialect가 각자의 네이티브 함수들을 많이 등록해놨다. logging.level.org.hibernate.HQL_FUNCTIONS=debug 로 로그 레벨을 설정하고 스프링을 실행시키면 rand()가 그 중에 하나임을 확인 할 수 있다.

구현

# Spring Data Jpa @Query 사용
@Query("select member from Member member order by rand()")
List<Member> getRandomMembers();

# Entity Manager 사용
List<Member> resultList = entityManager.createQuery(
                            "SELECT m " +
                                "FROM Member m " +
                                "ORDER BY rand()", Member.class)
                        .getResultList();

rand()를 직접 호출해주면 된다.

Hibernate 개발에 참여했던 Vlad Mihalcea도 블로그에서 이 방법을 언급했다. (링크)

QueryDsl

QueryDsl에서도 NumberExpressions.random() 을 통해 랜덤 함수 호출이 가능하지만 Mysql을 사용할 경우random()이란 함수를 찾을 수 없다고 예외가 던져진다. Querydsl 깃허브를 가보면 해당 이슈를 볼 수 있다. (링크)

의아했던 점은 SQLTemplates 에 이미 rand()가 정의되었고 MySQLTemplatesSQLTemplates 을 상속받고 있기 때문에 rand() 가 동작해야 할 것 같은데 그렇지 않다는 점이다.

// QueryDsl 코드 일부
package com.querydsl.jpa.impl;
public class JPAQueryFactory implements JPQLQueryFactory  {

    public JPAQueryFactory(final EntityManager entityManager) {
        this.entityManager = () -> entityManager;
        this.templates = null;
    }

    @Override
    public JPAQuery<?> query() {
        if (templates != null) {
            return new JPAQuery<Void>(entityManager.get(), templates);
        } else {
            return new JPAQuery<Void>(entityManager.get());
        }
    }
}

package com.querydsl.jpa.impl;
public class JPAQuery<T> extends AbstractJPAQuery<T, JPAQuery<T>> {

    public JPAQuery(EntityManager em) {
        super(em, JPAProvider.getTemplates(em), new DefaultQueryMetadata());
    }
}

package com.querydsl.jpa;
public class JPQLTemplates extends Templates {

}

이는 JPAQueryFactory를 생성할 때 JPQLTemplates을 파라미터로 넘기지 않으면 JPAProviderEntityManager를 통해 JPQLTemplates가 설정되기 때문이다.

JPQLTemplatesTemplates를 상속받고 있기 때문에 위에서 볼 수 있듯이 랜덤 함수가random()으로 설정되며 JPQLTemplates를 상속받은 여타 클래스들도 랜덤과 관련된 설정은 없기에 기본 설정이 유지된다. JPAQueryFactory은 JPQL을 위한 클래스이고 MySql 전용이 아니기에 어찌보면 당연한 것이라는 생각도 든다. 내 프로젝트에선 Hibernate5Templates가 사용되었다.

public class MySqlJpqlTemplates extends Hibernate5Templates {
    protected MySqlJpqlTemplates() {
        super();
        add(Ops.MathOps.RANDOM, "rand()");
    }
}

// 사용
QMember memberPath = new QMember("member");
List<Member> fetch1 = new JPASQLQuery<>(entityManager, MySQLTemplates.DEFAULT)
                .select(memberPath)
                .from(memberPath)
                .orderBy(NumberExpression.random().asc())
                .fetch();

List<Member> fetch2 = new JPAQueryFactory(new MySqlJpqlTemplates(), entityManager)
                .select(member)
                .from(member)
                .orderBy(NumberExpression.random().asc())
                .fetch();

List<Member> fetch3 = new JPAQuery<>(entityManager, new MySqlJpqlTemplates())
                .select(member)
                .from(member)
                .orderBy(NumberExpression.random().asc())
                .fetch();

// 생성 쿼리
Hibernate: select m1_0.id from member m1_0 order by rand()

JPAQueryFactoryJPAQuery 생성자 중에 JPQLTemplates를 받는 생성자가 있으므로 위와 같이 JPQLTemplates를 상속받아 새 클래스를 정의해 사용하면 rand()가 호출되는걸 확인할 수 있다. 혹은 JPASQLQueryMySQLTemplates.DEFAULT를 넘겨줘도 된다.

package com.querydsl.core.types.dsl;
public final class Expressions {
    // 생략
    public static <T extends Number & Comparable<?>> NumberTemplate<T> numberTemplate(Class<? extends T> cl,
            String template, Object... args) {
            return numberTemplate(cl, createTemplate(template), Arrays.asList(args));
        }
   // 생략
    private static Template createTemplate(String template) {
            return TemplateFactory.DEFAULT.create(template);
        }
   // 생략
}

// 사용
List<Member> members = repository.query(query -> query
                .select(member)
                .from(member)
                .orderBy(Expressions.numberTemplate(Double.class, "function('rand')").asc())
                .fetch());

아니면 더 간단하게 orderBy()

Expressions.numberTemplate(Double.class, "function('rand')") 를 넘겨 호출해도 된다.

NumberExpressions.random()와 뭐가 다르길래 다르게 동작할까

함수명에서도 알 수 있듯이 Expressions.numberTemplate()는 새 템플릿을 등록하는 함수다. 주석에도 Create a new Template expression 로 적혀있다. 내부 구현을 보면TemplateFactory.DEFAULT.create(template) 를 호출해 함수를 템플릿에 등록하는걸 알 수 있다.

원래 방언에 등록된 함수만 호출할 수 있는지 알았는데 Hibernate 6으로 오면서 변경된 것 같다고 한다.

(관련 링크)

order by rand()의 성능 이슈

Mysql의 order by rand()는 인덱스를 타지 않고 테이블 풀스캔을 한 후 랜덤으로 정렬을 하기 때문에 다량의 데이터를 다룰 때 성능 이슈가 있을 수 있다.

대량의 데이터가 아니라면 order by rand() 사용도 괜찮다고 생각한다.

현재의 요구사항은 소수의 데이터를 위한 임시적인 기능이니 해당 방식을 채택했지만 만약 대량의 데이터를 다뤄야 한다면 다른 방법을 찾아야 할 것이다.

다른 방법들은 추후 글을 통해 정리해볼 예정이다

13
Subscribe to my newsletter

Read articles from JaeHun Kim directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

JaeHun Kim
JaeHun Kim