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 파일이 전부 존재하는 구조이다.
- 의존성 관리
- Service단에서 Repository단으로의 하나의 의존성만을 갖도록 한다.
- 복잡한 설계 및 중복된 기술 선택 문제
- 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 구현체로 연결하여 단일 프록시 객체를 제공한다.
'Project' 카테고리의 다른 글
[BAW] Express + PostgreSQL 트랜잭션 처리 도입기 (1) | 2024.11.09 |
---|---|
[BAW] 중간점검 .. 프로젝트 모듈 구조화 (0) | 2024.11.07 |
[Express + React] OAuth2 로그인 구현 : Kakao 로그인에 닉네임이 반드시 필요하다면? (2) | 2024.10.21 |