👥💡 Spring Data JPA + Querydsl 커스텀 Repository 구조 정리

2026. 2. 14. 17:29·프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트

1️⃣ 왜 BlogRepositoryCustom을 따로 만들까?

JpaRepository는 기본 CRUD 기능만 제공한다.

예:

  • findAll()
  • findById()
  • save()
  • delete()

하지만 우리가 실제로 만들고 싶은 기능은 이런 것들이다.

  • keyword가 있으면 제목/내용 검색
  • authorNickname이 있으면 작성자 닉네임 검색
  • 둘 다 있으면 둘 다 적용
  • 아무 조건 없으면 전체 조회

이건 단순한 CRUD가 아니라 동적 검색 로직이다.
스프링이 자동으로 만들어줄 수 없는 “사용자 정의 기능”이다.

그래서 구조를 이렇게 나눈다.

① 커스텀 인터페이스 선언

public interface BlogRepositoryCustom {
    Page<Article> search(ArticleSearchCondition condition, Pageable pageable);
}

② Querydsl로 직접 구현

public class BlogRepositoryImpl implements BlogRepositoryCustom {
    ...
}

③ 기존 Repository에 합치기

public interface BlogRepository extends JpaRepository<Article, Long>, BlogRepositoryCustom {
}

결과적으로,

  • 기본 CRUD 기능
  • 내가 만든 검색 기능

이 둘을 하나의 Repository로 사용할 수 있게 된다.


2️⃣ 왜 BlogRepositoryImpl 이름이 중요할까?

이건 스프링 데이터 JPA의 자동 연결 규칙 때문이다.

스프링은 다음 규칙으로 커스텀 구현체를 찾는다.

Repository 인터페이스 이름 + Impl

예:

  • 인터페이스 → BlogRepository
  • 구현체 → BlogRepositoryImpl

이 규칙을 어기면 스프링이 커스텀 구현을 찾지 못하고
search() 메서드가 연결되지 않아 에러가 발생한다.

즉, 이건 선택이 아니라 규칙이다.


3️⃣ 생성자에서 EntityManager를 받는 이유

public BlogRepositoryImpl(EntityManager em) {
    this.queryFactory = new JPAQueryFactory(em);
}

Querydsl이 DB에 쿼리를 날리려면
JPA의 핵심 객체인 EntityManager가 필요하다.

  • EntityManager = JPA 엔진
  • JPAQueryFactory = Querydsl 쿼리 생성 도구

스프링이 이미 EntityManager를 관리하고 있기 때문에
생성자로 받아오면 자동으로 주입된다.

이렇게 해서 Querydsl을 사용할 준비가 완료된다.


4️⃣ .selectFrom(article) 과 Q타입 static import

Querydsl은 Q타입을 사용한다.

원래는 이렇게 써야 한다.

QArticle article = QArticle.article;
queryFactory.selectFrom(article);

하지만 static import를 하면

import static studying.blog.domain.QArticle.article;

queryFactory.selectFrom(article);

처럼 더 깔끔하게 사용할 수 있다.

즉,

.selectFrom(article)은 QArticle.article을 static import한 것


5️⃣ 동적 조건을 함수로 분리하는 이유

검색 조건이 늘어나면 이런 코드가 된다.

if (keyword != null && authorNickname != null) {
    ...
} else if (keyword != null) {
    ...
} else if (authorNickname != null) {
    ...
}

조건이 많아질수록 코드가 폭발한다.

그래서 Querydsl에서는 조건을 함수로 분리한다.

예:

.where(
    titleOrContentContains(condition.getKeyword()),
    authorNicknameEq(condition.getAuthorNickname())
)

각 함수는 이런 구조다.

private BooleanExpression titleOrContentContains(String keyword) {
    if (keyword == null || keyword.isBlank()) {
        return null;
    }
    return article.title.containsIgnoreCase(keyword)
            .or(article.content.containsIgnoreCase(keyword));
}

