01 기본
1. 환경 설정과 Q클래스
2. 기본 쿼리 작성
3. 조인과 서브쿼리
02 실전
4. 동적 쿼리
5. 프로젝션과 DTO 매핑
6. Spring Data JPA 통합 ← 현재 편
03 심화
7. 성능 최적화
8. 실전 패턴과 면접 대비
QueryDSL로 쿼리를 작성하는 법은 배웠다. 이제 이것을 Spring Data JPA의 Repository 패턴과 어떻게 합치는지가 문제다. 기본 CRUD는 JpaRepository가 처리하고, 복잡한 조회는 QueryDSL이 담당하는 구조를 만들어야 한다.
커스텀 Repository 패턴
Spring Data JPA에서 QueryDSL을 쓰는 표준 패턴은 커스텀 Repository다. 세 개의 파일로 구성된다.
커스텀 인터페이스
QueryDSL로 구현할 메서드를 선언한다.
public interface FeedRepositoryCustom {
List<Feed> search(FeedSearchCondition condition);
List<Feed> findRecentFeeds(int limit);
}
이 인터페이스에는 @Repository 같은 어노테이션이 필요 없다. 순수한 Java 인터페이스다.
구현체
커스텀 인터페이스를 구현하고, JPAQueryFactory를 주입받아 QueryDSL 쿼리를 작성한다.
@RequiredArgsConstructor
public class FeedRepositoryImpl implements FeedRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<Feed> search(FeedSearchCondition condition) {
return queryFactory
.selectFrom(feed)
.join(feed.user, user).fetchJoin()
.where(
titleContains(condition.title()),
regionEq(condition.region())
)
.orderBy(feed.createdAt.desc())
.fetch();
}
@Override
public List<Feed> findRecentFeeds(int limit) {
return queryFactory
.selectFrom(feed)
.orderBy(feed.createdAt.desc())
.limit(limit)
.fetch();
}
private BooleanExpression titleContains(String title) {
return title != null ? feed.title.contains(title) : null;
}
private BooleanExpression regionEq(String region) {
return region != null ? feed.region.eq(region) : null;
}
}
JpaRepository에 합치기
기존 JpaRepository가 커스텀 인터페이스를 추가로 상속하면 된다.
public interface FeedRepository extends JpaRepository<Feed, Long>, FeedRepositoryCustom {
// JpaRepository의 기본 메서드 + 커스텀 메서드가 모두 사용 가능
}
이제 FeedRepository를 주입받으면 save(), findById() 같은 기본 메서드와 search(), findRecentFeeds() 같은 커스텀 메서드를 모두 쓸 수 있다.
(Proxy) participant JPA as SimpleJpaRepository participant Impl as FeedRepositoryImpl S->>P: save(feed) P->>JPA: 기본 메서드 위임 JPA-->>S: 결과 반환 S->>P: search(condition) P->>Impl: 커스텀 메서드 위임 Impl-->>S: 결과 반환
구현체의 이름은 반드시 JpaRepository 인터페이스명 + Impl이어야 한다. FeedRepository가 메인 인터페이스이면 구현체는 FeedRepositoryImpl이다. Spring Data JPA가 이 네이밍 규칙으로 구현체를 자동 인식하여 프록시에 연결한다. FeedRepositoryCustomImpl이 아니라 FeedRepositoryImpl이다.
별도 쿼리 Repository 패턴
커스텀 Repository가 항상 정답은 아니다. 조회 쿼리가 많아지면 하나의 FeedRepositoryImpl에 메서드가 수십 개씩 쌓이고, 네이밍 규칙도 신경 써야 한다.
대안으로, QueryDSL 전용 Repository를 별도로 만드는 방법이 있다.
@Repository
@RequiredArgsConstructor
public class FeedQueryRepository {
private final JPAQueryFactory queryFactory;
public List<Feed> search(FeedSearchCondition condition) {
return queryFactory
.selectFrom(feed)
.where(
titleContains(condition.title()),
regionEq(condition.region())
)
.fetch();
}
// 조건 메서드들...
}
이 클래스는 Spring Data JPA와 무관한 일반 @Repository 빈이다. Service에서 FeedRepository와 FeedQueryRepository를 각각 주입받아 사용한다.
@Service
@RequiredArgsConstructor
public class FeedService {
private final FeedRepository feedRepository; // 기본 CRUD
private final FeedQueryRepository feedQueryRepository; // 복잡한 조회
}
어떤 패턴을 선택할까
커스텀 Repository
- 하나의 인터페이스로 모든 메서드에 접근할 수 있다.
- Spring Data JPA의 공식 확장 방식이다.
- Impl 네이밍 규칙을 지켜야 한다.
- 메서드가 많아지면 구현체가 비대해진다.
별도 쿼리 Repository
- 조회 전용 로직을 분리하여 관심사가 명확하다.
- 네이밍 규칙에 묶이지 않는다.
- Service에서 Repository를 두 개 주입받아야 한다.
- 팀 내 컨벤션이 필요하다.
두 방식을 섞어 쓰는 것도 가능하다. 간단한 커스텀 조회는 커스텀 Repository에, 복잡한 검색 쿼리는 별도 Repository에 두는 식이다. 프로젝트의 규모와 팀 컨벤션에 맞게 선택하면 된다.
QuerydslRepositorySupport
QueryDSL이 제공하는 추상 클래스로, EntityManager와 Querydsl 헬퍼를 자동으로 설정해준다.
public class FeedRepositoryImpl extends QuerydslRepositorySupport
implements FeedRepositoryCustom {
public FeedRepositoryImpl() {
super(Feed.class);
}
@Override
public List<Feed> search(FeedSearchCondition condition) {
return from(feed)
.where(titleContains(condition.title()))
.fetch();
}
}
from()을 바로 쓸 수 있어서 편리해 보이지만, 몇 가지 문제가 있다.
select로 시작할 수 없다. 항상from으로 시작해야 한다.JPAQueryFactory를 직접 사용할 때보다 API가 제한적이다.- Spring Data JPA의
Sort와의 통합이 번거롭다.
select 불가"] end subgraph "JPAQueryFactory 직접 사용" JQF["select/from 자유
유연한 API"] end QRS -.->|"레거시"| OLD[기존 프로젝트] JQF -->|"권장"| NEW[새 프로젝트] style JQF fill:#9f9,stroke:#333
실무에서는 JPAQueryFactory를 직접 주입받는 방식이 더 선호된다. QuerydslRepositorySupport는 레거시 프로젝트에서 볼 수 있지만, 새 프로젝트에서는 굳이 쓸 필요 없다.
자주 하는 실수
FeedRepository에 커스텀 인터페이스를 연결했는데, 구현체를 FeedCustomRepositoryImpl이나 FeedRepositoryCustomImpl로 이름 지으면 Spring이 인식하지 못한다. 반드시 FeedRepositoryImpl이어야 한다. 메인 JpaRepository 인터페이스 이름 + Impl이 규칙이다.
[!DANGER] 커스텀 인터페이스를 JpaRepository에 연결하지 않음
FeedRepositoryCustom을 만들고 FeedRepositoryImpl도 만들었는데, FeedRepository extends JpaRepository<Feed, Long>에 FeedRepositoryCustom을 추가 상속하지 않으면 커스텀 메서드가 연결되지 않는다.
[!DANGER] Impl 클래스에 @Repository 생략
커스텀 Repository 패턴에서 Impl 클래스에는 @Repository를 붙이지 않는다. Spring Data JPA가 자동으로 프록시에 연결하기 때문이다. 반면, 별도 쿼리 Repository 패턴에서는 @Repository를 붙여야 스프링 빈으로 등록된다. 두 패턴을 혼동하지 않도록 주의한다.