[UMC] JPA활용 미션
해당 화면들에 대해 작성했던 쿼리를 QueryDSL로 작성하여 리팩토링하기
⭐미션목록
1. Mission
2. Review
3. Member
Repository/Custom/Impl구조로 나누는 이유
1. 기존 SpringDataJPA와의 통합
JpaRepository를 통해 기본 CRUD기능 + 커스텀 기능 동시에 사용가능
하나의 레포지토리 인터페이스로 모든 기능 접근 가능
2. 관심사 분리
기본 기능과 복잡한 쿼리의 명확한 분리
인터페이스와 구현체 분리로 결합도 감소
3. 확장성
새로운 QueryDSL메서드가 필요할때 인터페이스에 추가하고 구현만 하면 됨
기존 코드 변경없이 기능 확장 가능
Repository = SpringDataJpa가 제공하는 JpaRepository등을 확장, CRUD를 자동으로 제공받음, 메서드 이름 규칙으로 간단한 쿼리기능 사용가능
RepositoryCustom = 복잡한 쿼리나 특별한 데이터 접근로직을 위한 사용자 정의 메서드 선언
RepositoryImpl = RepositoryCustom 인터페이스의 실제 구현 클래스, Querydsl등을 사용한 복잡한 쿼리 로직 작성(이름이 반드시 기본리포지토리이름Impl 형식이어야 자동으로 연결됨
1.Mission
(MissionRespositoryCustom)
package umc.springstart.repository.MissionRepository;
import org.springframework.data.domain.Pageable;
import umc.springstart.domain.Mission;
import umc.springstart.domain.enums.MissionStatus;
import java.util.List;
public interface MissionRepositoryCustom {
List<Mission> findByUserIdAndMissionStatus(Long user_id, MissionStatus missionStatus, Pageable pageable);
}
(MissionRepository)
package umc.springstart.repository.MissionRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import umc.springstart.domain.Mission;
public interface MissionRepository extends JpaRepository<Mission, Long>,MissionRepositoryCustom {
}
(MissionRepositoryImpl)
package umc.springstart.repository.MissionRepository;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import umc.springstart.domain.Mission;
import umc.springstart.domain.QMission;
import umc.springstart.domain.enums.MissionStatus;
import umc.springstart.domain.mapping.QMemberMission;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class MissionRepositoryImpl implements MissionRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
private final QMission mission = QMission.mission;
private final QMemberMission memberMission = QMemberMission.memberMission;
@Override
public Page<Mission> findByUserIdAndMissionStatus(Long userId, MissionStatus missionStatus, Pageable pageable) {
// 전체 개수 조회를 위한 쿼리
long total = jpaQueryFactory
.selectFrom(mission)
.join(memberMission).on(memberMission.mission.eq(mission))
.where(
memberMission.member.id.eq(userId),
memberMission.status.eq(missionStatus)
)
.fetchCount();
// 실제 데이터 조회 쿼리
List<Mission> missions = jpaQueryFactory
.selectFrom(mission)
.join(memberMission).on(memberMission.mission.eq(mission))
.where(
memberMission.member.id.eq(userId),
memberMission.status.eq(missionStatus)
)
.orderBy(mission.deadline.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
return new PageImpl<>(missions, pageable, total);
}
}
이제 서비스단에서 호출하면
(MissionQueryService)
package umc.springstart.service.MissionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import umc.springstart.domain.Mission;
import umc.springstart.domain.enums.MissionStatus;
import java.util.Optional;
public interface MissionQueryService {
Optional<Mission> findMission(Long id);
Page<Mission> findMissionsByMemberIdAndMissionStatus(Long memberId, MissionStatus missionStatus, Pageable pageable);
}
(MissionQueryServiceImpl)
package umc.springstart.service.MissionService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import umc.springstart.domain.Mission;
import umc.springstart.domain.enums.MissionStatus;
import umc.springstart.repository.MissionRepository.MissionRepository;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MissionQueryServiceImpl implements MissionQueryService {
private final MissionRepository missionRepository;
@Override
public Optional<Mission> findMission(Long id) {
return missionRepository.findById(id);
}
@Override
public Page<Mission> findMissionsByMemberIdAndMissionStatus(Long memberId, MissionStatus missionStatus, Pageable pageable) {
Page<Mission> filteredMissions = missionRepository.findByUserIdAndMissionStatus(memberId, missionStatus, pageable);
filteredMissions.forEach(mission -> System.out.println("Mission: " + mission));
return filteredMissions;
}
}
2. Review
리뷰의 경우 Review엔티티에 포함되지 않은 필드가 있기 때문에 Mission엔티티 자체를 조회하는 MissionRepositoryCustom과 달리 Tuple을 사용한다
(ReviewRepositoryCustom)
package umc.springstart.repository.ReviewRepository;
import com.querydsl.core.Tuple;
import umc.springstart.domain.enums.MemberStatus;
import java.util.List;
public interface ReviewRepositoryCustom {
List<Tuple> findStoreIdAndMemberStatus(Long id, MemberStatus memberStatus);
}
(ReviewRepository)
package umc.springstart.repository.ReviewRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import umc.springstart.domain.Review;
public interface ReviewRepository extends JpaRepository<Review, Long>, ReviewRepositoryCustom {
}
(ReviewRepositoryImpl)
package umc.springstart.repository.ReviewRepository;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import umc.springstart.domain.QMember;
import umc.springstart.domain.QReview;
import umc.springstart.domain.QStore;
import umc.springstart.domain.enums.MemberStatus;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class ReviewRepositoryImpl implements ReviewRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
private final QReview review = QReview.review;
private final QStore store = QStore.store;
private final QMember member = QMember.member;
@Override
public List<Tuple> findStoreIdAndMemberStatus(Long id, MemberStatus memberStatus) {
return jpaQueryFactory
.select(member.nickname, review)
.from(review)
.join(member).on(review.member.id.eq(member.id))
.join(store).on(review.store.id.eq(store.id))
.where(store.id.eq(store.id)
.and(member.status.eq(memberStatus.ACTIVE)))
.orderBy(review.updatedAt.desc(), member.createdAt.desc())
.fetch();
}
}
이걸 서비스단에서 사용하면
(ReviewQueryService)
package umc.springstart.service.ReviewService;
import com.querydsl.core.Tuple;
import umc.springstart.domain.Review;
import umc.springstart.domain.enums.MemberStatus;
import java.util.List;
import java.util.Optional;
public interface ReviewQueryService {
Optional<Review> findReview(Long id);
List<Tuple> findReviewsByIdAndUserStatus(Long id, MemberStatus memberStatus);
}
(ReviewQueryServiceImpl)
package umc.springstart.service.ReviewService;
import com.querydsl.core.Tuple;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import umc.springstart.domain.Review;
import umc.springstart.domain.enums.MemberStatus;
import umc.springstart.repository.ReviewRepository.ReviewRepository;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ReviewQueryServiceImpl implements ReviewQueryService {
private final ReviewRepository reviewRepository;
@Override
public Optional<Review> findReview(Long id) {
return reviewRepository.findById(id);
}
@Override
public List<Tuple> findReviewsByIdAndUserStatus(Long id, MemberStatus memberStatus) {
List<Tuple> filteredReviews = reviewRepository.findStoreIdAndMemberStatus(id, memberStatus);
filteredReviews.forEach(review -> System.out.println("Review: " + review));
return filteredReviews;
}
}
3.Member
(MemberRepository)
package umc.springstart.repository.MemberRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import umc.springstart.domain.Member;
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
( MemberRepositoryCustom)
package umc.springstart.repository.MemberRepository;
import com.querydsl.core.Tuple;
import umc.springstart.domain.enums.MissionStatus;
import java.util.List;
public interface MemberRepositoryCustom {
List<Tuple> findByIdAndMissionStatusAndRegionId(Long id, MissionStatus missionStatus, Long region_id);
}
( MemberRepositoryImpl)
package umc.springstart.repository.MemberRepository;
import com.querydsl.core.Tuple;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import umc.springstart.domain.QMember;
import umc.springstart.domain.QMission;
import umc.springstart.domain.QStore;
import umc.springstart.domain.enums.MissionStatus;
import umc.springstart.domain.mapping.QMemberMission;
import java.util.List;
@Repository
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
private final QMember member = QMember.member;
private final QMemberMission memberMission = QMemberMission.memberMission;
private final QMission mission = QMission.mission;
private final QStore store = QStore.store;
@Override
public List<Tuple> findByIdAndMissionStatusAndRegionId(Long id, MissionStatus missionStatus, Long region_id) {
return jpaQueryFactory
.select(store.name, store.id, mission)
.from(member)
.join(memberMission).on(member.id.eq(memberMission.member.id))
.join(mission).on(mission.id.eq(memberMission.mission.id))
.join(store).on(store.id.eq(mission.store.id))
.where(member.id.eq(member.id)
.and(memberMission.status.eq(MissionStatus.CHALLENGING))
.and(store.region.id.eq(region_id)))
.orderBy(mission.deadline.desc(), memberMission.updatedAt.desc())
.fetch();
}
}
이걸 서비스단에서 사용하면
( MemberQueryService)
package umc.springstart.service.MemberService;
import com.querydsl.core.Tuple;
import umc.springstart.domain.Member;
import umc.springstart.domain.enums.MissionStatus;
import java.util.List;
import java.util.Optional;
public interface MemberQueryService {
Optional<Member> findMember(Long id);
List<Tuple> findMembersByIdAndMissionStatusAndRegionId(Long id, MissionStatus missionStatus, Long region_id);
}
( MemberQueryServiceImpl)
package umc.springstart.service.MemberService;
import com.querydsl.core.Tuple;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import umc.springstart.domain.Member;
import umc.springstart.domain.enums.MissionStatus;
import umc.springstart.repository.MemberRepository.MemberRepository;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberQueryServiceImpl implements MemberQueryService {
private final MemberRepository memberRepository;
@Override
public Optional<Member> findMember(Long id){
return memberRepository.findById(id);
}
@Override
public List<Tuple> findMembersByIdAndMissionStatusAndRegionId(Long id, MissionStatus missionStatus, Long region_id) {
List<Tuple> filteredUsers = memberRepository.findByIdAndMissionStatusAndRegionId(id, missionStatus, region_id);
filteredUsers.forEach(user -> System.out.println("User: " + user));
return filteredUsers;
}
}