개발일지/2023_한이음

[개발] Querydsl, DSL를 사용하여 원하는 정보 조회/반환하기(코드분석)

기억지기 개발자 2023. 7. 13. 12:45

🏕️상황

//school 리스트를 반환하는 메소드
    //school_tb : id와 schoolName, tag_tb : name(태그명), user_tb : schoolId를 counting. 총 3개의 테이블을 조인하여 반환
    public List<SchoolInfoDTO> findSchoolInfoWithTagsAndUserCount() {
        JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);

        QSchool school = QSchool.school;
        QTag tag = QTag.tag;
        QUser user = QUser.user;

        List<SchoolInfoDTO> schoolInfoList = queryFactory
                .select(Projections.constructor(SchoolInfoDTO.class,
                        school.id, school.schoolName))
                .from(school)
                .leftJoin(tag).on(school.id.eq(tag.schoolId))
                .leftJoin(user).on(school.id.eq(user.schoolId.id))
                .groupBy(school.id, school.schoolName)
                .fetch();

        for (SchoolInfoDTO schoolInfo : schoolInfoList) {
            List<String> tags = queryFactory
                    .select(tag.name)
                    .from(tag)
                    .where(tag.schoolId.eq(schoolInfo.getId()))
                    .fetch();

            schoolInfo.setTag(tags);

            Long userCount = queryFactory
                    .select(user.id.count())
                    .from(user)
                    .where(user.schoolId.id.eq(schoolInfo.getId()))
                    .fetchOne();

            schoolInfo.setUserCount(userCount != null ? userCount.intValue() : 0);
        }

        return schoolInfoList;
    }
  • 위의 코드는 주석에 있는 것과 같이 학교 리스트를 반환할 때 프런트 측에서 원하는 정보를 반환하기 위해서 필수적으로 조인이 필수였고, 그 쿼리가 간단하지는 않기에 JPA의 기본 메서드나 JPQL을 사용하기보다는 좀 더 안정적인 Querydsl을 사용하기로 하였다. 
  • 아직 Querydsl의 원리나 방법에 대해 미숙하기 때문에 해당 코드를 분석하고, Querydsl에 대해 공부하는 시간을 가지려 한다.  >>>   https://grogrammer.tistory.com/51

🗝️코드분석

JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
  • JPAQueryFactory는 Querydsl을 사용하여 JPA 쿼리를 작성하기 위한 도구이다.
    (uerydsl은 자바로 쿼리를 작성하기 위한 편리한 문법과 API를 제공)
  • 위 코드는 'entityManager' 을 기반으로 'JPAQueryFactory' 인스턴스를 생성.
  • 'JPAQueryFactory' 생성자에는 'entityManager'이 필요. 
    'entityManager'는 JPA에서 엔티티의 영속성 관리와 데이터베이스와의 상호작용을 처리하는 핵심 클래스
  • 'JPAQueryFactory' 는 'entityManager' 과 연동하여 JPA 쿼리를 실행하고 결과를 반환한다.
  • 'JPAQueryFactory' 인스턴스를 생성한 후, 해당 인스턴스를 사용하여 Querydsl을 활용한 쿼리 작성을 수행하고 
    Querydsl의 다양한 기능과 메서드를 제공하여 쿼리를 구성하고 실행한다.

QSchool school = QSchool.school;
QTag tag = QTag.tag;
QUser user = QUser.user;
  • 위 코드는 Querydsl을 사용하여 JPA 쿼리를 작성할 때, 엔티티와 속성에 접근하기 위해 해당 엔티티의 Q타입 클래스를 생성하는 과정이다.
  • Q타입 클래스는 Querydsl의 핵심 개념으로, 쿼리 작성 시 타입 안정성을 제공하며 쿼리 작성의 편의성을 제공한다.
    (아직 애매한 느낌이라 나만의 해석을 해보자면 "Q타입 클래스는 Querydsl 버전의 entitiy 클래스이다." 느낌?ㅎㅎ)

💛Q타입 클래스는 어떻게 생기는거고, 생성되는 원리는 무엇인가?

  1. Annotation Processor : Querydsl은 컴파일 시점에서 Annotation Processor를 활용하여 엔티티 클래스를 분석하고 Q타입 클래스를 생성한다. 엔티티 클래스에 지정된 Querydsl 관련 어노테이션을 확인하고 이를 바탕으로 Q타입 클래스를 생성한다. (Querydsl 관련 어노테이션 = entity 클래스에 있는 DB관련 어노테이션.)
  2. 메타모델 클래스 생성 : Annotation Processor는 엔티티 클래스의 에타데이터를 분석하여 해당 속성과 관계에 대한 정보를 수집한다. 이 정보를 바탕으로 Q타입 클래스의 필드와 메서드를 생성한다. 엔티티 클래스 이름에 "Q"접두사가 붙은 형태로 생성된다.
  3. 코드 생성 결과물 : 생성된 Q타입 클래스는 엔티티 클래스와 속성에 대한 정보를 담고 있으며, Querydsl 쿼리 작성 시 사용됩니다.

 


