SpringPlus- 개인과제(7)

ChoRong0824·2025년 3월 14일
0

Web

목록 보기
42/51
post-thumbnail

QueryDSL

세팅이랑, 레포지토리 일부 수정이나 만들기만 하면 끝남. todo쪽에.


그 전에 앞서 했던 포스팅과 유사하게 필요한 개념을 먼저 정리하고 시작하도록 하겠습니다.

기본 개념 (필수라고 생각함)

1. QueryDSL

  • Spring Data JPA에서 JPQL을 보다 타입 안전하고 동적으로 생성할 수 있도록 도와주는 프레임워크.
  • JPQL과의 차이점
    - JPQL은 문자열 기반 → 런타임 오류 발생 가능성 있음.
    - QueryDSL은 코드 기반 (메서드 체이닝) → 컴파일 타임에 오류 감지 가능.

2. JPQL vs QueryDSL 비교

기존 JPQL 방식 (문자열 기반)

@Query("SELECT t FROM Todo t LEFT JOIN t.user WHERE t.id = :todoId")
Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);

QueryDSL 방식 (메서드 체이닝)

QTodo todo = QTodo.todo;
QUser user = QUser.user;

return queryFactory.selectFrom(todo)
        .leftJoin(todo.user, user).fetchJoin()
        .where(todo.id.eq(todoId))
        .fetchOne();

3. N+1 문제 해결 개념

LEFT JOIN FETCH 사용 시 한 번의 쿼리로 조회해야 함.
QueryDSL의 .fetchJoin()을 사용하면 EAGER FETCH와 같은 효과로 N+1 문제 해결 가능.


심화 개념 (더 깊이 공부할 것)

1. QueryDSL을 적용하려면 JpaRepository를 확장하는 QuerydslPredicateExecutor를 사용할 수도 있음.

2. QueryDSL에서 동적 검색 (BooleanBuilder, Predicate)을 활용할 수도 있음.

3.QueryDSL 설정이 필요함 → build.gradle에서 QueryDSL 설정 추가해야 함.

Spring Boot 3.x 버전과 호환되는 QueryDSL 5.x 버전을 명시적으로 추가하는 것이 좋습니다

dependencies {
    // QueryDSL
    implementation 'com.querydsl:querydsl-jpa:5.0.0' // QueryDSL JPA 의존성 추가
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa' // Q클래스 자동 생성
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0' // JPA 메타 모델을 위해 필요
}

4. Q클래스 생성 경로 설정

sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', 'build/generated/querydsl']
        }
    }
}

tasks.withType(JavaCompile) {
    options.annotationProcessorGeneratedSourcesDirectory = file('build/generated/querydsl')
}

Q클래스가 생성될 경로를 지정하고, 해당 경로를 소스 디렉토리로 인식시키기 위해 설정.

5. IDE 설정

  1. IDE 설정

IntelliJ IDEA를 사용하신다면, build/generated/querydsl 디렉토리를 'Generated Sources Root'로 설정하여 IDE가 해당 디렉토리를 소스 루트로 인식하도록 해야 합니다.

  • build/generated/querydsl 폴더에서 우클릭
  • Mark Directory as → Generated Sources Root 선택

이러한 설정을 통해 QueryDSL의 Q클래스가 자동으로 생성되고, QTodo, QUser 등의 클래스를 인식할 수 있게 됩니다.


QueryDSL 적용 코드 변경

  1. 기존 TodoRepository에서 JPQL을 제거하고 QueryDSL을 적용할 새로운 Custom Repository를 추가

  2. TodoRepositoryImpl에서 QueryDSL을 활용한 findByIdWithUser 구현

  3. QueryDSL을 적용하도록 TodoRepository 인터페이스를 변경

  4. TodoService에서 findByIdWithUser 호출 방식 변경


QTodo와 QUser에 빨간 밑줄이 그어지는 건 QueryDSL의 Q 클래스가 아직 생성되지 않았기 때문이라서 괜찮습니다.
이건 빌드 과정에서 자동으로 생성되는 파일이라서, 직접 만들 필요는 없습니다.

그래도 실행때 안된다면, 확인해줘야할 것들이 있습니다.
(위에 설정이 제대로 되어있는지 확인하면 됩니다. 위에 설정 잘못 되어있는 부분들 다 수정했습니다.)

