[Spring Plus] Level 2-3 요구사항 반영

2024. 11. 15. 11:04내일배움캠프/Spring Plus

 'Level 2-3' 의 요구사항을 반영한 내용을 기록한 포스팅이다. 어떠한 생각과 과정을 통해 요구사항을 반영했는지 알 수 있도록 작성해 보았다.

 

0. 요구사항

 현재 일정 조회(단건)시 사용되는 'TodoRepository' 인터페이스의 'findByIdWithUser()' 는 JPQL 을 사용해 DB 에서 정보를 조회하고 있다. 이제는 일정 조회시 QueryDSL 을 사용해 DB 에 일정 조회 쿼리를 날리는 방식을 사용할 수 있게 코드를 수정해야 한다.

 

 

1. QueryConfig 생성

 우선 QueryDSL 을 사용하려면 'build.gradle' 의 'dependencies' 에 의존성을 아래와 같이 추가해 주어야 한다. 해당 부분은 프로젝트 초기 설정 당시 추가해둔 상태이다.

implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta"

 

다음으로 Repository 에서 QueryDSL 을 사용하기 위해선 JPAQueryFactory 객체를 활용해야 하는데, 해당 객체를 Repository 별로 별도로 생성해 사용하는 것은 리소스 낭비라 판단 해당 객체를 생성하는 Config 클래스를 생성해 Bean 으로 등록하기로 하였다. 그래서 'org.example.expert.config' 패키지에 아래의 QueryConfig 클래스를 생성했다.

@Configuration
public class QueryConfig {
    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
        return new JPAQueryFactory(entityManager);
    }
}

 

생성자에 @Bean 을 사용해 JPAQueryFactory 객체를 빈으로 등록하고 Repository 에서는 해당 빈을 주입 받도록 할 것이다.

 

 

2. QueryTodoRepository(Impl) 수정

 Repository 의 경우 Level 1-5 요구사항 반영시 생성한 QueryTodoRepository(Impl) 를 활용하였다.

public interface QueryTodoRepository {
    ...
    
    Todo findByTodoId(Long todoId);
}


@Repository
@RequiredArgsConstructor
public class QueryTodoRepositoryImpl implements QueryTodoRepository {
    private final EntityManager entityManager;
    private final JPAQueryFactory jpaQueryFactory;

    ...

    @Override
    public Todo findByTodoId(Long todoId) {
        return Optional.ofNullable(jpaQueryFactory
                        .selectFrom(todo)
                        .join(todo.user, user).fetchJoin()
                        .where(todo.id.eq(todoId))
                        .fetchOne())
                .orElseThrow(() -> new InvalidRequestException("Todo not found"));
    }
}

 

패스 파라미터로 전달 받은 'todoId(일정 ID)' 와 일치하는 'id' 를 갖는 엔티티를 DB 에서 조회하는 'findByTodoId()' 를 작성해 주었다. 또한 TodoService 클래스의 'getTodo()' 의 경우 Repository 에서 반환 받은 엔티티를 통해 응답 DTO 생성시 Todo 와 연관관계인 User 의 정보 또한 접근하기에 'fetch join()' 을 사용해 한 번에 두 엔티티의 모든 정보를 가져올 수 있도록해 N+1 문제를 방지했다.

 

그리고 반환 타입의 경우 Todo 를 사용하고 조회한 엔티티에 'Optional.ofNullable()' 을 사용하게 되면서 더 이상 TodoService 의 'getTodo()' 에서 조회한 엔티티의 null 유무를 확인할 필요가 없어졌다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TodoService {
    ...

    public TodoResponse getTodo(long todoId) {
        Todo todo = todoRepository.findByTodoId(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()
        );
    }
}

 

 

3. 테스트

 Postman 을 통해 일정 조회에 대한 API 테스트를 진행했다.

일정 조회 요청

 

요청이 잘 수행되어 'id = 1' 인 일정에 대한 정보가 잘 반환된 것을 확인할 수 있었다. 또한 API 요청시 작성되는 쿼리 또한 확인해 보았는데

요청 수행시 작성된 쿼리

 

하나의 쿼리가 사용된 것을 확인할 수 있었다. DB 조회시 'fetch join()' 을 사용했기에 Todo 엔티티의 User 정보가 프록시 객체가 아닌 실제 User 객체이므로 TodoService 에서 User 정보에 접근하더라도 추가 쿼리가 작성되지 않아 이렇게 하나의 쿼리로 요청을 수행할 수 있게 된 것이다.