인프런에서 querydsl에 관한 강의를 듣고 토이 프로젝트에 적용해보고 싶은 생각이 들었다. 그래서 한 번 해보기로 함~~!!

 

 

먼저 gradle 설정

plugins {
    ...
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    ...
}
...
dependencies {
    ...
    implementation 'com.querydsl:querydsl-jpa'
    ...
}
...
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을 기존의 spring data JPA repository를 사용하던 곳에 적용해 보려고 했다.

사용자 정의 Repository

복잡한 기능을 추가하거나, 사용자 정의 함수를 추가할 때 사용한다.

Spring data JPA는 인터페이스로 동작하기 때문에, 내가 원하는 코드를 넣으려면 사용자 정의 repository라는 것을 구현해야 한다.

출처: https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84

 

사용자 정의 repository의 사용법은 아래 단계들과 같다.

1. 사용자 정의 인터페이스 작성

2. 사용자 정의 인터페이스 구현

3. 스프링 데이터 레포지토리에 사용자 정의 인터페이스 상속

 

여기서 주의할 점은 기존에 있던 JpaRepository를 상속하는 repository 인터페이스와 같은 이름의 Impl을 구현해야 한다는 점이다.

 

이 방법 외에도 특정한 기능인 경우에는 따로 repository를 만들 수도 있다.

현재 구현하는 애플리케이션의 경우, 게시판에만 search & filter 기능을 사용하기 때문에 후자의 방법을 이용하여 구현해 보기로 했다.

 

 

 

기존 코드: stream filter, map, collect 등의 기능을 이용했었음

1. 모든 게시판 가져오면서 숨김처리한 게시물 필터링

2. 조건에 맞추어 필터링

3. 조건에 맞추어 정렬

(정렬 조건 중 까다로운 점은 현재까지 모임 신청이 approved된 인원을 뺀 remain 에 대한 정렬을 구현하는 것이었다)

여기서 나는 같은 stream을 여러번 읽어서 필터링 하고, 이 필터링 된 코드를 또 stream으로 바꾸어 sorting하는 등이 문제라고 생각했다.

private List<Board> sortBoardList(SortingType sorting, List<Address> addresses, List<Category> categories, User user) {
	List<Board> boards = filterBoardList(addresses, categories, user);

	if (sorting == SortingType.REMAIN) {
		return boards.stream()
				.sorted(Comparator.comparing(Board::getRemainRecruitNumber, Comparator.reverseOrder()))
				.collect(Collectors.toList());
	} else if (sorting == SortingType.DEADLINE) {
		return boards.stream()
				.sorted(Comparator.comparing(Board::getStartsAt, Comparator.naturalOrder()))
				.collect(Collectors.toList());
	}
	return boards.stream()
			.sorted(Comparator.comparing(BaseEntity::getCreatedAt, Comparator.reverseOrder()))
			.collect(Collectors.toList());
}

private List<Board> filterBoardList(List<Address> addresses, List<Category> categories, User user) {
	return findAllBoards(user).stream()
			.filter(board -> addresses.contains(board.getAddress()))
			.filter(board -> categories.contains(board.getCategory()))
			.collect(Collectors.toList());
}

private List<Board> findAllBoards(User user) {
	Set<Board> hiddenBoards = user.getUserHiddenBoard().stream()
			.map(HiddenBoard::getBoard).collect(Collectors.toSet());

	return boardRepository.findAll().stream()
			.filter(board -> board.getStatus().getCode() != BoardStatus.CANCELED.getCode())
			.filter(board -> !hiddenBoards.contains(board))
			.collect(Collectors.toList());
}

이러고 나서 마지막에 페이징 처리를 했음...

 

 

바뀐 코드: 

queryDSL로 바꾸기 위해서 일단 필터링 조건 들을 나누었다.

BooleanBuilder를 사용하면 훨씬 쉽게 할 수 있었지만,

where절의 조건으로 쓰면 가독성이 좋은 BooleanExpression으로 구현하고 싶었다.

BooleanExpression을 사용할 때 주의할 점은 null 에 대한 부분을 생각해 주어야 한다는 점이다.

private BooleanExpression isFilteredCategories(List<Long> categories) {
	return categories != null ? Expressions.anyOf(categories.stream().map(this::isFilteredCategory).toArray(BooleanExpression[]::new)) : null;
}

private BooleanExpression isFilteredCategory(Long categoryId) {
	return board.category.id.eq(categoryId);
}

private BooleanExpression isFilteredCities(List<Long> cities) {
	return cities != null ? Expressions.anyOf(cities.stream().map(this::isFilteredCity).toArray(BooleanExpression[]::new)) : null;
}

private BooleanExpression isFilteredCity(Long cityId) {
	return board.address.id.eq(cityId);
}

private BooleanExpression isSearchedKeywords(List<String> keywords) {
	return Expressions.allOf(keywords.stream().map(this::isSearchedKeyword).toArray(BooleanExpression[]::new));
}

private BooleanExpression isSearchedKeyword(String keyword) {
	return board.content.containsIgnoreCase(keyword);
}

private BooleanExpression isDeletedBoard() {
	return board.status.ne(BoardStatus.CANCELED);
}

BooleanExpression을 쓰면서 어려웠던 점은, 단일 파라미터에 대한 예제는 많이 나와있었지만,

여러가지 필터링을 하기 위해 List 값에 대한 BooleanExpression을 만드는 것들은 잘 나오지 않아 시간을 좀 많이 썼다...

 

 

그 다음은 이 조건들을 적용하기 전에 먼저 querydsl로 짤 쿼리문을 직접 짜 보았다.

