Project/Nuwa

Nuwa Project - Querydsl 적용기

Llimy1 2024. 2. 24. 03:43
반응형
SMALL
반응형
SMALL

이번 프로젝트 진행에서 처음으로 Querydsl을 사용을 해보고자 했습니다.

이전까진 항상 JPQL을 사용을 해서 쿼리를 구현을 했었는데 동적 쿼리를 구현을 할 때 굉장히 힘들게 구현을 했습니다.

검색을 구현을 하더라도 검색 조건이 두가지라면 두 개의 JPQL을 만들어서 작업을 했습니다.

그러면 더욱 많아진다면 코드 자체가 매우 지저분해지고 불필요한 로직이 생겨났습니다.

 

이전에 코드를 만들 때는 파라미터로 검색 조건을 받아서 직접 if문을 사용하여 나눠주는 작업을 했습니다.

예를 들면

public ResponseEntity<Object> search(@RequestParam String type) {
	if (type.equals("id") {
    	memberService.findId(type);
    } else {
    	memberService.findName(type);
    }
    return ResponseEntity.status(OK);
}

 

이런 방법으로 구분을 해서 각각 조건에 맞는 로직을 수행을 하도록 했었습니다.

이런 불필요한 로직이 Querydsl을 사용하면 깔끔하게 해결되고

JPQL을 사용해서 만들었던 쿼리문이 자바 코드로 작성되니 오타가 나와도 컴파일 시점에서 오류를 반환하는 이점도 있기에

Querydsl 도입을 하기로 결정 했습니다.

 

bulid.gradle

//Querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

def querydslSrcDir = 'src/main/generated'
clean {
    delete file(querydslSrcDir)
}

 

의존성을 추가하고 Q파일이 생성이 될 수 있는 경로를 설정을 합니다.

다음과 같이 생성을 하게 된다면 컴파일 시점에서

build파일 내부에 Q파일 시점에서 생성이 되기에 따로 ignore 파일에 작성을 하지 않아도 됩니다.

(대부분의 ignore엔 build 파일을 제거하기 때문입니다.)

 

다음과 같이 설정을하고

@Configuration
public class QuerydslConfig {

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

 

JPAQueryFactory 빈 등록을 해줍니다.

각 파일에서 설정이 가능하지만 여러 파일에서 사용을 할 예정이여서 빈 등록으로 했습니다.

 

각 파일에서 설정도 가능합니다.

@Repository
public class MemberJpaRepository {

    private final EntityManager em;
    private final JPAQueryFactory queryFactory;

    public MemberJpaRepository(EntityManager em) {
        this.em = em;
        this.queryFactory = new JPAQueryFactory(em);
    }
 }

 

 

먼저 JPQL로 작성한 코드를 보여드리겠습니다.

@Query("SELECT f " +
        "FROM File f " +
        "JOIN f.workSpace w " +
        "JOIN f.workSpaceMember wm " +
        "WHERE w.id = :workSpaceId AND f.fileType = :fileType " +
        "ORDER BY f.createdAt DESC ")
Slice<File> findByFileOrImageOrderByCreatedAtDesc(@Param("workSpaceId") Long workSpaceId, @Param("fileType") FileType fileType, Pageable pageable);

 

다음과 같이 파일 형식에 따라 데이터를 페이징을 해서 가져오는 코드가 있습니다.

이 코드를 작성을 하고 저는 전체 조회하는 코드를 따로 작성을 했습니다.

Slice<File> findByWorkSpaceIdOrderByCreatedAtDesc(Long workSpaceId, Pageable pageable);

 

해당 코드를 작성하고 서비스 로직은 두 개의 로직이 필요합니다.

// 파일 전체 조회
public Slice<FileInfoResponseDto> fileAndImageUrlList(Long workSpaceId, Pageable pageable) {
    log.info("파일 전체 조회");
    Slice<File> fileSlice = fileRepository.findByWorkSpaceIdOrderByCreatedAtDesc(workSpaceId, pageable);

    return fileSlice.map(file -> FileInfoResponseDto.builder()
            .fileId(file.getId())
            .fileUrl(file.getUrl())
            .fileType(file.getFileType())
            .fileMemberUploadId(file.getWorkSpaceMember().getId())
            .fileMemberUploadName(file.getWorkSpaceMember().getName())
            .createdAt(file.getCreatedAt())
            .build());
}
public Slice<FileInfoResponseDto> fileOrImageListV2(Long workSpaceId, FileType fileType, Pageable pageable) {
    return fileRepository.findByFileOrImageOrderByCreatedAtDesc(workSpaceId, fileType, pageable)
            .map(file -> FileInfoResponseDto.builder()
                    .fileId(file.getId())
                    .fileUrl(file.getUrl())
                    .fileMemberUploadId(file.getWorkSpaceMember().getId())
                    .fileMemberUploadName(file.getWorkSpaceMember().getName())
                    .fileType(file.getFileType())
                    .createdAt(file.getCreatedAt())
                    .build());
}

 

파일 전체 조회를 하는 로직과 파일 타입에 맞게 조회를 하는 로직이 구분이 되어 있습니다.

 

현재 이 두가지 코드를 Querydsl을 사용하면 하나의 로직으로 작성이 가능합니다.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class FileQueryService {

    private final JPAQueryFactory jpaQueryFactory;


    public Slice<FileInfoResponseDto> imageOrFileList(Long workSpaceId, FileType fileType, Pageable pageable) {
        List<FileInfoResponseDto> fileInfoResponseDtoList = jpaQueryFactory.select(Projections.constructor(FileInfoResponseDto.class,
                        file.id.as("fileId"),
                        file.url.as("fileUrl"),
                        file.fileType.as("fileType"),
                        workSpaceMember.id.as("fileMemberUploadId"),
                        workSpaceMember.name.as("fileMemberUploadName"),
                        file.createdAt))
                .from(file)
                .orderBy(file.createdAt.desc())
                .join(file.workSpaceMember, workSpaceMember)
                .where(
                        workSpaceIdEq(workSpaceId),
                        fileTypeEq(fileType)
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize() + 1)
                .fetch();

        boolean hasNext = fileInfoResponseDtoList.size() > pageable.getPageSize();
        List<FileInfoResponseDto> fileContent = hasNext ? fileInfoResponseDtoList.subList(0, pageable.getPageSize()) : fileInfoResponseDtoList;

        return new SliceImpl<>(fileContent, pageable, hasNext);
    }

    private BooleanExpression workSpaceIdEq(Long workSpaceId) {
        return file.workSpace.id.eq(workSpaceId);
    }

    private BooleanExpression fileTypeEq(FileType fileType) {
        return fileType != null ? file.fileType.eq(fileType) : null;
    }
}

 

저는 바로 DTO로 반환을 하기 위해 Projections.constructor을 사용하여 DTO에 각 데이터를 맵핑을 해주었고

where문에서 들어오는 값에 맞는 데이터를 넣어주기 위해 BooleanExpression을 사용하여

해당 값이 맞는지 맞지 않는지를 판별하는 메소드를 만들었습니다.

그리고 Slice로 반환을 하기 위해 limit에선 페이지 사이즈에 +1을 해서 다음 값이 있는지 판별을 했습니다.

그 아래 hasNext를 만드는데 현재 리스트의 사이즈보다 페이지 사이즈가 크다면 hasNext를 ture로 해주었습니다.

(hasNext는 다음 값이 있는지 판별을 위해 만들었습니다.)

그리고 hasNext가 없다면 리스트를 그대로 반환을 하고 hasNext가 있다면 subList를 사용하여 0번부터 페이지 사이즈까지 잘라서 반환을 하도록 했습니다.

그리고 SliceImpl을 사용하여 반환 값을 Slice로 변경을 했습니다.

(그리고 fetchResults를 왜 사용하지 않냐라고 하실 수도 있는데 현재 deprecate되었기에 직접 로직을 작성을 했습니다.)

 

이제 여기서 이점이 위에 작성을 했던 코드 두가지를 하나의 로직으로 처리가 되었습니다.

그 이유는 파라미터로 어떠한 데이터도 넘기지 않으면 null 값이 들어가게 됩니다.

where 절에 null 값이 들어가게 되면 오류가 나는 것이 아닌 무시를 하고 쿼리문이 실행이 됩니다.

이렇게 해서 파라미터로 데이터를 넘기지 않는다면 전체 조회를 하는 로직이 되고

파라미터로 데이터를 넘긴다면 해당 타입에 맞는 데이터를 조회하는 로직이 가능하게 됩니다.

 

그래서 위에 두 서비스 코드가 존재가 되었던 부분에서

public Slice<FileInfoResponseDto> fileOrImageList(Long workSpaceId, FileType fileType, Pageable pageable) {
    return fileQueryService.imageOrFileList(workSpaceId, fileType, pageable);
}

 

단 한 줄의 코드로 해당 로직을 수행을 할 수 있게 됩니다.

 

Issue

도메인을 생성을 할 때 fileType을 enum으로 생성을 했습니다.
그래서 처음에 StringUtils.hasText()를 사용하여 값이 존재하는지 없는지를 판단을 하기 위해

private BooleanExpression fileTypeEq(FileType fileType) {
    return StringUtils.hasText(String.valueOf(fileType)) ? file.fileType.eq(fileType) : null;
}
다음과 같이 코드를 작성을 했었는데
여기서 파라미터 값을 넘기지 않으면 null이 들어오게 됩니다.
그런데 String.valueOf(fileType) -> 이 부분에서 null 자체를 반환을 해주는 것이 아닌
"null" 과 같이 반환을 해주었습니다.
그래서 hasText()는 공백 또는 null이라면 데이터가 없다고 판단을 하는데 "null"이 들어오니
데이터가 있다고 판단을 하여
file.fileType.eq("null") 이런 식으로 데이터가 들어가서 오류가 발생을 했습니다.

그래서 직접 null 체크를 해주는 방식으로 변경을 했습니다.
반응형
LIST