Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
Tags
- mysql 표 출력
- mysql 표
- 욕심쟁이 방법
- MySQL
- 티스토리챌린지
- VI
- 동적 프로그래밍 방법
- 순차탐색
- table status
- 맥
- spring boot
- oh-my-zsh
- Less
- zsh theme
- 인프런
- zsh
- 스터디2기
- 터미널
- 이진탐색
- 데이크스트라
- mycli
- 오일러 경로
- 인프런워밍업클럽
- CS스터디
- 오블완
- cs
- 분할정복 방법
- 네트워킹데이
- Pager
- 알고리즘
Archives
- Today
- Total
Develop
[Spring Boot] 게시판 - 10. QueryDSL로 동적 검색 기능 구현하기 본문
안녕하세요 .ᐟ
오늘은 게시판의 핵심 기능 중 하나인 검색 기능을 QueryDSL을 사용해서 구현해봤습니다.
"제목으로만 검색할까? 내용도 포함할까? 작성자 이름도?" 등등 .. 이런 동적 검색 조건을 깔끔하게 처리하는 방법을 공유합니다~
왜 QueryDSL을 사용할까?
JPA 의 @Query 로 검색 구현
- 검색 조건이 늘어날수록 쿼리가 복잡해지고, 가독성이 떨어집니다.
@Query("SELECT p FROM Post p WHERE " +
"(:title IS NULL OR p.title LIKE %:title%) AND " +
"(:content IS NULL OR p.content LIKE %:content%) AND " +
"(:author IS NULL OR p.user.username LIKE %:author%)")
Page<Post> searchPosts(String title, String content, String author, Pageable pageable);
QueryDSL 로 검색 구현
- 자바 코드로 쿼리 작성 → 컴파일 시점에 오류 체크
- 동적 쿼리를 깔끔하게 → null 조건 자동 처리
- IDE 자동완성 지원 → 오타 방지
- 타입 안전성 → 리팩토링 시 자동 반영
QueryDSL로 검색 기능 구현하기
1. QueryDSL 설정
build.gradle 의존성 추가
- Gradle 빌드 후 build/generated/sources/annotationProcessor/java/main에 Q클래스(QPost, QUser 등)가 자동 생성됩니다.
dependencies {
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
QueryDslConfig (설정 클래스)
- 역할
- QueryDSL을 사용하기 위한 핵심 도구를 스프링 빈으로 등록
- 용도
- JPAQueryFactory 가 있어야 QueryDSL 쿼리 작성 가능
- EntityManager 를 주입받아서 데이터베이스와 통신
@Configuration
public class QueryDslConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager em) {
return new JPAQueryFactory(em);
}
}
PostSearchCondition (검색 조건 DTO)
- 역할
- 검색에 필요한 조건들을 묶어서 전달
- 용도
- 파라미터가 많을때 깔끔하게 관리
- null 체크를 한 곳에서 처리
- 나중에 조건을 추가하기 쉬움
@Getter
public class PostSearchCondition {
private String title;
private String content;
private String author;
private Long categoryId;
}
2. Custom Repository 패턴 적용
- QueryDSL을 사용하려면 Custom Repository 패턴을 적용해야 합니다.
repository/
├── PostRepository.java (메인)
├── PostRepositoryCustom.java (인터페이스)
└── PostRepositoryImpl.java (구현체)
- 왜 이렇게 나눌까?
- JpaRepository: 기본 CRUD 기능 제공
- Custom Repository: 복잡한 동적 쿼리 전용
둘을 분리해서 관심사를 명확히 하고, 코드를 재사용 가능하게 만듭니다.
3. PostRepositoryCustom (인터페이스)
- 역할
- "이런 기능이 필요해요"라고 선언하는 계약서 역할입니다.
- 용도
- JpaRepository 가 제공하는 기본 메서드(findById, save.. 등) 외에 복잡한 동적 쿼리 기능을 추가하고 싶을 때 사용
public interface PostRepositoryCustom {
Page<Post> searchPosts(PostSearchCondition condition, Pageable pageable);
}
PostRepositoryImpl (구현체)
- 역할
- QueryDSL로 쿼리를 작성하는 곳 입니다.
- 레포지토리 이름 뒤에 Impl 을 붙이면 Spring Data JPA가 자동으로 인식해서 연결해줍니다.
@RequiredArgsConstructor
public class PostRepositoryImpl implements PostRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Page<Post> searchPosts(PostSearchCondition condition, Pageable pageable) {
// 실제 QueryDSL 작성
}
}
- BooleanExpression과 null 처리
- null 을 리턴하면 QueryDSL이 해당 조건을 자동으로 무시합니다!
private BooleanExpression titleContains(String title) {
return hasText(title) ? post.title.contains(title) : null;
}
아래처럼 null 인 필드는 where 조건에서 제외하고 검색하게 됩니다.
- title=Spring, content=null → WHERE title LIKE '%Spring%'
- title=null, content=JPA → WHERE content LIKE '%JPA%'
- 둘 다 null → WHERE 조건 없이 전체 조회
- fetchJoin() 사용
- 연관된 엔티티(User, Category)를 한 번에 조회합니다. → N+1 문제 예방
.leftJoin(post.user, user).fetchJoin()
.leftJoin(post.category, category).fetchJoin()
아래처럼 fetchJoin을 안 쓰면, 총 21번의 쿼리가 발생합니다!
fetchJoin을 쓰면 단 1번으로 해결 됩니다.
- 게시글 10개 조회 (쿼리 1번)
- 각 게시글의 작성자 조회 (쿼리 10번)
- 각 게시글의 카테고리 조회 (쿼리 10번)
- count 쿼리를 따로 날리는 이유
- 페이징을 위해 전체 개수가 필요하기 때문입니다.
- count 쿼리 최적화 가능 (JOIN 불필요)
- 쿼리 의도가 명확해짐
- 페이징을 위해 전체 개수가 필요하기 때문입니다.
// 데이터 조회
List<Post> content = queryFactory.select(...).fetch();
// 개수 조회 (별도)
Long total = queryFactory.select(post.count()).fetchOne();
4. PostRepository 연결 (main Repository)
- 역할
- 기본 CRUD + 커스텀 검색 기능 모두 사용이 가능합니다.
- 나누는 이유는?
- 관심사의 분리
- 테스트 용이
- 재사용 가능
public interface PostRepository
extends JpaRepository<Post, Long>, PostRepositoryCustom {
// 기존 메서드들...
}
- PostRepositoryCustom을 상속받으면 끝!
5. 테스트
1. 전체 조회
GET /posts/search
2. 제목으로 검색
GET /posts/search?title=Spring
3. 복합 검색
GET /posts/search?title=JPA&categoryId=1
4. 페이징 + 정렬
GET /posts/search?title=Spring&page=0&size=10&sort=createdAt,desc
검색 결과 예시
데이터가 있을 때
{
"success": true,
"data": {
"content": [
{
"id": 1,
"title": "Spring Boot 입문",
"content": "Spring Boot를 시작하는 방법",
"authorName": "초",
"categoryName": "개발",
"viewCount": 5,
"createdAt": "2024-12-05T..."
}
],
"totalElements": 1,
"totalPages": 1,
"size": 10,
"number": 0
}
}
검색 결과가 없을 때
- 빈 배열을 받으면 프론트엔드에서 "검색 결과가 없습니다" 메시지를 표시하면 됩니다.
{
"success": true,
"data": {
"content": [],
"totalElements": 0,
"totalPages": 0
}
}
느낀 점
- QueryDSL 을 쓰면 컴파일 타임에 오류를 잡을 수 있다.(JPQL 은 오타가 나도 실행전까지는 모름)
- 파일을 왜이렇게 많이 만들어야 되지? 했는데 Custom Repository 패턴을 적용해보고 나니 검색 조건을 추가하거나 수정할때 필요한 Condition 부분만 수정하면돼서 확장성과 편리함이 좋았다.
- BooleanExpression 에서 null 리턴 시 조건이 무시되고 where 절을 만드는 부분이 신기했다.

'백엔드 > 게시판 만들기' 카테고리의 다른 글
| [Spring Boot] 게시판 - 12. JPA N+1 문제 해결하기 (EntityGraph vs Fetch Join) (0) | 2025.12.09 |
|---|---|
| [Spring Boot] 게시판 - 11. 동적 정렬과 필터링으로 사용자 경험 개선하기 (0) | 2025.12.09 |
| 초보 백엔드가 무중단 배포와 CI/CD를 직접 구축하면서 만난 에러들 (0) | 2025.12.04 |
| [Spring Boot] 게시판 - 09. 게시글 이미지 업로드 구현하기 (feat. 이미지 여러개 업로드 & 순서 관리) (0) | 2025.11.30 |
| [Spring Boot] 게시판 - 08. 파일 업로드 구현하기 (feat. 프로필 이미지 업로드) (0) | 2025.11.28 |