Tiny Star

프로젝트/반려동물

[반려동물 프로젝트] 백엔드 구현 (QueryDSL)

흰둥아 2025. 4. 24. 23:54

화면으로 보낼 데이터를 작업하려고 한다.

서비스 고민할 때 가장 첫 번째로 떠올렸던 '지역별 대표 품종' 쿼리부터 작업해보자.

 


 

의존성 설정

val queryDslVersion = "5.0.0"
dependencies {
    implementation("com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta")
    annotationProcessor("com.querydsl:querydsl-apt:${queryDslVersion}:jakarta")
    annotationProcessor("jakarta.annotation:jakarta.annotation-api")
    annotationProcessor("jakarta.persistence:jakarta.persistence-api")
}

val querydslDir = "src/main/generated"
sourceSets {
    main {
        java {
            srcDirs("src/main/java", querydslDir)
        }
    }
}
tasks.withType<JavaCompile> {
    options.generatedSourceOutputDirectory.set(file(querydslDir))
}
tasks.named("clean") {
    doLast {
        file(querydslDir).deleteRecursively()
    }
}

 

QueryDSL 을 사용할 것이기 때문에 gradle 설정을 추가한다.

 

항목 설명
querydsl-jpa:jakarta JPA 기반 QueryDSL (Jakarta API용)
querydsl-apt:jakarta APT(Annotation Processing Tool) → Q클래스 자동 생성
jakarta.persistence-api JPA 어노테이션 (@Entity 등) Jakarta 버전
jakarta.annotation-api @Generated 등 Jakarta 관련 어노테이션 처리용
generatedSourceOutputDirectory Q클래스를 어느 폴더에 만들지 지정

 

 

 

Q클래스?

JPA 엔티티를 기반으로 QueryDSL이 빌드 타임에 생성하는 정적 메타 모델 클래스

쿼리에서 문자열 대신 타입 기반으로 안전하게 쿼리를 작성할 수 있게 해줌.

 

✅QueryDSL은 타입 기반이라 IDE 자동완성 + 컴파일 오류 잡힘

QAnimalStats stats = QAnimalStats.animalStats;

queryFactory.selectFrom(stats)
    .where(stats.animalType.id.eq(1L)) // 자동완성 + 안전함
    .fetch();

 

 

 

 

QueryDSL 사용 준비

@Configuration
public class QuerydslConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }

}

QuerydslConfiguration 만 추가하면 끝이다.

원래는 repository 를 상속받고 구현해야하는 작업이 복잡한데

향로님 블로그 참고해서 querydsl로만 구현할 것이다.

 

 

먼저 프로젝트를 빌드해서 Q클래스들을 생성한다.

@Entity를 사용해서 DTO를 잘 만들었다면 잘 생성된다.

 

 

 

 

QueryRepository

@RequiredArgsConstructor
@Repository
public class AnimalStatsQueryRepository {
    private final JPAQueryFactory queryFactory;

    public List<RegionTopAnimalTypeResponse> getTopAnimalTypeResponseList(RegionTopAnimalTypeRequest request) {
        List<RegionTopAnimalTypeResponse> dogTop10 = getTopBreedsBySpecies(request.getRegionId(), 1);
        List<RegionTopAnimalTypeResponse> catTop10 = getTopBreedsBySpecies(request.getRegionId(), 2);

        List<RegionTopAnimalTypeResponse> result = new ArrayList<>();
        result.addAll(dogTop10);
        result.addAll(catTop10);
        return result;
    }

    private List<RegionTopAnimalTypeResponse> getTopBreedsBySpecies(Integer regionId, Integer speciesId) {
        QAnimalStats stats = QAnimalStats.animalStats;
        QAnimalType type = QAnimalType.animalType;
        QSpecies species = QSpecies.species;

        return queryFactory
                .select(Projections.constructor(
                        RegionTopAnimalTypeResponse.class,
                        species.name,
                        type.name,
                        stats.animalCount.sum()
                ))
                .from(stats)
                .join(stats.animalType, type)
                .join(type.species, species)
                .where(
                        regionId != null ? stats.region.id.eq(regionId) : null,
                        species.id.eq(speciesId)
                )
                .groupBy(type.id, species.name, type.name)
                .orderBy(stats.animalCount.sum().desc())
                .limit(10)
                .fetch();
    }

}

작성한 쿼리

 

조인쿼리가 많아서 좀 복잡한데, 지피티한테 내가 작성한 쿼리를 기반으로 querydsl 작성해달라고 요청했다.

species_name 은 빼먹었는데, 찰떡같이 알아듣고 추가해줬다.

 

(소스코드 내에 Q클래스를 찾지 못해서 이것저것해봤는데, clean restart build 몇 번 하니까 됨;;)

 

 

 

 

Service & Controller

@RequiredArgsConstructor
@Service
public class AnimalTypeService {
    private final AnimalStatsQueryRepository animalStatsQueryRepository;

    public List<RegionTopAnimalTypeResponse> getTopAnimalTypes(RegionTopAnimalTypeRequest request) {
        return animalStatsQueryRepository.getTopAnimalTypeResponseList(request);
    }
}
@RequiredArgsConstructor
@RestController
public class AnimalTypeController {
    private final AnimalTypeService animalTypeService;

    @GetMapping("/api/top-animal-type")
    public List<RegionTopAnimalTypeResponse> getTopAnimalType(RegionTopAnimalTypeRequest regionTopAnimalTypeRequest) {
        return animalTypeService.getTopAnimalTypes(regionTopAnimalTypeRequest);
    }
}

서비스와 컨트롤러는 테스트용도로 간단하게 작성했다.

수정한다고해도 path 외에는 크게 변할 건 없어 보인다.

 

 

테스트

 

잘 나온다!

 


 

쿼리하기까지 우여곡절이 많을 줄 알았는데 생각보다 바로 됐다.

프론트 할 시간이 생각보다 빠르게 다가와서 겁난다 ^^;;

 

top