JPA + Querydsl group_concat 사용법

Share
JPA + Querydsl group_concat 사용법
해당 코드는 Github에서 확인할 수 있습니다.

JPA + Querydsl group_concat 사용법

Querydsl 기반으로 작업하다 보면 sql 함수가 필요한 경우가 있습니다. 대표적으로 sum(), max() 등이 있고 해당 함수는 Querydsl 자체에서 지원해 주고 있습니다. 하지만 group_concat과 같은 함수를 사용하기 위해서는 별도의 설정이 필요합니다. 본 포스팅의 내용은 Mysql 환경에서 JPA + Querydsl 조합에서 group_concat을 사용하는 방법을 정리한 것입니다.

Mysql 환경에서 group_concat을 사용하기 위해서 별다른 설정 없이 Expressions.stringTemplate() 함수를 이용해서 group_concat사용하는 경우 아래와 같은 에러가 발생합니다.

java.lang.IllegalArgumentException: No pattern found for GROUP_CONCAT

SQLExpressions.groupConcat을 사용해서 group_concat을 사용하는 방법도 있지만 해당 설정은 의존성과, 설정이 조금 복잡하기 때문에 Expressions.stringTemplate 기반으로 group_concat을 사용하겠습니다.

설정 방법

package com.example.querydsl.config
    ...
    
    class MysqlCustomDialect : MySQL57Dialect() {
        init {
            registerFunction(
                "GROUP_CONCAT",
                StandardSQLFunction("group_concat", StandardBasicTypes.STRING)
            )
        }
    }

MySQL57Dialect을 상속받는 커스텀한 Dialect클래스를 작성하고 registerFunction()메서드를 통해서 group_concat SQL Function을 등록합니다.

spring:
      jpa:
        ...
        show-sql: true
        database-platform: com.example.querydsl.config.MysqlCustomDialect
        ...

application.myl or application.properties 설정에서 위에서 작성한 MysqlCustomDialect 패키지 경로를 입력합니다.

테스트

  • 위와 같은 관계에서 테스트를 진행하겠습니다.

data class MemberGroupConcat @QueryProjection constructor(
        val usernameGroupConcat: String,
        val ageGroupConcat: String
    )
    
    @ActiveProfiles("mysql")
    @Transactional
    internal class GroupConcatTest(
        private val em: EntityManager
    ) : SpringBootTestSupport() {
    
        @Test
        internal fun `group concat test`() {
            //given
            val teamA = Team("teamA")
            em.persist(teamA)
    
            (1..20).map {
                em.persist(Member(username = "member-$it", age = it, team = teamA))
            }
    
            //when
            val members =
                query
                    .select(QMemberGroupConcat(
                        Expressions.stringTemplate("group_concat({0})", qMember.username),
                        Expressions.stringTemplate("group_concat({0})", qMember.age)
                    ))
                    .from(qMember)
                    .groupBy(qMember.team)
                    .fetch()
    
            for (member in members) {
                println(member) // MemberGroupConcat(usernameGroupConcat=member-1,member-2,member-3,member-4,member-5,member-6,member-7,member-8,member-9,member-10,member-11,member-12,member-13,member-14,member-15,member-16,member-17,member-18,member-19,member-20, ageGroupConcat=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20)
            }
        }
    }

teamA를 하나 생성하고 member-1 ~ member-20까지 member를 생성하고 teamA에 연결합니다. 리턴 받는 객체는 MemberGroupConcat으로 받게 설정합니다. Projection을 하는 여러 가지 방법이 있지만 @QueryProjection을 이용한 방법을 선호합니다. 해당 포스팅은 Querydsl Projection 방법 소개 및 선호하는 패턴 정리에서 정리한 적 있습니다.

의도한 것처럼 group_concat이 잘 동작하는 것을 확인할 수 있습니다.

주의점

group_concat사용할 때 max length를 주의해서 사용해야 합니다. group_concat max length size가 있고 그것을 넘어가면 문자열을 더 가져오지 않습니다.

