Today I Learned
Spring 플러스 주차 개인 과제
#QueryDSL을 활용한 조건 분기 검색 기능 리팩토링하기
이번 과제에서는 할 일(Todo) 엔티티를 조회할 때 연관된 사용자(User) 정보도 함께 가져오는 기능을 QueryDSL을 활용해 구현해보았다.
목표
QueryDSL을 활용해 Todo를 조회하면서 연관된 User 정보를 한 번의 쿼리로 함께 가져오기
todoId로 조회하되, 존재하지 않으면 예외 처리
코드 가독성과 유지보수성을 높이기
구현 방식
1. QueryDSL 설정
@Configuration
public class QuerydslConfig {
private final EntityManager entityManager;
public QuerydslConfig(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
QueryDSL을 사용하려면 `JPAQueryFactory`를 빈으로 등록해줘야 한다.
이 설정 클래스 덕분에 커스텀 레포지토리에서 `JPAQueryFactory`를 주입받아 사용할 수 있다.
2. QueryDSL 설정 완료 후, 커스텀 레포지토리 구조 도입
public interface TodoRepositoryCustom {
Todo findByIdWithUser(Long todoId);
}
public class TodoRepositoryImpl implements TodoRepositoryCustom {
private final JPAQueryFactory jpaQueryFactory;
public TodoRepositoryImpl(JPAQueryFactory jpaQueryFactory) {
this.jpaQueryFactory = jpaQueryFactory;
}
@Override
public Todo findByIdWithUser(Long todoId) {
QTodo todo = QTodo.todo;
QUser user = QUser.user;
return Optional.ofNullable(
jpaQueryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(todo.id.eq(todoId))
.fetchOne()
).orElseThrow(() -> new InvalidRequestException("Todo not found"));
}
}
`fetchOne()`은 `null`을 반환할 수 있기 때문에, `Optional`로 감싸서 예외를 던지는 방식이 null-safe하고 깔끔하다.
`fetchJoin()`을 활용하면 연관 엔티티를 한 번의 쿼리로 가져옴으로써 N+1 문제를 방지할 수 있다.
`QTodo`: `QueryDSL`이 엔티티 클래스를 기반으로 자동 생성하는 쿼리용 클래스다.
예를 들어 `Todo` 엔티티가 있으면 `QTodo`가 생성되며, 이를 통해 타입 안전한 쿼리를 작성할 수 있다.
`TodoRepositoryImpl` 클래스는 따로 호출하지 않아도 Spring이 자동으로 연결해준다.
이유는 이름 규칙 덕분인데, `TodoRepositoryCustom`이라는 인터페이스가 있으면 Spring은 `TodoRepositoryImpl`이라는 이름의 클래스를 자동으로 찾아서 연결한다.
그래서 `@Repository`를 붙이지 않아도 정상 동작한다.
3. TodoRepository에서 커스텀 인터페이스 확장
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryCustom {
}
확장만 해주면 `TodoRepository`를 통해 QueryDSL 기반의 커스텀 메서드를 바로 사용할 수 있다.
4. 서비스 레이어에서 활용
@Transactional(readOnly = true)
public TodoResponse getTodo(long todoId) {
Todo todo = todoRepository.findByIdWithUser(todoId);
User user = todo.getUser();
return new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(user.getId(), user.getEmail(), user.getNickname()),
todo.getCreatedAt(),
todo.getModifiedAt()
);
}
서비스에서는 `TodoRepository`를 통해 `findByIdWithUser()`를 호출하고, 연관된 `User` 정보까지 함께 가져온다.
마치며
이번 구현을 통해 QueryDSL의 기본적인 사용법뿐만 아니라, 연관 엔티티를 효율적으로 조회하는 방법과 Spring의 커스텀 레포지토리 연결 방식까지 함께 익힐 수 있었다. 특히 `fetchJoin()`과 `Optional` 패턴을 활용한 예외 처리 방식은 실무에서도 자주 쓰이는 패턴이라, 앞으로 복잡한 조회 로직이나 성능 최적화가 필요한 상황에서 QueryDSL을 적극적으로 활용할 수 있을 것 같다.