SpringBoot + QueryDSL 적용 살펴보기

2024. 11. 25. 20:47·만들어보기/Spring

QueryDslConfig.java

package com.lettrip.lettripbackend.configuration;


import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDslConfig {
    @PersistenceContext
    private EntityManager entityManager;
    public QueryDslConfig() {
    }

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(this.entityManager);
    }
}
EntityManager : JPA에서 Entity와 상호작용하는 인터페이스, Spring에서는 하나의 영속성 컨텍스트를 여러 EntityManager가 참조한다. 
JPAQueryFactory : JPA를 사용해 동적 쿼리를 작성하고 실행하기 위한 핵심 클래스이다. EntityManager를 기반으로 QueryDSL 쿼리를 실행할 수 있는 인터페이스를 제공한다.

 

기존 Repository 구조

기존 프로젝트 구조는 아래와 같다.

 

 

JPA, JPA Specification, QueryDSL을 전부 사용하고 있음을 알 수 있다.

위에서 나열된 순서대로 도입하였고, 사실 QueryDSL를 사용해야겠다고 마음 먹었을 때 Specification 을 사용해서 구현했던 내용을 QueryDSL로 옮겼어야 했다. 

하지만 나는 졸작 제출 기한에 쫓기던 4학년이었고 일단 기능에 문제가 없으니 나중에 옮기지 뭐 ~ 하고 일단 셋 다 사용해서 구현을 해놓았다.

 

그리고 ... 지금 그 나중이 되었다 ...

 

Repository 구조 다시 잡기

우선 QueryDSL을 사용할 때, JPAQueryFactory를 사용하는 QueryRepository를 JPARepository와 별개로 두어 사용했다.

그래서 Service 레이어에서 두 개의 Repository 의존성을 가지게 되었다. 

 

MissionService.java

package com.lettrip.lettripbackend.service;

import ...

@RequiredArgsConstructor
@Service
public class MissionService {
    public static final int QR_MISSION_POINT = 500;
    private final MissionRepository missionRepository;
    private final MissionQueryRepository missionQueryRepository;
    private final UserService userService;
    private final PlaceService placeService;

    @Transactional
    public ApiResponse saveMission(MissionDto.Request missionDto) {
        // 1. 미션 포인트 저장할 사용자 email로 찾기
        User user = userService.findUserByEmail(missionDto.getEmail());
        // 2. 미션 장소 찾기
        Place place = placeService.findPlaceByLocationPoint(
                placeService.getFormatLocationPoint(
                        String.valueOf(missionDto.getXpoint()),
                        String.valueOf(missionDto.getYpoint())
                )
        );
        // 3. 미션 저장
        missionRepository.save(
                Mission.builder()
                        .user(user)
                        .place(place)
                        .missionType(MissionType.QR_MISSION)
                        .accomplishedDate(missionDto.getAccomplishedDate())
                        .build()
        );
        // 4. 포인트 반영
        user.addPoint(QR_MISSION_POINT);

        return new ApiResponse(true,"미션 포인트 적립이 완료되었습니다.");
    }

    /*
    1. 미션 다득점자 (각 미션 별로)
    2. 음식점 & 카페 다방문자
    3. 다양한 지역을 여행다닌 사람
     */

    public List<RankingDto.Response> getRankingList(RankingDto.Request request) {
        RankingType rankingType = RankingType.valueOf(request.getRankingType());
        switch (rankingType) {
            case QR_MISSION -> {return getMissionRanking(MissionType.QR_MISSION, request);}
            case RANDOM_MISSION -> {return getMissionRanking(MissionType.RANDOM_MISSION, request);}
            case REVIEW_RESTAURANT -> {return getFoodPlaceVisitRanking(PlaceCategory.FD6,request);}
            case REVIEW_CAFE -> {return getFoodPlaceVisitRanking(PlaceCategory.CE7,request);}
            case TRAVEL_CITY -> {return getMostCityVisitRanking(request);}
            default -> {return new ArrayList<>();}
            }
    }

    public  List<RankingDto.Response> getMissionRanking(MissionType missionType, RankingDto.Request request) {
        return missionQueryRepository.getMissionCountByUser(missionType,request.getFrom(),request.getTo());
    }

    public List<RankingDto.Response> getFoodPlaceVisitRanking(PlaceCategory placeCategory, RankingDto.Request request) {
        return missionQueryRepository.getFoodReviewCountByUser(placeCategory,request.getFrom(),request.getTo());
    }

    public List<RankingDto.Response> getMostCityVisitRanking(RankingDto.Request request) {
        return missionQueryRepository.getMostCityCountByUser(request.getFrom(),request.getTo());
    }



}
private final MissionRepository missionRepository;
private final MissionQueryRepository missionQueryRepository;

