๐Ÿ’ป PROJECT/[Spring Boot, React] ๋…์„œ ์Šต๊ด€ ๊ด€๋ฆฌ ์„œ๋น„์Šค

[ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…] Spring Boot์—์„œ ๋™์  ์ฟผ๋ฆฌ ๊ฐœ์„ ํ•˜๊ธฐ: @Query์—์„œ QueryDSL๋กœ

devCloud 2026. 4. 29. 13:45
728x90
TROUBLE SHOOTING

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/generated

04. ๋นˆ ๋“ฑ๋ก ๋ฐ 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์„ ๋„์ž…ํ–ˆ๋‹ค. ์ดˆ๊ธฐ ์„ค์ • ๋น„์šฉ์ด ๋‹ค์†Œ ์žˆ์ง€๋งŒ ํƒ€์ž… ์•ˆ์ „์„ฑ๊ณผ ํ™•์žฅ์„ฑ ๋ฉด์—์„œ ์žฅ๊ธฐ์ ์œผ๋กœ ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ํ›จ์”ฌ ํŽธํ•˜๋‹ค๋Š” ๊ฑธ ๋А๊ผˆ๋‹ค.

Last Sync: 2026-04-29 | Booktine Project Log
728x90