SELECT HIDDEN_FILTERED.*, COALESCE(APPROVED_USER.approved_number, 0) as approved_number, (HIDDEN_FILTERED.recruit_count - COALESCE(APPROVED_USER.approved_number, 0)) as remain_number
FROM (SELECT b.* FROM board as b left join hidden_board as hb on (b.id = hb.board_id and hb.user_id=1) where hb.user_id is null) as HIDDEN_FILTERED
left join (select board_id, count(user_id) as approved_number from applied_user where status='APPROVED' group by board_id) as APPROVED_USER
on HIDDEN_FILTERED.id = APPROVED_USER.board_id
order by approved_number DESC;

첫번째 시도는 위와 같이 mysql에서 직접 쿼리문을 작성할 경우 결과값이 아주아주~~ 잘 나왔다.

 

 

하지만, querydsl에서는 서브쿼리를 from문에서 사용하지 못한다는것을 몰랐다... 그래서 아래와 같이 left join을 3번 해보기로 했다.

SELECT b.*, (b.recruit_count - COALESCE(count(au.user_id), 0)) as remain_count
FROM (board as b left join hidden_board as hb on b.id = hb.board_id and hb.user_id=1) left join applied_user as au on b.id = au.board_id and au.status = 'APPROVED'
where hb.user_id is null
group by b.id
order by remain_count DESC;

 

 

 

쿼리 테스트를 해 본 후, querydsl에 해당 쿼리문을 적용해 보았다.

여기서 어려웠던 점은, 정렬 조건 중 remain 인원에 대한 것이었다.

모집 인원에 대한 인원 수는 db 필드로 있었으나, remain 인원에 대한 것은 applied user의 상태를 보고 세어야 했다.

이것은 자바 함수를 쓰면 매우 간단해서 이전 방법을 사용할 때는 깨닫지 못했지만, querydsl로 바꾼 후 시간이 많이 걸리게 된 원인이다ㅠ

 

인프런에 관련 함수를 쓸 수 없을지 질문을 올렸는데, 아래는 그 답변이었다.

---

querydsl의 결과는 결국 JPQL이 만들어지고, JPQL은 또 SQL로 번역되기 때문에 SQL로 할 수 있는 로직만 실행할 수 있습니다.

해당 코드는 자바 함수이기 때문에 JPQL이나 SQL에서 사용하는 것이 불가능합니다.

대신에 JPQL이나 SQL에서 다음과 같은 방식은 사용할 수 있습니다.

order by item.count2 - item.count1 desc

querydsl에서 사용하려면 다음 코드를 참고해주세요.

---

 

그래서 결국 applied user를 직접 구해서 뺀 값에 대한 정렬을 하도록 바꾸었다.

public List<Board> filter(BoardFilterCondition boardFilterCondition, Pageable pageable) {
	return jpaQueryFactory
			.select(board)
			.from(board)
			.leftJoin(hiddenBoard).on(board.id.eq(hiddenBoard.board.id).and(hiddenBoard.user.id.eq(boardFilterCondition.getUserId())))
			.leftJoin(appliedUser).on(board.id.eq(appliedUser.board.id).and(appliedUser.status.eq(AppliedStatus.APPROVED)))
			.where(
					hiddenBoard.user.id.isNull(),
					isFilteredCategories(boardFilterCondition.getCategory()),
					isFilteredCities(boardFilterCondition.getCity()),
					isDeletedBoard()
			)
			.groupBy(board.id)
			.orderBy(orderType(boardFilterCondition.getSorting()))
			.offset(pageable.getOffset())
			.limit(pageable.getPageSize())
			.fetch();
}

private OrderSpecifier<?> orderType(SortingType sortingType) {
	if(sortingType == SortingType.REMAIN){
		return board.recruitCount.subtract(appliedUser.user.id.count().coalesce(0L)).desc();
	}
	if (sortingType == SortingType.DEADLINE) {
		return board.startsAt.asc();
	}
	return board.createdAt.desc();
}

여러번 stream으로 필터링을 해야 했던 코드가 비교적 가독성 있어졌다...!! 그리고 페이징 처리도 repository 내에서 다 할 수 있었다.

 

 

 

마찬가지로 검색 쿼리도 구현해 보았다. 검색 쿼리는 필터링 보다 정렬도 까다롭지 않아 더 쉬운 편이었다.

public List<Board> search(BoardSearchCondition boardSearchCondition, Pageable pageable) {
	return jpaQueryFactory.select(board)
			.from(board).leftJoin(hiddenBoard)
			.on(board.id.eq(hiddenBoard.board.id).and(hiddenBoard.user.id.eq(boardSearchCondition.getUserId())))
			.where(
					hiddenBoard.user.id.isNull(),
					isSearchedKeywords(boardSearchCondition.getKeywords()),
					isDeletedBoard()
			)
			.orderBy(board.createdAt.desc()) // 최신순 정렬
			.offset(pageable.getOffset())
			.limit(pageable.getPageSize())
			.fetch();
}

 

 

 

느낀점

인강만 듣고 쉬운 예제들을 봤을 땐 금방 적용하겠지~ 하고 생각했는데,

실제 구현할 부분에 적용하려고 하니 생각보다 어려웠다. 그리고 가장 중요한 것은 sql 자체를 짜는 능력이라고 생각했다.

sql 자체를 제대로 짜면 그 것을 querydsl로 옮기는 것은 쉬운 일이라고 느꼈다. 성능은 또 다른 문제지만...ㅜㅠ

 

구현한 repository 전체 코드는 아래와 같다.

github.com/Yapp-17th/Android_2_Backend/blob/develop/common-module/src/main/java/com/yapp/crew/domain/repository/BoardSearchAndFilterRepository.java

 

Yapp-17th/Android_2_Backend

Backend Repository for Android Team 2. Contribute to Yapp-17th/Android_2_Backend development by creating an account on GitHub.

github.com

 

+ Recent posts