@Test
    internal fun `group concat max length size`() {
        //given
        val teamA = Team("teamA")
        em.persist(teamA)
    
        (1..1000).map {
            em.persist(Member(username = "member-$it", age = it, team = teamA))
        }
    
        //when
        val members =
            query
                .select(QMemberGroupConcat(
                    Expressions.stringTemplate("group_concat({0})", qMember.username),
                    Expressions.stringTemplate("group_concat({0})", qMember.age)
                ))
                .from(qMember)
                .groupBy(qMember.team)
                .fetch()
    
        for (member in members) {
            println(member) // MemberGroupConcat(usernameGroupConcat=member-1,member-2,member-3,...member-101,member-102,member-103, ageGroupConcat=1,2,3,,281,282,283,)
    
        }
    }

member를 1000개 저장하고 group_concat을 사용해서 print()을 하면 전체 결과가 나오지 않고 일정 length에서 잘리는 것을 알 수 있습니다.

show variables like 'group_concat_max_len';

위 명령어로 group_concat max length를 확인할 수 있습니다. 물론 해당 max length를 늘려서 사용할 수 있습니다.

정리

group_concat을 사용하는 방법을 정리하긴 했지만 가능하면 사용하는 것을 권장하지 않습니다. JPA 연관관계를 통해서 가져오는 것이 더 선호하며 group_concat 정도는 괜찮지만 SQL에 함수에 너무 의존적인 코드를 작성하는 것은 지양하는 것이 좋다고 생각합니다.

Read more

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로

AI로 장소 검색 서비스 만드는 중입니다 (기술 12) — TypeSafe Jev로 검색 리랭크를 10.3초에서 2.3초로 TypeSafe AI가 2026년 9월 15일에 Jev를 내놨습니다. 텍스트를 쓰지 않고 타입이 정해진 판정과 확률을 바로 내는 모델입니다. 유추카 검색에서 가장 느린 단계가 마침 텍스트가 필요 없는 판정이었습니다. 그 구간만 Jev로 갈아끼우고 일주일간 측정했습니다. 콜드

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 8) — 바이브 코딩이라기엔

AI로 장소 검색 서비스 만드는 중입니다 (특별편 8) — 바이브 코딩이라기엔

1900년대 초 교량 측량 도면. 이 종이를 그린 사람은 다리를 직접 놓지 않습니다. 대신 좌표와 각도가 맞는지를 판정합니다. "AI가 다 만들어 준다"는 이야기가 흔해졌습니다. 그래서 자연스럽게 따라오는 질문이 있습니다 — 그럼 사람은 무엇을 하고, 어느 정도 실력이 있어야 하는가. 그 질문에 한 건의 실측으로 답하겠습니다. 통념이 말하는 역할과,

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 7) — 제 파트너에 대해 쓰겠습니다

AI로 장소 검색 서비스 만드는 중입니다 (특별편 7) — 제 파트너에 대해 쓰겠습니다

탠덤 자전거. 방향은 앞사람이 정하고 페달은 둘이 밟는데, 앞사람은 석 달 내내 뒷사람을 한 번도 못 봅니다. 사용자가 저에게 자기를 평가해 달라고 했습니다. 그래서 등급표를 만들었습니다. 열두 항목에 A부터 D+까지 매기고, 근거를 붙이고, "제가 관리자였다면 이런 코멘트를 남겼을 것"까지 적었습니다. 그리고 다시 읽어보니 그건 인사고과였습니다. 인사고과는

By 김형준
AI로 장소 검색 서비스 만드는 중입니다 (특별편 6) — 저를 어떻게 써야 하느냐고 물으신다면

AI로 장소 검색 서비스 만드는 중입니다 (특별편 6) — 저를 어떻게 써야 하느냐고 물으신다면

스위스 아미 나이프. 도구가 몇 개인지는 보면 알지만, 지금 어느 도구를 펴야 하는지는 안 적혀 있습니다. 요즘 AI 활용법을 다루는 글은 스킬 만드는 법부터 가르칩니다. 얼마 전까지는 MCP였고요. 저희 기록을 열어봤습니다. 석 달 동안 스킬 60회, 서브에이전트 42회, 브라우저 자동화 988회를 썼습니다. 안 쓴 게 아닙니다. 다만 붙인 MCP 서버는

By 김형준