빙응의 공부 블로그

[UniP]QueryDSL로 조회 최적화하기 본문

Project/UniP

[UniP]QueryDSL로 조회 최적화하기

빙응이 2024. 11. 7. 19:09

이번 게시글은 조회 최적화와 QueryDSL을 다루겠습니다.

 

📝배경

이번에 졸업 작품 프로젝트에서 메인서버를 맡아 개발하게 되었습니다.

 

파티를 만들어서 그안에 코스를 보고 대학생들이 자연스럽게 약속하는 앱을 만드는 과정인데

 

해당 과정에서 중요하게 본 점은 조회 쿼리에 대한 효율입니다. 

그래서 김영한의 스프링에서 학습한 내용을 기반으로 QueryDSL을 사용하여 조회 최적화를 시도해 보았습니다 .

 

QueryDSL을 사용하여 조건에 따른 동적 쿼리를 유연하게 처리할 수 있었습니다.

해당 슬라이드에서 설명하는 것처럼 파티는 식사, 음주, 종합으로 나누어 조회합니다. 

이 과정을 쉽게 하기 위해 동적 쿼리 추가가 쉬운 QueryDSL을 사용했습니다. 

 

QueryDSL에 대해 자세히 알고 싶으면 전에 공부했던 포스팅을 보시면 됩니다.

[Spring]QueryDSL에 대해 알아보자!

 

[Spring]QueryDSL에 대해 알아보자!

📝 QueryDSL은 무엇일까요?DSL은 Domain Specific Language의 약자로, 특정 영역에 특화된 언어를 의미한다.즉 QueryDSL은 쿼리생성에 특화된 프레임워크를 의미한다. 그렇다면 QueryDSL이 필요한 이유가 무엇

quddnd.tistory.com

 

📝QueryDSL 설정하기 

QueryDSL을 프로젝트에 적용하기 위해서는 의존성 설정과 다른 설정들이 필요합니다.

 

의존성 설정하기 
	// Querydsl
	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
	annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
	annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
	annotationProcessor 'jakarta.persistence:jakarta.persistence-api'

 

Q타입 만들기

Q타입클래스는 QueryDSL 플러그인으로 컴파일하면 지정된 위치에 생성되며

QueryDSL이 JPQL을 생성할 수 있도록 Entity 정보를 담습니다.  

개발자는 Q타입 객체를 생성하여 JPQL 생성에 필요한 데이터를 QueryDSL로 넘길 수 있습니다.

 

Q 타입은 generated에 만들어지며 build.clean에서 만들 수 있습니다.

만약 엔티티를 만들고 해당 과정을 거치지않으면 빌드 시 오류가 생기니 참고해주세요

 

 

 

📝QueryDSL 적용하기

 

이제 QueryDSL로 동적 쿼리를 만드는 일부터 해봅시다.

파티 타입은 아래 3개의 타입 중 하나를 가집니다. 

이것을 가지고 조회를 할 겁니다. 파티 타입이 null로 들어오면 모든 파티를 조회할 수 있도록 설정했습니다.

@Getter
public enum PartyType {
    RESTAURANT("식사"),
    BAR("술집"),
    COMPREHENSIVE("종합");
    private final String description;

    PartyType(String description) {
        this.description = description;
    }

}

 

 

 

일단 QueryDSL를 작성하기 전에 Repository 부분과 합쳐줘야 합니다. 그래서 PartyRepository에

QueryDSL을 구현할 인터페이스를 extends 해줍니다.

 

@Repository
public interface PartyRepository extends JpaRepository<Party, Long>, PartyRepositoryCustom {

    Optional<Party> findById(Long id);

}

 

 

PartyRepositoryCustom 인터페이스에서 기존에는 Repository 방식이 아닌 직접 DTO로 데이터를 받아와 쿼리 최적화를 진행하였습니다. 이를 통해 필요한 정보만을 선택적으로 조회할 수 있게 되어, 성능을 높이겠습니다.

public interface PartyRepositoryCustom {
    List<PartyResponseDto> getMainPartyPage(PartyType partyType);
    Optional<PartyDetailDto> findPartyDetailById(Long id);
    List<PartyMyDto> getMyParty(Long id);
}

 

이 방식은 불필요한 엔티티 조회를 줄이고, 원하는 정보만을 DTO로 받아와 조회 효율성을 높이는 데 유리합니다.

 

 

    @Override
    public List<PartyResponseDto> getMainPartyPage(PartyType partyType) {
        BooleanBuilder conditions = createMainPartyConditions(partyType);

        return queryFactory
            .select(Projections.constructor(PartyResponseDto.class,
                party.id,
                member.name,
                member.profile_image,
                party.title,
                party.partyType,
                party.partyLimit,
                party.peopleCount,
                party.startTime,
                party.endTime
            ))
            .from(party)
            .join(party.member, member)
            .where(conditions)
            .fetch();
    }
    private BooleanBuilder createMainPartyConditions(PartyType partyType) {
        BooleanBuilder builder = new BooleanBuilder();
        builder.and(party.endTime.goe(LocalDateTime.now())); // 현재 시간 이후의 파티만 조회
        builder.and(party.isClosed.isFalse()); // 종료되지 않은 파티만 조회

        if (partyType != null) {
            builder.and(party.partyType.eq(partyType)); // 파티 타입 조건 추가
        }

        return builder;
    }

 

일단 위와 같이 동적 쿼리를 만들어 주었습니다. 만약 타입이 들어오면 해당 파티 타입을 추가하는 방식으로 만들었습니다.

 

나머지 메소드도 구현해주었습니다.

    @Override
    public Optional<PartyDetailDto> findPartyDetailById(Long id) {
        Party result = queryFactory
            .selectFrom(party)
            .where(party.id.eq(id))
            .fetchOne();

        if (result == null) {
            return Optional.empty();
        }

        List<CourseDto> courses = queryFactory
            .select(Projections.constructor(CourseDto.class,
                course.address,
                course.name,
                course.content
            ))
            .from(course)
            .where(course.party.id.eq(result.getId()))
            .fetch();

        PartyDetailDto partyDetailDto = PartyDetailDto.builder()
            .id(result.getId())
            .title(result.getTitle())
            .content(result.getContent())
            .limit(result.getPartyLimit())
            .peopleCount(result.getPeopleCount())
            .startTime(result.getStartTime())
            .endTime(result.getEndTime())
            .courses(courses)
            .build();

        return Optional.of(partyDetailDto);
    }

    @Override
    public List<PartyMyDto> getMyParty(Long id) {
        return queryFactory
            .select(Projections.constructor(PartyMyDto.class,
                party.id,
                party.title,
                party.partyLimit,
                party.peopleCount
            ))
            .from(party)
            .where(party.member.id.eq(id))
            .fetch();
    }

 

 


이번 포스팅에서는 QueryDSL을 활용하여 파티 조회 최적화와 동적 쿼리 처리를 진행했습니다. QueryDSL을 통해 복잡한 조건을 유연하게 처리할 수 있었고, 불필요한 엔티티 조회를 줄여 성능을 최적화할 수 있었습니다. 특히, 파티 타입에 따른 동적 쿼리 생성과 DTO로 필요한 정보만을 조회하는 방식으로 데이터베이스 부담을 줄였으며, 이를 통해 효율적인 데이터 처리가 가능해졌습니다. QueryDSL을 사용하여 동적 쿼리를 작성하는 방식은 조건에 따라 쿼리를 쉽게 조정할 수 있어 향후 다양한 요구사항에 대응하기 좋을 것 같습니다.