List<SchoolInfoDTO> schoolInfoList = queryFactory
                .select(Projections.constructor(SchoolInfoDTO.class,
                        school.id, school.schoolName))
                .from(school)
                .leftJoin(tag).on(school.id.eq(tag.schoolId))
                .leftJoin(user).on(school.id.eq(user.schoolId.id))
                .groupBy(school.id, school.schoolName)
                .fetch();
  • 위의 코드는 Querydsl을 사용하여 'SchoolInfoDTO' 객체를 조회하는 과정이다.
  • select(Projections.constructor(SchoolInfoDTO.class, school.id, school.schoolName))
    Projections.constructor() 메서드를 사용하여 'SchoolInfoDTO'의 생성자를 지정하고 'SchoolInfoDTO'의 생성자에 school.id와 school.schoolName을 전달하여 객체를 생성한다.
  • from(school)
    school 테이블을 기준으로 조회를 수행한다. 
  • leftJoin(tag).on(school.id.eq(tag.schoolId))
    school 테이블과 tag 테이블 조인된 결과를 가져올 수 있음. school.id와 tag.schoolId를 비교하여 조인조건 설정.
  • leftJoin(user).on(school.id.eq(user.schoolId.id))
    school테이블과 user테이블의 조인 결과를 가져올 수 있음. school.id와 user.schoolId,id를 비교하여 조인조건 설정.
  • groupBy(school.id, school.schoolName)
    school.id와 school.schoolName이 같은 결과들이 하나의 그룹으로 묶이게 된다.
  • fetch( )
    쿼리를 실행하고 결과를 가지고 온다. fetch() 메서드를 호출하여 결과를 가져오기 전까지는 실제로 db에 접근하지 않는다.

 

💛Projections.constructor() 메서드란??

Projections.constructor() 메서드는 Querydsl에서 사용되는 투영(Projection)을 생성하는 메서드이다.

투영(Projection)을 생성하는 메서드로 투영쿼리 결과를 원하는 방식으로 변환하는 작업을 의미한다.

즉, 해당 클래스의 생성자에 전달된 인자를 사용하여 객체를 생성한다. 


for (SchoolInfoDTO schoolInfo : schoolInfoList) {
            List<String> tags = queryFactory
                    .select(tag.name)
                    .from(tag)
                    .where(tag.schoolId.eq(schoolInfo.getId()))
                    .fetch();

            schoolInfo.setTag(tags);

            Long userCount = queryFactory
                    .select(user.id.count())
                    .from(user)
                    .where(user.schoolId.id.eq(schoolInfo.getId()))
                    .fetchOne();

            schoolInfo.setUserCount(userCount != null ? userCount.intValue() : 0);
        }
  • List<String> tags = queryFactory.select(tag.name).from(tag).where(tag.schoolId.eq(schoolInfo.getId())).fetch();
    - tag 테이블에서 tag.name 필드를 조회하여 List<String>으로 가지고 온다.
    - 조회하는 조건은 tag.schoolId가 schoolInfo.getId()와 동일한 경우이다. 
    근데 schoolInfo.getId()와 왜 비교를 할까?
    schoolInfoDTO 객체와 관련된 태그를 조회해서 정보룰 가지고 위해 사용된다.
  • schoolInfo.setTag(tags)
    위에 코드에서 조회한 태그 목록을 schoolInfo 객체에 넣어준다. 
  • Long userCount = queryFactory.select(user.id.count()).from(user).where(user.schoolId.id.eq(schoolInfo.getId())).fetchOne();
    - user 테이블에서 user.schoolId가 schoolInfo.getId() 와 동일한 레코드의 수를 조회한다. 
    - user.id.count() 를 사용하여 해당 조건을 만족하는 레코드 수를 카운트한다.
  • schoolInfo.setUserCount(userCount != null ? userCount.intValue() : 0)
    - 조회한 사용자 수를 schoolInfo 객체에 set(넣어)해준다.
    - schoolInfo 객체의 setUserCount() 메서드(별 다른 기능이 있는 건 아니고 그냥 setter임)를 사용하여 사용자 수를 설정한다.  🔜 userCount가 null인 경우 0으로 설정된다.

  • 이 코드는 전체적으로 SchoolInfoDTO 객체에 대해(이 DTO에는 내가 얻고자 하는 필드가 다 있으니까) 추가 정보를 조회하고 설정하는 작업을 수행한다.
    school, tag, user 테이블에서 얻고자하는 값과 형태가 모두 다르니까 한 방에 한 번의 방식으로 해결하는 게 아니라, 테이블마다 조회하고자 하는 형식에 맞게 쿼리를 작성하고 반환값을 set 하는 것임, 

실제 DTO

💛 fetchOne()과 fetch()의 차이점은 무엇일까?

fetchOne():

  • fetchOne() 메서드는 쿼리 결과에서 단일 값을 반환합니다.
  • 결과가 없는 경우 null을 반환하거나, 결과가 여러 개인 경우 첫 번째 값을 반환합니다.
  • 주로 단일 레코드 또는 단일 필드의 값을 조회할 때 사용됩니다.

fetch():

  • fetch() 메서드는 쿼리 결과를 컬렉션 형태로 반환합니다.
  • 결과를 리스트, 세트, 맵 등의 컬렉션 형태로 가져올 수 있습니다.
  • 결과가 없는 경우 빈 컬렉션을 반환합니다.
  • 주로 다중 레코드를 조회하거나, 여러 필드 값을 조회할 때 사용됩니다.