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 |