[문제]
QueryDSL을 사용해서 조회할 때 특정 검색어가 없으면 일정 목록이 전부 조회되어야 하는데 이상하게 '403 Forbidden' 오류 메시지가 떴다. 메시지야 직접 설정하지 않았으니 그렇다 쳐도, 어디선가 문제가 생겼다는 뜻이라 눈을 동그랗게 뜨고 원인을 찾았다.
[원인]
private long countByTitle(String search) {
return Optional.ofNullable(
jpaQueryFactory.select(Wildcard.count)
.from(todo)
.where(todo.title.contains(search))
.fetchOne()
)
.orElse(0L);
}
오류 메시지를 한 줄씩 꼼꼼하게 읽은 결과, 'Cannot invoke "Object.getClass()" because "constant" is null' 메시지가 눈에 띄었다. 즉, 특정 검색어를 입력하지 않으면, null인 'search'가 contains() 메서드(method)에 들어가서 오류가 발생했다. null이 들어가면 안 된다는 점은 Javadoc에서도 확인할 수 있었다.
[해결]
/**
* 검색 조건에 맞는 일정의 총 개수 반환
* 결과가 없으면 0 반환
*/
private long countTotalElements(TodoSearchRequestDto dto) {
return Optional.ofNullable(
jpaQueryFactory.select(todo.count()) // 일정 Entity 개수 조회
.from(todo) // 일정 테이블에서
.where( // 검색 조건 추가
containsTitle(dto.getTitle()), // (1) 일정 제목
goeStartsAt(dto.getStartsAt()), // (2) 검색 시작일
loeEndsAt(dto.getEndsAt()) // (3) 검색 종료일
)
.fetchOne() // 총 개수 값 반환 (결과가 없을 수 있음)
)
.orElse(0L);
// 결과가 없으면 0 반환
// Optional을 사용하여 null 안전성 처리
}
원인을 찾은 다음엔 일정 제목뿐만 아니라 특정한 조건으로 조회한 일정의 총 개수 값을 정확하게 반환할 수 있도록 메서드를 수정했다. 특히 조건에 맞는 일정이 없어서 값이 없을 때는 null을 안전하게 처리하고자 Optional.ofNullable을 사용했으며, 'orElse(0L)'이라고 코드를 작성하여 Optional이 비었을 때는 기본값인 0을 반환하도록 했다.
package org.example.expert.domain.todo.repository;
import static org.example.expert.common.entity.QTodo.todo;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.example.expert.common.entity.Todo;
import org.example.expert.domain.todo.dto.request.TodoSearchRequestDto;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class TodoQueryRepository {
private final JPAQueryFactory jpaQueryFactory;
// 주어진 조건에 맞는 일정 리스트를 페이징 처리하여 반환
public Page<Todo> search(
TodoSearchRequestDto dto,
Pageable pageable
) {
List<Todo> todoList = new ArrayList<>();
todoList = findTodoList(dto, pageable);
long totalElements = countTotalElements(dto);
return new PageImpl<>(
todoList,
pageable,
totalElements
);
}
// 검색 조건에 맞는 일정 목록을 조회
private List<Todo> findTodoList(
TodoSearchRequestDto dto,
Pageable pageable
) {
List<Todo> todoList = new ArrayList<>();
todoList = jpaQueryFactory.selectFrom(todo)
.leftJoin(todo.user).fetchJoin()
.where(
containsTitle(dto.getTitle()),
goeStartsAt(dto.getStartsAt()),
loeEndsAt(dto.getEndsAt())
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(todo.createdAt.desc())
.fetch();
return todoList;
}
// 제목이 주어진 값으로 포함되는 일정을 찾는 조건 생성
private BooleanExpression containsTitle(String title) {
if (title == null) {
return null;
}
return todo.title.contains(title);
}
// 시작 시간이 주어진 값 이후인 일정을 찾는 조건 생성
private BooleanExpression goeStartsAt(LocalDateTime startsAt) {
if (startsAt == null) {
return null;
}
return todo.createdAt.goe(startsAt);
}
// 종료 시간이 주어진 값 이전인 일정을 찾는 조건 생성
private BooleanExpression loeEndsAt(LocalDateTime endsAt) {
if (endsAt == null) {
return null;
}
return todo.createdAt.loe(endsAt);
}
// 검색 조건에 맞는 일정의 총 개수 반환
private long countTotalElements(TodoSearchRequestDto dto) {
return Optional.ofNullable(
jpaQueryFactory.select(todo.count())
.from(todo)
.where(
containsTitle(dto.getTitle()),
goeStartsAt(dto.getStartsAt()),
loeEndsAt(dto.getEndsAt())
)
.fetchOne()
)
.orElse(0L);
}
}
메서드를 수정한 다음, 가장 먼저 아무런 조건 없이 검색했을 때 데이터베이스(database)에 저장된 모든 일정이 조회되는지, totalElements가 올바르게 출력되는지 점검했다. 다행히 메서드를 고친 뒤에는 문제없이 일정 목록이 조회되었다.
두 번째로는 일정 제목만 입력했을 때와 일정 생성일 범위만 설정했을 때 각 조건에 맞게 일정 목록이 조회되는지 점검했고, 'N+1' 문제나 오류 없이 조회하는 데에 성공했다. 차츰차츰 막막하게만 느껴진 queryDSL에 한걸음 가까워진 기분이 들었다.
'Troubleshooting: 무엇이 문제였는가? > 본캠프 6주 차: 플러스 프로젝트' 카테고리의 다른 글
10단계: "세상에, 'HttpMessageNotWritableException'이라니! 순환 참조에 걸린 사람? 저요!" (수정 중) (0) | 2025.01.21 |
---|---|
10단계: Null이 아니라 널 보고 싶어요, 이메일 씨 (0) | 2025.01.20 |