[JPA] Querydsl의 InnerJoin과 Painging을 이용한 검색기능

재호·2022년 6월 18일
0

개요

현재 블록체인과 핀테크 직업 훈련 과정을 이수중인데 훈련 과정 중 웹프로그래밍 과제를 하던 중에 검색 서비스 구현에서 Querydsl 을 사용해야 해서 공부하면서 구현했던 내용을 기록하고자 한다.

필요 기능

게시판에서 게시글을 검색할 때 작성자로 검색을 하고자 하는데 작성자는 아이디(PK)가 아닌 닉네임값으로 지정되어있다.
Board(게시판) Entity는 Member(회원) Entity와 ManyToOne 관계를 가지고 있다.
검색 조건이 작성자의 이메일이라면 조인이 필요없겠지만 닉네임이기 때문에 Board와 Member의 Join이 필요하다.

Entity

Member

@Entity
@Table(name = "tbl_member")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString
public class Member {
	
	@Id
	private String email;
	
	@Column(nullable = false)
	private String password;
	
	@Column(nullable = false)
	private String name;
	
	@Column(nullable = false)
	private String nickname;
	
	@Column(columnDefinition = "boolean default false")
	private boolean officialmark;
}

Board

@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@ToString(exclude = "member")
public class Board extends BaseEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long bno;
	
	private String title;
	
	private String description;
	
	private LocalDateTime closetime;
	
	private boolean anonymous;
	
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "member_email")
	private Member member;
}

Querydsl 사용을 위한 gradle 설정

build.gradle

plugins {
	id 'org.springframework.boot' version '2.5.5'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	
	id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
	
}

group = '그룹 이름'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	// ...생략
	
	// QueryDSL
    implementation 'com.querydsl:querydsl-jpa'
    implementation 'com.querydsl:querydsl-apt'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.1'
    
}

// querydsl 추가 시작
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

// ...생략

Querydsl은 컴파일 단계에서 엔티티를 기반으로 Q클래스 파일들을 생성한다. 이 클래스를 기반으로 쿼리를 작성하게 된다.

Q클래스를 생성하려면 Gradle 옵션을 통해서 소스 코드를 컴파일시키면 된다. 즉, build task의 build 옵션을 실행하거나 단순히 Q클래스만 만들 목적이라면 other 태스크의 compileJava만 실행시키면 된다.

Querydsl 사용

Querydsl config 생성

Querydsl을 사용하기 위해 기본적인 EntityManagerJpaQueryFactory 설정해야 한다.

QuerydslConfig 란 이름으로 클래스 생성

@Configuration
public class QuerydslConfig {
	@PersistenceContext
	private EntityManager entityManager;
	
	@Bean
	public JPAQueryFactory jpaQueryFactory() {
		return new JPAQueryFactory(entityManager);
	}
}

검색받은 keyword와 페이징 처리를 위한 DTO를 파라미터로 받는다.

BoardRepositoryCustom 인터페이스 생성

repository 인터페이스를 구현하고 해당 인터페이스를 상속받아 사용하도록 하다

/**
 * Querydsl로 작성할 쿼리는 이 곳에 시그니처를 선언하고 `~RepositoryImpl`에서 구현한다.
 */
public interface BoardRepositoryCustom {
	List<Tuple> findByNicknamWithPaging(String keyword, PageRequestBoardDTO dto);
}

추상 메서드를 구현하는 Impl 클래스 생성

@Repository
@RequiredArgsConstructor
public class BoardRepositoryCustomImpl implements BoardRepositoryCustom {
	
	private final JPAQueryFactory query;

	@Override
	public List<Tuple> findByNicknamWithPaging(String keyword, PageRequestBoardDTO dto) {
		// Q클래스 사용
		QBoard board = QBoard.board;
		QMember member = QMember.member;
		
		// 페이지 정보
		int page = dto.getPage()-1;
		int size = dto.getSize();
		
		// board와 member를 이너조인
		List<Tuple> list = query.select(board, member.nickname)
			.from(board)
			.join(board.member, member)
			.fetchJoin()
			.where(member.nickname.contains(keyword)) //검색 키워드가 포함된 컬럼들만 조회
			.offset(page)
			.limit(size)
			.fetch();
		
		return list;
	}

}

앞서 설정해놓은 JPAQueryFactory를 받아오고 Q클래스를와 얻고자 하는 페이지의 정보를 정의한다.

검색을 통해 게시글과 관련 회원의 정확한 닉네임을 얻고자 select에 파라미터로 주고 boardmember를 이너조인하는데 이 때 조인의 조건을 board.member(FK)member가 같을 때로 설정한다. board의 정보외에 member의 닉네임값이 필요하기 때문에 fetchJoin을 꼭 걸어주어야 한다.
검색 키워드가 포함된 컬럼들만 나올 수 있도록 where를 설정하고 offsetlimit으로 페이징 처리까지 한 후 fetch로 조회 결과를 가져온다.

쿼리에 문제가 없는지 테스트를 해본 결과 아래와 같이 쿼리가 잘 날라갔다.

select
        board0_.bno as col_0_0_,
        member1_.nickname as col_1_0_,
        member1_.email as email1_2_1_,
        board0_.bno as bno1_0_0_,
        board0_.moddate as moddate2_0_0_,
        board0_.regdate as regdate3_0_0_,
        board0_.anonymous as anonymou4_0_0_,
        board0_.closetime as closetim5_0_0_,
        board0_.description as descript6_0_0_,
        board0_.member_email as member_e8_0_0_,
        board0_.title as title7_0_0_,
        member1_.name as name2_2_1_,
        member1_.nickname as nickname3_2_1_,
        member1_.officialmark as official4_2_1_,
        member1_.password as password5_2_1_ 
    from
        board board0_ 
    inner join
        tbl_member member1_ 
            on board0_.member_email=member1_.email 
    where
        member1_.nickname like ? escape '!' limit ?

Repository 상속

기존 BoardRepositoryBoardRepositoryCustom을 상속해준다.

public interface BoardRepository extends JpaRepository<Board, Long>, BoardRepositoryCustom {
	
}

마치며

Querydsl을 사용하기 위해 공부를 하며 찾은 내용들 중에 fetchJoin과 페이징을 함께 사용할 때에는 모든 데이터가 한번에 불려져서 성능에 문제가 생기는 부분을 주의해야 한다고 들었는데 나는 데이터가 적어서 그런 것인지 모르겠지만 해당 경고인 HHH000104가 발생되지 않아서 일단은 그대로 진행하기로 했다. 만약 개발을 더 진행하면서 문제가 발생한다면 또 포스팅하며 공부하려 한다.

참조

https://madplay.github.io/post/introduction-to-querydsl
https://tecoble.techcourse.co.kr/post/2020-10-21-jpa-fetch-join-paging/

profile
Java, Spring, SpringMVC, JPA, MyBatis

0개의 댓글