implementation 'com.querydsl:querydsl-jpa'
    annotationProcessor "com.querydsl:querydsl-apt"
    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
    annotationProcessor "jakarta.annotation:jakarta.annotation-api"

에서

 //queryDSL
    implementation 'com.querydsl:querydsl-jpa:5.0.0' // QueryDSL JPA 의존성 추가
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa' // JPA 기반으로  Q 클래스 자동 생성
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0' // JPA 메타 모델을 위해 필요
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

등 수정사항들을 작성하려 했으나, 수정본만 올리도록 하겠습니다!
plugin 추가

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.3'
    id 'io.spring.dependency-management' version '1.1.6'
    id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10' //QueryDSL 플러그인 추가
}

build 파일 삭제하고 다시 실행.

Execution failed for task ':compileQuerydsl'.
> Annotation processor 'com.querydsl.apt.jpa.JPAAnnotationProcessor' not found

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.12.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD FAILED in 1s
4 actionable tasks: 4 executed

에러 발생.

Execution failed for task ':compileQuerydsl'.
> Annotation processor 'com.querydsl.apt.jpa.JPAAnnotationProcessor' not found

JPAAnnotationProcessor를 찾지 못하는 이유는 QueryDSL의 annotationProcessor 의존성이 올바르게 설정되지 않았거나, Gradle이 이를 제대로 인식하지 못하는 경우 발생합니다.


해결 과정

일단 build.gradle를 분석하는게 제일 좋다고 판단했습니다.

1. 플러그인

id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'

이 플러그인은 QueryDSL 관련 설정을 쉽게 관리할 수 있도록 도와줍니다.
QueryDSL을 사용할 때 수동으로 QClass(예: QTodo, QUser)를 생성하지 않아도 되도록 해줍니다.


2. queryDsl 관련 의존성 확인

// QueryDSL JPA 의존성 추가
implementation 'com.querydsl:querydsl-jpa:5.0.0' 

// JPA 기반으로 Q 클래스 자동 생성
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'

// JPA 메타 모델을 위해 필요
annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'

// Jakarta Annotation 처리
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

🔥!!중요!!🔥

annotationProcessor 설정이 제대로 적용되지 않으면 QClass가 생성되지 않습니다


3. QClass 생성 경로 지정

def querydslDir = "$buildDir/generated/querydsl"

QueryDSL의 Q클래스가 생성될 디렉토리를 설정합니다.
build/generated/querydsl/ 경로에 QTodo, QUser 같은 QClass 파일이 생성됩니다.


4. QueryDSL 플러그인 설정

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}

QueryDSL의 JPA 모드를 활성화하고, QClass 생성 디렉토리를 설정합니다.


5. 소스 디렉토리 추가

sourceSets {
    main {
        java {
            srcDirs += querydslDir
        }
    }
}

QClass가 생성된 querydslDir을 소스 디렉토리에 추가합니다.
why ? -> 그래야 QClass(QTodo, QUser)를 import할 때 Gradle이 인식할 수 있습니다.


6. Gradle 컴파일러 설정 (QClass 자동 생성)

tasks.withType(JavaCompile).configureEach {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

Java 컴파일 시, annotationProcessor가 querydslDir에 QClass를 생성하도록 설정합니다.


7. 빌드 시 QClass 생성 보장

tasks.named('compileJava') {
    dependsOn 'clean'
}

매 빌드 시 clean을 실행하여 QClass를 강제로 재생성합니다.
why ? -> QClass가 생성되지 않는 문제를 방지할 수 있음.


plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.3'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'org.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JPA 관련 의존성
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // Lombok (컴파일 시 필요)
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 데이터베이스 (H2, MySQL)
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'

    // JWT 관련 라이브러리
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

    // QueryDSL 의존성 (JPA)
    implementation 'com.querydsl:querydsl-jpa:5.0.0'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'

    // Jakarta Persistence API (javax.persistence.Entity 해결)
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
    implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'

    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
}

def querydslDir = "$buildDir/generated/sources/annotationProcessor/java/main"

