Spring Boot์์ ๋์ ์ฟผ๋ฆฌ ๊ฐ์ ํ๊ธฐ — @Query์์ QueryDSL๋ก
๋ชฉ์ฐจ
01. ๋ค์ด๊ฐ๋ฉฐ
Booktine ํ๋ก์ ํธ์์ ๊ฒ์๋ฌผ ๊ฒ์ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ฉด์ ๋์ ์ฟผ๋ฆฌ ์ฒ๋ฆฌ ๋ฐฉ์์ ๊ณ ๋ฏผํ๊ฒ ๋๋ค. ์๊ตฌ์ฌํญ์ ์ฌ์ฉ์๊ฐ ํค์๋(์ ๋ชฉ, ์ ์)์ ๋ ์ ์ํ(์ฝ๋ ์ค, ์๋ , ์ฝ๊ณ ์ถ์)๋ฅผ ์กฐ๊ฑด์ผ๋ก ๊ฒ์๋ฌผ์ ๊ฒ์ํ ์ ์์ด์ผ ํ๋ค. ๋ฌธ์ ๋ ๋ ์กฐ๊ฑด ๋ชจ๋ ์ ํ ์ฌํญ์ด๋ผ๋ ๊ฒ์ด๋ค. ํค์๋๋ง ์ ๋ ฅํ ์๋ ์๊ณ , ์ํ๋ง ์ ํํ ์๋ ์๊ณ , ์๋ฌด ์กฐ๊ฑด ์์ด ์ ์ฒด ์กฐํ๋ฅผ ํ ์๋ ์๋ค.
์ด๋ฐ ๋์ ์ฟผ๋ฆฌ ์ํฉ์์ ์ฒ์์๋ @Query๋ก ๊ตฌํํ๋ค๊ฐ QueryDSL๋ก ์ ํํ๊ณ , ๊ทธ ๊ณผ์ ์ ์ ๋ฆฌํ๋ค.
02. ์ฒ์ ๊ตฌํ — @Query
์ฒ์์๋ JPA @Query๋ก ๊ฐ๋จํ๊ฒ ์์ฑํ๋ค.
@Query("SELECT p FROM Post p WHERE p.user.id = :userId " +
"AND (:keyword IS NULL OR p.title LIKE %:keyword% OR p.author LIKE %:keyword%) " +
"AND (:status IS NULL OR p.readingStatus = :status)")
List<Post> searchPosts(Long userId, String keyword, ReadingStatus status);
๋์์ ํ์ง๋ง ์ฝ๋๋ฅผ ๋ณด๋ฉด์ ๋ช ๊ฐ์ง ๋ถํธํจ์ด ๋๊ปด์ก๋ค.
๋ฌธ์ ์
- ๋ฌธ์์ด ์ฟผ๋ฆฌ๋ผ ์ปดํ์ผ ํ์์ ์ค๋ฅ๋ฅผ ์ก์ ์ ์๋ค. p.title์ p.titlee๋ก ์คํ๋ฅผ ๋ด๋ ์ปดํ์ผ์ ํต๊ณผํ๊ณ ๋ฐํ์์์์ผ ์ค๋ฅ๊ฐ ๋๋ค.
- ์กฐ๊ฑด์ด ์ถ๊ฐ๋ ์๋ก ์ฟผ๋ฆฌ ๋ฌธ์์ด์ด ๊ธธ์ด์ง๋ค. ์ง๊ธ์ ์กฐ๊ฑด์ด 2๊ฐ์ง๋ง ์ฅ๋ฅด ํํฐ, ๋ ์ง ๋ฒ์ ๋ฑ์ด ์ถ๊ฐ๋๋ฉด ์ฟผ๋ฆฌ๊ฐ ํ๋์ ๋ค์ด์ค์ง ์๋๋ค.
- IS NULL ์ฒ๋ฆฌ ๋ฐฉ์์ด ์ด์ํ๋ค. ์กฐ๊ฑด์ด ์์ ๋๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด :keyword IS NULL OR ... ํจํด์ ๋ฐ๋ณตํด์ผ ํ๋ค.
03. QueryDSL ๋์
QueryDSL์ด๋?
QueryDSL์ ์๋ฐ ์ฝ๋๋ก ํ์ ์์ ํ๊ฒ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๊ฒ ํด์ฃผ๋ ํ๋ ์์ํฌ๋ค. ์ํฐํฐ ํด๋์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก QPost, QUser ๊ฐ์ Qํด๋์ค๋ฅผ ์๋ ์์ฑํ๊ณ , ์ด๋ฅผ ์ด์ฉํด IDE ์๋์์ฑ๊ณผ ์ปดํ์ผ ํ์ ๊ฒ์ฆ์ ์ง์ํ๋ค.
์์กด์ฑ ์ถ๊ฐ (build.gradle)
dependencies {
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'
}
def generated = 'src/main/generated'
sourceSets {
main.java.srcDirs += [generated]
}
tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(generated))
}
clean {
delete file(generated)
}
./gradlew compileJava๋ฅผ ์คํํ๋ฉด src/main/generated ๊ฒฝ๋ก์ Qํด๋์ค๊ฐ ์๋ ์์ฑ๋๋ค.
์ฐธ๊ณ : src/main/generated๋ ๋น๋ ์ ์๋ ์์ฑ๋๋ ํ์ผ์ด๋ฏ๋ก .gitignore์ ์ถ๊ฐํ์.
# QueryDSL
src/main/generated04. ๋น ๋ฑ๋ก ๋ฐ Repository ๊ตฌ์กฐ
JPAQueryFactory ๋น ๋ฑ๋ก
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Repository ๊ตฌ์กฐ
- PostRepository: JpaRepository + PostRepositoryCustom ์์
- PostRepositoryCustom: ์ธํฐํ์ด์ค
- PostRepositoryImpl: QueryDSL ๊ตฌํ์ฒด
PostRepositoryImpl.java ๊ตฌํ
@RequiredArgsConstructor
public class PostRepositoryImpl implements PostRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<Post> searchPosts(Long userId, String keyword, ReadingStatus status) {
QPost post = QPost.post;
BooleanBuilder builder = new BooleanBuilder();
// ํ์ ์กฐ๊ฑด: ์ฌ์ฉ์ ID
builder.and(post.user.id.eq(userId));
// ์ ํ ์กฐ๊ฑด: ํค์๋ (์ ๋ชฉ ๋๋ ์ ์)
if (keyword != null && !keyword.isBlank()) {
builder.and(
post.title.containsIgnoreCase(keyword)
.or(post.author.containsIgnoreCase(keyword))
);
}
// ์ ํ ์กฐ๊ฑด: ๋
์ ์ํ
if (status != null) {
builder.and(post.readingStatus.eq(status));
}
return queryFactory
.selectFrom(post)
.where(builder)
.orderBy(post.createdAt.desc())
.fetch();
}
}
05. @Query vs QueryDSL ๋น๊ต
| ๋น๊ต ํญ๋ชฉ | @Query | QueryDSL |
|---|---|---|
| ํ์ ์์ ์ฑ | โ ๋ฐํ์ ์ค๋ฅ ๊ฐ๋ฅ | โ ์ปดํ์ผ ํ์ ๊ฒ์ฆ |
| ๋์ ์ฟผ๋ฆฌ ๊ฐ๋ ์ฑ | โ ์กฐ๊ฑด ๋ง์์ง์๋ก ๋ณต์ก | โ BooleanBuilder๋ก ๊น๋ |
| ์กฐ๊ฑด ์ถ๊ฐ ํ์ฅ์ฑ | โ ์ฟผ๋ฆฌ ๋ฌธ์์ด ์ง์ ์์ | โ ์กฐ๊ฑด ๋ธ๋ก๋ง ์ถ๊ฐ |
| ์ค์ ๋ณต์ก๋ | โ ์ค์ ๋ถํ์ | โ ์์กด์ฑ/Qํด๋์ค ์ค์ |
| ํ์ต ๊ณก์ | โ ๋ฎ์ (SQL ์ ์ฌ) | โ ๋ค์ ์์ |
06. ํธ๋ฌ๋ธ์ํ — Qํด๋์ค ์์ฑ ์ค๋ฅ
[Error] cannot find symbol: QPost post = QPost.post;
์์ธ: APT๊ฐ Qํด๋์ค๋ฅผ ์์ฑํ ์ถ๋ ฅ ๊ฒฝ๋ก๊ฐ ์ค์ ๋์ง ์์ compileJava ์คํ ์ Qํด๋์ค๊ฐ ์์ฑ๋์ง ์์ ๊ฒ์ด์์ต๋๋ค.
ํด๊ฒฐ: build.gradle์ Qํด๋์ค ์์ฑ ๊ฒฝ๋ก ์ค์ ์ ์ถ๊ฐํ๊ณ ./gradlew clean compileJava๋ฅผ ๋ค์ ์คํํ๋ src/main/generated์ Qํด๋์ค๊ฐ ์ ์ ์์ฑ๋์ต๋๋ค.
07. ๋ง์น๋ฉฐ
ํ์ฌ ์กฐ๊ฑด์ด 2๊ฐ๋ฟ์ธ ๋จ์ํ ๊ฒ์ ๊ธฐ๋ฅ์์๋ @Query๋ก๋ ์ถฉ๋ถํ ๊ตฌํ ๊ฐ๋ฅํ๋ค. ํ์ง๋ง ์ดํ ์ฅ๋ฅด ํํฐ, ๋ ์ง ๋ฒ์ ๊ฒ์ ๋ฑ ์กฐ๊ฑด์ด ์ถ๊ฐ๋ ๊ฒ์ ๊ณ ๋ คํด QueryDSL์ ๋์ ํ๋ค. ์ด๊ธฐ ์ค์ ๋น์ฉ์ด ๋ค์ ์์ง๋ง ํ์ ์์ ์ฑ๊ณผ ํ์ฅ์ฑ ๋ฉด์์ ์ฅ๊ธฐ์ ์ผ๋ก ์ ์ง๋ณด์๊ฐ ํจ์ฌ ํธํ๋ค๋ ๊ฑธ ๋๊ผ๋ค.
'๐ป PROJECT > [Spring Boot, React] ๋ ์ ์ต๊ด ๊ด๋ฆฌ ์๋น์ค' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [ํ๋ก์ ํธ] ๋ฆฌ๋ง์ธ๋ ์๋ฆผ ๊ตฌํ ๋ฐฉ์ ์ ํ ๊ณผ์ (0) | 2026.05.01 |
|---|---|
| [ํ๋ก์ ํธ] ํ์ด์ง๋ค์ด์ ๋ฐฉ์ ์ ํ ๊ณผ์ (0) | 2026.05.01 |
| [ํ๋ก์ ํธ] ๊ณตํต ์๋ต ํฌ๋งท ์ค๊ณ (0) | 2026.04.30 |
| [ํ๋ก์ ํธ] ์ด๋ฏธ์ง ์ ๋ก๋ ์ ์ฑ ๊ฒฐ์ ๊ณผ์ (0) | 2026.04.30 |
| [Spring Boot, React] ๋ ์ ์ต๊ด ๊ด๋ฆฌ ์๋น์ค(1์ฐจ ๊ฐ๋ฐ) (0) | 2026.01.04 |