핵심 포인트:

  • 조건이 있으면 BooleanExpression 반환
  • 조건이 없으면 null 반환
  • where()는 null을 자동 무시

그래서 동적 쿼리가 깔끔하게 구성된다.


6️⃣ PageImpl은 왜 직접 생성할까?

Querydsl의 .fetch()는 List만 반환한다.

하지만 우리는 컨트롤러에서 이런 형태를 원한다.

Page<Article>

Page에는 다음 정보가 포함된다.

  • content (현재 페이지 데이터)
  • pageable (page 번호, size, 정렬 정보)
  • totalCount (전체 데이터 수)

그래서 직접 Page 객체를 만들어줘야 한다.

return new PageImpl<>(content, pageable, totalCount);

PageImpl은 Spring Data가 제공하는 Page의 구현체다.


7️⃣ containsIgnoreCase()는 무엇을 의미할까?

article.title.containsIgnoreCase(keyword)

SQL로 보면 이런 의미다.

where lower(title) like '%keyword%'

즉,

대소문자 구분 없이 keyword가 포함되어 있는지 검사

or()를 붙이면:

article.title.containsIgnoreCase(keyword)
    .or(article.content.containsIgnoreCase(keyword));

→ 제목 또는 내용에 포함되면 조회


8️⃣ 왜 두 개를 상속받을까?

public interface BlogRepository 
    extends JpaRepository<Article, Long>, BlogRepositoryCustom {
}

이 구조의 의미는 다음과 같다.

  • JpaRepository → 기본 CRUD
  • BlogRepositoryCustom → 내가 만든 검색 기능

둘을 합쳐서 하나의 Repository처럼 사용한다.

그래서 서비스나 컨트롤러에서는 이렇게 쓸 수 있다.

blogRepository.search(condition, pageable);
blogRepository.save(article);
blogRepository.findById(id);

기본 기능과 커스텀 기능을 한 곳에서 사용하는 구조다.

전체 구조 한눈에 정리

BlogRepository (통합 인터페이스)
 ├─ JpaRepository (기본 CRUD)
 └─ BlogRepositoryCustom (내가 만든 검색 기능)

BlogRepositoryImpl
 └─ BlogRepositoryCustom 구현체 (Querydsl 사용)

'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글

👥💡 원자적 방법이란? 벌크성 내용이랑 무슨 차이가 있는지?  (0) 2026.02.14
👥💡 조회수 동시성 문제 해결(원자적)  (0) 2026.02.14
👥💡 Querydsl 페이징: fetchResults()가 비권장(Deprecated)된 이유와 실무 정석 패턴  (0) 2026.02.14
👥🚨 수정시간 및 수정하기 작성자 권한 변경  (0) 2026.02.14
👥🚨 수정은 됐는데 화면에는 “실패”라고 뜬다?  (1) 2026.02.07
'프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
  • 👥💡 원자적 방법이란? 벌크성 내용이랑 무슨 차이가 있는지?
  • 👥💡 조회수 동시성 문제 해결(원자적)
  • 👥💡 Querydsl 페이징: fetchResults()가 비권장(Deprecated)된 이유와 실무 정석 패턴
  • 👥🚨 수정시간 및 수정하기 작성자 권한 변경
hak0622
hak0622
개발하면서 “이게 뭐지?”라는 순간마다 궁금한 점을 바탕으로 정리한 개발 블로그입니다.
  • hak0622
    궁금한 개발 이야기 Why?
    hak0622
  • 전체
    오늘
    어제
    • 분류 전체보기 (68)
      • 공부 (36)
        • 1. 자바 ORM 표준 JPA 프로그래밍 - 기본.. (35)
        • 시험 (1)
      • 프로젝트 (32)
        • 스프링 부트 3 백엔드 개발자 되기 Blog + .. (32)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hak0622
👥💡 Spring Data JPA + Querydsl 커스텀 Repository 구조 정리
상단으로

티스토리툴바