tasks.withType(JavaCompile).configureEach {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

sourceSets {
    main {
        java {
            srcDirs += querydslDir
        }
    }
}

tasks.named('compileJava') {
    dependsOn 'clean'
}

https://developer-jinnie.tistory.com/21 를 참고하여,
순서대로 해봤음에도 실패.

적용 방법

1. 기존 build 디렉토리 삭제 후 재빌드

./gradlew clean build --stacktrace
➜  spring-plus git:(main) ✗ ./gradlew clean build --stacktrace
Starting a Gradle Daemon, 1 incompatible and 4 stopped Daemons could not be reused, use --status for details

> Task :compileJava FAILED
/Users/mun/Desktop/2025/spring-plus/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryQueryDslImpl.java:6: error: cannot find symbol
import org.example.expert.domain.todo.entity.QTodo;
                                            ^
  symbol:   class QTodo
  location: package org.example.expert.domain.todo.entity
/Users/mun/Desktop/2025/spring-plus/src/main/java/org/example/expert/domain/todo/repository/TodoRepositoryQueryDslImpl.java:8: error: cannot find symbol
import org.example.expert.domain.user.entity.QUser;
                                            ^
  symbol:   class QUser
  location: package org.example.expert.domain.user.entity

[Incubating] Problems report is available at: file:///Users/mun/Desktop/2025/spring-plus/build/reports/problems/problems-report.html

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileJava'.
> java.lang.NoClassDefFoundError: javax/persistence/Entity

* Try:
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':compileJava'.
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:130)
        at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:293)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:128)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:116)
        at org.gradle.api.internal.tasks.execution.ProblemsTaskPathTrackingTaskExecuter.execute(ProblemsTaskPathTrackingTaskExecuter.java:40)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:51)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:74)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
        at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
        at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:42)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:331)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:318)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.lambda$execute$0(DefaultTaskExecutionGraph.java:314)
        at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:314)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:303)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:459)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:376)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
Caused by: java.lang.RuntimeException: java.lang.NoClassDefFoundError: javax/persistence/Entity
        at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.invocationHelper(JavacTaskImpl.java:168)
        at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.doCall(JavacTaskImpl.java:100)
        at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.call(JavacTaskImpl.java:94)
        at org.gradle.internal.compiler.java.IncrementalCompileTask.call(IncrementalCompileTask.java:92)
        at org.gradle.api.internal.tasks.compile.AnnotationProcessingCompileTask.call(AnnotationProcessingCompileTask.java:94)
        at org.gradle.api.internal.tasks.compile.ResourceCleaningCompilationTask.call(ResourceCleaningCompilationTask.java:57)
        at org.gradle.api.internal.tasks.compile.JdkJavaCompiler.execute(JdkJavaCompiler.java:78)
        at org.gradle.api.internal.tasks.compile.JdkJavaCompiler.execute(JdkJavaCompiler.java:46)
        at org.gradle.api.internal.tasks.compile.daemon.AbstractIsolatedCompilerWorkerExecutor$CompilerWorkAction.execute(AbstractIsolatedCompilerWorkerExecutor.java:78)
        at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
        at org.gradle.workers.internal.AbstractClassLoaderWorker$1.create(AbstractClassLoaderWorker.java:54)
        at org.gradle.workers.internal.AbstractClassLoaderWorker$1.create(AbstractClassLoaderWorker.java:48)
        at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:100)
        at org.gradle.workers.internal.AbstractClassLoaderWorker.executeInClassLoader(AbstractClassLoaderWorker.java:48)
        at org.gradle.workers.internal.FlatClassLoaderWorker.run(FlatClassLoaderWorker.java:32)
        at org.gradle.workers.internal.FlatClassLoaderWorker.run(FlatClassLoaderWorker.java:22)
        at org.gradle.workers.internal.WorkerDaemonServer.run(WorkerDaemonServer.java:108)
        at org.gradle.workers.internal.WorkerDaemonServer.run(WorkerDaemonServer.java:77)
        at org.gradle.process.internal.worker.request.WorkerAction$1.call(WorkerAction.java:159)
        at org.gradle.process.internal.worker.child.WorkerLogEventListener.withWorkerLoggingProtocol(WorkerLogEventListener.java:41)
        at org.gradle.process.internal.worker.request.WorkerAction.lambda$run$1(WorkerAction.java:156)
        at org.gradle.internal.operations.CurrentBuildOperationRef.with(CurrentBuildOperationRef.java:85)
        at org.gradle.process.internal.worker.request.WorkerAction.run(WorkerAction.java:148)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
        at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
        at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:182)
        at org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection$DispatchWrapper.dispatch(MessageHubBackedObjectConnection.java:164)
        at org.gradle.internal.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:414)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)