두 개의 Repository 의존성을 가지고 있음을 확인할 수 있다.

 

MissionRepository

package com.lettrip.lettripbackend.repository;

import com.lettrip.lettripbackend.domain.mission.Mission;
import jakarta.annotation.Nullable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface MissionRepository extends JpaRepository<Mission, Long>, JpaSpecificationExecutor<Mission> {
    Page<Mission> findAll(@Nullable Specification<Mission> spec, Pageable pageable);
}

보다싶이 MissionRepository에서는 또 Specification을 사용하고 있다.

Specification을 정의하는 파일이 또 필요하므로 MissionSpecification.java 파일이 존재한다.

하나의 도메인에 대해서 JPA, JPA Specification, QueryDSL 각각을 위한 Repository 파일이 전부 존재하는 구조이다. 

 

 

  1. 의존성 관리
    • Service단에서 Repository단으로의 하나의 의존성만을 갖도록 한다.
  1. 복잡한 설계 및 중복된 기술 선택 문제
    • Specification과 QueryDSL은 중복되므로 두 기술 중 QueryDSL을 선택하여 복잡한 쿼리를 작성한다.

 

변경된 Repository 구조

1. __Repository (interface)

JPARepository와 Custom Interface를 상속한다.

Service단에서는 이 파일에 대한 의존성만을 가진다.

 

2. __RepositoryCustom (interface)

QueryDSL로 사용할 메서드를 선언한다.

 

3. __RepositoryImpl (class)

Custom Interface에서 선언한 메서드를 구현한다.

 

Spring Data JPA는 다음 규칙을 사용하여 Custom Repository를 매핑한다.

1. Custom Interface와 Custom Implementation의 이름이 일치해야 한다. 즉, __RepositoryCustom이라는 인터페이스가 있다면, 해당 구현체의 이름은 반드시 __RepositoryImpl이어야 한다.
2. Spring은 __Repository를 생성할 때, JpaRepository의 메서드는 기본 구현체로, __RepositoryCustom의 메서드는 __RepositoryImpl 구현체로 연결하여 단일 프록시 객체를 제공한다.

 

 

저작자표시 비영리 변경금지 (새창열림)

'만들어보기 > Spring' 카테고리의 다른 글

[Springboot] AWS SQS를 이용한 알림 시스템 구현하기 - 메시지 발행 로직  (0) 2025.04.24
[SpringBoot] SSE를 이용한 실시간 알림 전송 구현기 (feat. Redis Pub/Sub)  (0) 2025.04.08
[SpringBoot] AWS S3를 이용한 프로필 이미지 업로드 로직 구현기  (0) 2025.03.21
[Spring] 회원탈퇴, Soft Delete로 구현하기 (JPA)  (0) 2025.02.12
'만들어보기/Spring' 카테고리의 다른 글
  • [Springboot] AWS SQS를 이용한 알림 시스템 구현하기 - 메시지 발행 로직
  • [SpringBoot] SSE를 이용한 실시간 알림 전송 구현기 (feat. Redis Pub/Sub)
  • [SpringBoot] AWS S3를 이용한 프로필 이미지 업로드 로직 구현기
  • [Spring] 회원탈퇴, Soft Delete로 구현하기 (JPA)
다섯자두
다섯자두
All I need is 💻 , ☕️ and a dash of luck
  • 다섯자두
    subbni
    다섯자두
  • 전체
    오늘
    어제
    • 전체 글 (88) N
      • 개발 이야기 (0)
      • 만들어보기 (17)
        • FromBookToBook (5)
        • Spring (5)
        • Node.js & React (3)
        • TroubleShooting (4)
      • 공부하기 (71) N
        • Network (3)
        • Cloud (1)
        • Database (5)
        • Java (13)
        • Javascript (0)
        • Spring (9)
        • React (18)
        • Algorithm (8)
        • 자료구조 (7)
        • ETC (7) N
      • 회고 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • velog
  • 공지사항

  • 인기 글

  • 태그

    알고리즘
    network
    SSE
    오블완
    SQL
    HTTP
    redis
    SQS
    springboot
    pdf 프리뷰 실패
    최단거리
    실시간 데이터 전송 기술
    pdf 자동 다운로드
    Database
    Express
    Spring
    서명알고리즘
    자료구조
    알림 기능
    Til
    재시도 로직
    JPA
    로그인
    java
    프로젝트
    티스토리챌린지
    outbox
    outbox 패턴
    aws
    mysql
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
다섯자두
SpringBoot + QueryDSL 적용 살펴보기
상단으로

티스토리툴바