안녕하세요 이번 시간에는 QueryDSL을 사용하여 게시물 카테고리로 게시물을 검색하는 기능을 만들어 보겠습니다.

QueryDSL을 사용하기위해 build.gradle에 라이브러리를 추가해주도록 하겠습니다.

plugins {
    id 'org.springframework.boot' version '2.4.5'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'

    //querydsl 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

group = 'hose'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // test용 assertj 추가
    testCompile 'org.assertj:assertj-core:3.19.0'

    // spring security 추가
    implementation 'org.springframework.boot:spring-boot-starter-security'

    // jwt
    compile group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
    runtime group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
    runtime group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

    // swagger
    compile('io.springfox:springfox-swagger2:2.7.0')
    compile('io.springfox:springfox-swagger-ui:2.7.0')

    //querydsl 추가
    implementation 'com.querydsl:querydsl-jpa'
}

test {
    useJUnitPlatform()
}

// querydsl 추가 시작
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}
// querydsl 추가 끝

 

설치를 하고 나서 IntelliJ를 사용하신다면 오른쪽 Gradle에서 cleanQuerydslSourcedDir을 눌러 줍니다.

 

그 후 other에서 compileQuerydsl을 해줍니다 그러고 나면 build > generated > querydsl 디렉토리가 생기며 내부에 Q가 붙은 엔티티가 생깁니다.

 

만약 IntelliJ가 없다면 ./gradlew cleanQuerydslSourcesDir 과 ./gradlew compileQuerydsl 을 명령어를 실행시켜 줍니다.

 

그 다음 PostController, SearchDTO,  PostRepositoryCustom, PostRepositoryImpl 을 만들어주고 PostRepository를 수정해줍니다.

- PostController

@GetMapping("/board/post-list")
    @ResponseStatus(value = HttpStatus.OK)
    @ApiOperation(value = "게시물 목록 조회", notes = "게시물 목록을 조회합니다.")
    public SuccessResponse<List<PostDTO>> getPostList(SearchDTO searchDTO) {
        List<PostDTO> postList = postService.getPostList(searchDTO);

        return SuccessResponse.success(postList);
    }

 

- SearchDTO

@Builder
@Data
public class SearchDTO {
    private String category;
    private String searchType;
    private String query;

    @ConstructorProperties({"category", "search_type", "query"})
    public SearchDTO(String category, String searchType, String query) {
        this.category = category;
        this.searchType = searchType;
        this.query = query;
    }
}

@ConstructorProperties를 넣은 이유는 현재 QueryParameter를 어노테이션으로 받는게 아니라 Object로 받고 있습니다.

왜냐하면 Json은 snake_case를, 자바에서는 cameCase를 쓰고있기 때문에 통일이 되지않기 때문에

필드중 searchType이 실제 QueryParameter를 넘겨줄때도 searchType으로 적어줘야 입력받는 문제점이 있습니다.

그렇기 때문에 @ConstructorProperties에 적어줌으로써 필드 순서에 맞게 매핑이 되는것 같습니다.

 

 

- PostRepositoryCustom

public interface PostRepositoryCustom {
    List<Post> postListQueryDSL(SearchDTO searchDTO);
}

- PostRepositoryImpl

@RequiredArgsConstructor
public class PostRepositoryImpl implements PostRepositoryCustom {
    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public List<Post> postListQueryDSL(SearchDTO searchDTO) {
        BooleanExpression postCategoryQuery = getPostCategoryQuery(searchDTO.getCategory());

        List<Post> postList = jpaQueryFactory
                .selectFrom(post)
                .where(postCategoryQuery)
                .join(post.user, user).fetchJoin()
                .join(post.category, postCategory).fetchJoin()
                .fetch();

        return postList;
    }

     private BooleanExpression postCategoryQuery(String category) {
        if (StringUtils.hasLength(category)) {
            return post.category.eq(getPostCategory(category));
        }

        return null;
    }

    private PostCategory getPostCategory(String category) {
        return jpaQueryFactory
                .selectFrom(postCategory)
                .where(postCategory.name.eq(category))
                .fetchOne();
    }
}

여기서 JPAQueryFactory를 주입받고 있는데 Spring boot에서 기본적으로 주입시켜주는게 아니기 때문에 자동으로 주입시켜줄수 있게 작성해야 합니다.

- QueryDslConfig

@Configuration
public class QueryDslConfig {

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

}

QueryDslConfig를 만들어 JPAQueryFactory을 자동으로 주입시켜줄 수 있게 Bean으로 등록시켜줍니다.

 

- PostRepository

@Repository
public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom {
    @EntityGraph(attributePaths = {"category", "user", "commentList.user"})
    @Override
    Optional<Post> findById(Long postId);

    @EntityGraph(attributePaths = {"user", "category"})
    @Override
    List<Post> findAll(Sort sort);
}

여기서 눈여겨 보셔야 할 점이 PostRepository에 PostRepositoryCustom을 새롭게 상속받았다는 것입니다. 그 다음 PostRepositoryCustom을 구현하는 PostRepositoryImpl 클래스에서 postListQueryDSL 메서드를 구현하고 있는점입니다.

이렇게 하게되면 ServiceLayer에서는 새롭게 PostRepositoryCustom을 추가적으로 의존할 필요없이 기존의 PostRepository를 통해 postListQueryDSL 메서드를 사용 할 수 있습니다.

 

이제 postListQueryDSL 코드를 살펴보도록 하겠습니다.

getPostCategoryQuery(String category) 메서드는 동적 쿼리입니다.

API를 요청할때 Query Parameter에 category를 명시해준다면 좋겠지만 명시하지 않을경우 카테고리와 상관없이 모든글을 가져와야 합니다. 이런 경우를 생각하면 있을 경우와 없을경우, 카테고리가 무엇이냐에 따라 분기문으로 처리해야하는데 그것을 방지하기 위해 동적 쿼리를 사용하였습니다.

QueryDSL에서는 where절에 null이 들어갈경우 자동으로 무시하기 때문에 동적쿼리를 처리하기 좋습니다.

이제 본 메서드의 post, user, postCategory는 아까 gradle로 만들어준 QueryDSL 디렉토리 내부에 있는 Q엔티티 입니다. QueryDSL 에서는 이것을 이용하여 쿼리를 작성합니다.

join을 통해 게시물을 작성한 유저와 카테고리를 한꺼번에 가져오고 fetchJoin()을 통해 N + 1 문제를 해결하였습니다.

 

+ Recent posts