Caused by: java.lang.NoClassDefFoundError: javax/persistence/Entity
        at com.querydsl.apt.jpa.JPAAnnotationProcessor.createConfiguration(JPAAnnotationProcessor.java:37)
        at com.querydsl.apt.AbstractQuerydslProcessor.process(AbstractQuerydslProcessor.java:82)
        at org.gradle.api.internal.tasks.compile.processing.DelegatingProcessor.process(DelegatingProcessor.java:62)
        at org.gradle.api.internal.tasks.compile.processing.IsolatingProcessor.process(IsolatingProcessor.java:50)
        at org.gradle.api.internal.tasks.compile.processing.DelegatingProcessor.process(DelegatingProcessor.java:62)
        at org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor.access$401(TimeTrackingProcessor.java:37)
        at org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor$5.create(TimeTrackingProcessor.java:99)
        at org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor$5.create(TimeTrackingProcessor.java:96)
        at org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor.track(TimeTrackingProcessor.java:117)
        at org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor.process(TimeTrackingProcessor.java:96)
        at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:1023)
        at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:939)
        at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1267)
        at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1382)
        at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1234)
        at jdk.compiler/com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:916)
        at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.lambda$doCall$0(JavacTaskImpl.java:104)
        at jdk.compiler/com.sun.tools.javac.api.JavacTaskImpl.invocationHelper(JavacTaskImpl.java:152)
        ... 32 more
Caused by: java.lang.ClassNotFoundException: javax.persistence.Entity
        ... 50 more


Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/8.12.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD FAILED in 6s
2 actionable tasks: 2 executed

문제를 보면 자꾸
javax.persistence.Entity 클래스를 찾을 수 없음
라고 뜨는데, javax.persistence.Entity를 찾을 수 없다는 것을 의미합니다.

원인

  • javax.persistence.Entity는 JPA API의 일부였으나, Spring Boot 3.x에서는 javax.persistence 대신 jakarta.persistence를 사용합니다.
  • QueryDSL의 JPAAnnotationProcessor가 javax.persistence를 찾으려 하지만, 프로젝트에서는 jakarta.persistence를 사용하고 있어 충돌 발생.

해결책

QueryDSL이 jakarta.persistence를 사용할 수 있도록 설정하면 될 것 같습니다.. ㅎㅎ...

그리고 에러 중간에 보면 Q클래스가 생성되지 않는데, 이건 쿼리dsl 어노테이션 프로세서가 실행되지 않았거나, 쿼리dsl이 javax.persistence를 요구하는데 jakarta.persistence를 사용하면서 충돌이 발생하는 것입니다.
이것 또한 앞서 말햇듯이 QueryDSL의 어노테이션 프로세서가 jakarta.persistence를 지원하도록 수정.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.3'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'org.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JPA 관련 의존성
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // Lombok (컴파일 시 필요)
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 데이터베이스 (H2, MySQL)
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'

    // JWT 관련 라이브러리
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

    // bcrypt
    implementation 'at.favre.lib:bcrypt:0.10.2'

    // QueryDSL 의존성 (JPA)
    implementation 'com.querydsl:querydsl-jpa:5.0.0'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'

    // Jakarta Persistence API (javax.persistence.Entity 해결)
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
    implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

    //test
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

}

def querydslDir = "$buildDir/generated/sources/annotationProcessor/java/main"

