요구사항
- JPQL로 작성된 `findByIdWithUser`를 QueryDSL로 변경하기
- N+1 문제가 발생하지 않도록 유의하기
1. 빌드 환경 구성
- build.gradle에 해당 코드 추가 (`build.gradle`)
- Q파일 생성 경로 지정 (`build.gradle`)
dependencies {
// queryDSL 설정
implementation "com.querydsl:querydsl-jpa:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
implementation "com.querydsl:querydsl-core"
implementation "com.querydsl:querydsl-collections"
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" // querydsl JPAAnnotationProcessor 사용 지정
annotationProcessor "jakarta.annotation:jakarta.annotation-api" // java.lang.NoClassDefFoundError (javax.annotation.Generated) 대응 코드
annotationProcessor "jakarta.persistence:jakarta.persistence-api" // java.lang.NoClassDefFoundError (javax.annotation.Entity) 대응 코드
}
// QueryDSL 설정
// QClass 파일이 기본 경로(src/main/generated)가 아닌 다른 위치에 생성될 경우,
// IntelliJ가 빌드된 클래스 파일을 중복으로 스캔하면서 충돌이 발생할 수 있으므로
// QClass 생성 위치를 명시적으로 지정해 충돌 방지
def generated = 'src/main/generated'
// QueryDSL의 QClass 생성 경로 지정
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}
// 생성된 QClass 경로를 Java 소스 경로에 추가하여 인식 가능하게 함
sourceSets {
main.java.srcDirs += [ generated ]
}
// gradle clean 시 생성된 QClass 디렉토리도 함께 삭제
clean {
delete file(generated)
}
- Gradle Wrapper 수동 설치
- Q파일 생성 작업을 하려는데, gradle-wrapper 필수파일이 빠져있다는 에러가 떠서 직접 작성함
- https://github.com/gradle/gradle/tree/master/gradle/wrapper
해당 깃을 통해 다운받은 wrapper 파일을 내 프로젝트에 복사하였음
- 🌟QuertdslConfig 파일 생성
- `JPAQueryFactory `는 QueryDSL의 쿼리 실행 엔진.
- 이 코드를 통해 Spring이 `JPAQueryFactory`를 스프링빈으로 등록함
- 이거 없으면 TodoRepository에서 `JPAQueryFactory`를 생성자 주입 못받아서 오류 발생
( No qualifying bean of type JPAQueryFactor )
@Configuration
public class QuerydslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
2. QueryDSL로 전환
- 기존 JPQL 방식 코드 수정
- `findByIdWithUser()`는 커스텀인터페이스에서 이미 정의되었기 때문에 여기에서 선언하지 않고도 사용 가능(`extends`)
- JPQL로 작성된 메서드를 제거하면 QueryDSL 방식으로 완전히 바뀌어서 n+1문제 방지할 수 있음
// 기본 CRUD용 repository
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryCustom {
// @Query("SELECT t FROM Todo t " +
// "LEFT JOIN t.user " +
// "WHERE t.id = :todoId")
// Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);
}
- QueryDSL 방식 코드 작성
// 사용자 정의 인터페이스 : 동적 검색용 로직
public interface TodoRepositoryCustom {
Optional<Todo> findByIdWithUser(Long todoId); // QueryDSL 구현
}
// Custom에 선언된 메서드를 JPQL로 직접 구현
@Repository
@RequiredArgsConstructor // queryFactory가 생성자로 주입됨
public class TodoRepositoryImpl implements TodoRepositoryCustom {
private final JPAQueryFactory queryFactory;
// QueryDSL 구현
@Override
public Optional<Todo> findByIdWithUser(Long todoId) {
// todoId로 특정todo 조회할때 연결된 User정보까지 같이 조회
// Q클래스 : QueryDSL이 엔티티를 바탕으로 자동 생성한 쿼리전용클래스
QTodo todo = QTodo.todo;
QUser user = QUser.user;
Todo result = queryFactory
.selectFrom(todo)
// todo조회할거야
.leftJoin(todo.user, user).fetchJoin()
// todo의 user필드 기준으로 user테이블과 leftjoin
// fetchjoin : 연관된 user객체도 한번에 가져와(n+1 방지)
.where(todo.id.eq(todoId))
// todo.id와 입력받은 todoid가 같을때
.fetchOne();
// 결과 1건 가져와
return Optional.ofNullable(result);
// 결과가 null일수도 있으니까 Optional로 감싸서 반환
}
}
🌕🌕🌕 하루 회고
JPQL과 QueryDSL의 개념에 대해서는 배웠었는데 직접 코드를 리팩토링하는건 처음이였다. 단순히 문자열 쿼리만 바꿔적어주면 될 줄 알았는데 생각보다 환경설정이 복잡했다. gradle/wrapper 오류는 나만 나는건지, 모두가 나는건지 모르겠다. 그래도 환경 설정은 한번 잘 해두면 앞으로 안해도 되는거니까 잘 마쳤다. 내가 느낀 리팩토링 포인트는 QuerydslConfig에서 JPAQueryFactory를 빈으로 등록해줘야 생성자 주입이 가능하다는 점! 따로 QuerydslConfig 파일을 안 만들었더니 자꾸 생성자 주입이 안된다는 오류가 나서 QueryDSL은 어디서 생성자 주입을 해주어야하나 한참을 구글링했다.. 이부분을 놓쳐서 한번 오류가 났지만, 해결하고나니까 오히려 흐름을 이해하는데 도움이 된 듯 하다. 일단은 간단한 메서드 하나만 전환하는 식으로 리팩토링 해보았는데, 문자열 대신에 객체로 쿼리를 짠다는 점에서 만족스럽다.
LIST
'👩🏻💻Project' 카테고리의 다른 글
| 🚈실시간 기차 예매 서비스 프로젝트의 Docker 관련 트러블슈팅 (0) | 2025.05.26 |
|---|---|
| [JWT] 커스텀 필터와 Resolver 기반의 인증 구조를 Spring Security로 리팩토링하기! (0) | 2025.05.11 |
| [JAVA/Spring] DeliSpring 프로젝트 정리본 및 트러블슈팅 (0) | 2025.04.29 |
| [JPA] ⏰타이머 과제 (1) | 2025.04.21 |
| [Java/Spring] postory 프로젝트 트러블슈팅🚀 및 회고 (1) | 2025.04.11 |