tasks.withType(JavaCompile).configureEach {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

sourceSets {
    main {
        java {
            srcDirs += querydslDir
        }
    }
}

tasks.named('compileJava') {
    dependsOn 'clean'
}

이것을 수정해야함.
보면,QueryDSL과 Jakarta Persistence 설정 관련 문제 때문에 여전히 javax.persistence.Entity 관련 에러가 발생할 가능성이 있기 때문에 수정이 필요함.

annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'

에서 수정이 필요함.
위 설정이 QueryDSL의 어노테이션 프로세서(querydsl-apt) 와 충돌을 일으킬 가능성이 있음. 현재 QueryDSL이 jakarta.persistence 기반으로 동작할 수 있도록 명확하게 설정되지 않았기 떄문에 수정해주면 됩니다.

// Jakarta Persistence API (javax.persistence.Entity 해결)
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api' // QueryDSL이 jakarta.persistence를 사용하도록 보장
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' 이 부분은 필요 없음. annotationProcessor에만 추가하면 해결되기 떄문에 위와같이 수정해주면 됩니다.


쿼리dsl의 Qclass 자동 생성 경로 설정

def querydslDir = "$buildDir/generated/sources/annotationProcessor/java/main"
이렇게 해주면 제대로 적용되지 않을 가능성이 있음.
QueryDsl이 생성한 Qclass가 프로젝트의 소스 경로로 제대로 인식되지 않으면,
빌드시 QTodo, QUser를 찾을 수 없는 오류가 발생됨.

그래서,

def querydslDir = "$buildDir/generated/querydsl"

위와 같이 설정하고 소스 세트(sourceSets.main.java.srcDirs += querydslDir) 를 올바르게 추가해야 함.


compileJava 태스크 설정 문제

tasks.named('compileJava') {
    dependsOn 'clean'
}

이렇게 설정하면 매번 빌드할 때마다 clean을 수행하므로 빌드 속도가 느려짐.
따라서, 쿼리dsl 관련 파일이 삭제되는 부작용도 있을 수 있으니까, 수정.

tasks.withType(JavaCompile).configureEach {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

이와 같이 설정하면 clean 없이도 QueryDsl 의 Q클래스를 자동 생성하고, 올바른 디렉토리로 저장되게 됨.

정리

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.3'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'org.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JPA 관련 의존성
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // Lombok (컴파일 시 필요)
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 데이터베이스 (H2, MySQL)
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'

    // JWT 관련 라이브러리
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

    // bcrypt
    implementation 'at.favre.lib:bcrypt:0.10.2'

    // QueryDSL 의존성 (JPA)
    implementation 'com.querydsl:querydsl-jpa:5.0.0'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'

    // Jakarta Persistence API (javax.persistence.Entity 해결)
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api' // QueryDSL이 jakarta.persistence를 사용하도록 보장
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

    //test
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

}

def querydslDir = "$buildDir/generated/querydsl"

tasks.withType(JavaCompile).configureEach {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

sourceSets {
    main {
        java {
            srcDirs += querydslDir
        }
    }
}

tasks.withType(JavaCompile).configureEach {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

코드에 중복 발견. 수정

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.3'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'org.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JPA 관련 의존성
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // Lombok (컴파일 시 필요)
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 데이터베이스 (H2, MySQL)
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'

    // JWT 관련 라이브러리
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

    // bcrypt
    implementation 'at.favre.lib:bcrypt:0.10.2'

    // QueryDSL (JPA 지원)
    implementation 'com.querydsl:querydsl-jpa:5.0.0'  // QueryDSL JPA 버전
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'  // JPA 기반으로 Q 클래스 생성

    // Jakarta Persistence API (javax.persistence.Entity 문제 해결)
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

    // 테스트 의존성
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

// QueryDSL Q 클래스 생성 경로
def querydslDir = "$buildDir/generated/querydsl"

tasks.withType(JavaCompile).configureEach {
    options.annotationProcessorGeneratedSourcesDirectory = file(querydslDir)
}

sourceSets {
    main {
        java {
            srcDirs += querydslDir
        }
    }
}

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.3'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'org.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JPA 관련 의존성
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // Lombok (컴파일 시 필요)
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // 데이터베이스 (H2, MySQL)
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'

    // JWT 관련 라이브러리
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

    // bcrypt
    implementation 'at.favre.lib:bcrypt:0.10.2'

    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"

    // 테스트 의존성
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

로 튜터님께 문의해서 수정했는데, 이건 QEntitY는 생성되는데, 콘솔창에 에러가 발생했습니다.
이에 설정 파일이 저랑 무엇이 다른지 확인해보고, 이를 분석해보고 콘솔창 에러를 해결해보겠습니다.


  1. 쿼리 Dsl 버전 및 자카르타 설정 차이
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    이렇게 수정했는데, 직접 자카르타 버전을 명시
    (이전 코드)
    implementation 'com.querydsl:querydsl-jpa:5.0.0 + :jpa
    별도로 자카르타.persistence 추가

  2. Q 클래스 자동 생성 관련 설정

  • Q 클래스를 자동으로 생성할 경로를 따로 설정하지 않았음 (수정된 코드)
  • Q 클래스 자동 생성 경로 명확하게 설정(수정 전 코드)
  1. QueryDsl 어노테이션 프로세서 설정 차이
  • 수정된 코드 annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" => 자카르타 버전 지정
    (이전코드) annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'
  • 이전 코드에선 jpa버전을 지정하고 있었습니다. 이 부분 때문에 Q클래스가 자동 생성되지 않았던 것으로 확인됩니다.

Err

2025-03-14T20:47:44.961+09:00  INFO 21544 --- [foodduck] [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2025-03-14T20:47:45.244+09:00  WARN 21544 --- [foodduck] [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'commentController' defined in file [/Users/mun/Desktop/2025/spring-plus/out/production/classes/org/example/expert/domain/comment/controller/CommentController.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'commentService' defined in file [/Users/mun/Desktop/2025/spring-plus/out/production/classes/org/example/expert/domain/comment/service/CommentService.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'todoRepository' defined in org.example.expert.domain.todo.repository.TodoRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Cannot resolve reference to bean 'jpa.TodoRepository.fragments#0' while setting bean property 'repositoryFragments'
2025-03-14T20:47:45.244+09:00  INFO 21544 --- [foodduck] [           main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2025-03-14T20:47:45.247+09:00  INFO 21544 --- [foodduck] [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2025-03-14T20:47:45.251+09:00  INFO 21544 --- [foodduck] [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
2025-03-14T20:47:45.252+09:00  INFO 21544 --- [foodduck] [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2025-03-14T20:47:45.262+09:00  INFO 21544 --- [foodduck] [           main] .s.b.a.l.ConditionEvaluationReportLogger : 

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2025-03-14T20:47:45.275+09:00 ERROR 21544 --- [foodduck] [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in org.example.expert.domain.todo.repository.TodoRepositoryQueryDslImpl required a bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' that could not be found.


Action:

Consider defining a bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' in your configuration.


Process finished with exit code 1

콘솔을 보면

Description:

Parameter 0 of constructor in org.example.expert.domain.todo.repository.TodoRepositoryQueryDslImpl required a bean of type 'com.querydsl.jpa.impl.JPAQueryFactory' that could not be found.

JpaQueryFactory를 찾을 수 없다는 것을 확인할 수 있습니다.
즉, TodoRepostiroyQueryDslImpl 클래스에서 의존성 주입을 바등려는 JPAQueryFactory가 빈으로 등록되지 않아서 발생한 것입니다.
(bean of type 'com.querydsl.jpa.impl.JPAQueryFactory)

쿼리 dsl을 사용할 때, 직접 빈으로 등록해주면 해결되는 것입니다.

@Configuration
public class QueryDslFactory {

    @PersistenceContext
    private EntityManager entityManager;


    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

저는 이렇게 추가해줬습니다.
빈으로 관리하도록 설정했고, 엔티티 매니저를 persistenceContext로 주입해서 jpa쿼리팩토리에 연결했습니다. 그 후, jpa쿼리 팩토리를 빈으로 등ㄹ고하면, 쿼리 dsl을 사용하는 리포에선 자동으로 주입 가능해지는 것입니다.
(사실 이렇게만 하면 해결된거임)


참고해서 공부할만한 포스팅 1, 2

profile
백엔드를 지향하며, 컴퓨터공학과를 졸업한 취준생입니다. 많이 부족하지만 열심히 노력해서 실력을 갈고 닦겠습니다. 부족하고 틀린 부분이 있을 수도 있지만 이쁘게 봐주시면 감사하겠습니다. 틀린 부분은 댓글 남겨주시면 제가 따로 학습 및 자료를 찾아봐서 제 것으로 만들도록 하겠습니다. 귀중한 시간 방문해주셔서 감사합니다.

0개의 댓글