QueryDsl ์ ์šฉ

์ตœ์ค€ํ˜ธยท2022๋…„ 11์›” 27์ผ
0

molu ๊ฐœ๋ฐœ์ผ์ง€

๋ชฉ๋ก ๋ณด๊ธฐ
1/2
post-thumbnail

๐Ÿ“— QueryDsl ์ ์šฉ

์ฐธ๊ณ  QueryDSL ํ”„๋กœ์ ํŠธ ์ ์šฉ ํ›„๊ธฐ ๋ฐ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๐Ÿ“„ ์„ค์ •

โŒจ๏ธ gradle ์˜์กด์„ฑ ์ถ”๊ฐ€

plugins {
	...
    
	// QueryDsl
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

dependencies {

	...
    
	// QueryDsl
	implementation "com.querydsl:querydsl-jpa:5.0.0"
	annotationProcessor "com.querydsl:querydsl-apt:5.0.0"
}

// script for 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
}

โŒจ๏ธ QueryDslConfig ์ถ”๊ฐ€

@Configuration
public class QueryDslConfig {
    @PersistenceContext
    private EntityManager em;

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

โŒจ๏ธ Entity -> Qclass ์ƒ์„ฑ

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 30)
    private String title;
    @Column(nullable = false, length = 3000)
    private String content;
    @Column(nullable = false)
    private String writer;

    @Column(nullable = true, columnDefinition = "bigint default 0")
    private long heart;

    @LastModifiedDate
    private LocalDateTime modifiedAt;

    @CreatedDate
    private LocalDateTime createdAt;

    public static Board createBoard(PostBoard request){
        return new Board(null, request.getTitle(), request.getContent(), Naming.getName(), 0L, null, null);
    }

    public void addHeart(){
        this.heart++;
    }
}
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Long memberId;

    @ManyToOne
    @JoinColumn(name = "board_id")
    private Board board;

    @Column(nullable = false, length = 100)
    private String comment;
    @LastModifiedDate
    private LocalDateTime modifiedAt;
    @CreatedDate
    private LocalDateTime createdAt;
}

Board์™€ Comment๋ฅผ QueryDsl์„ ํ†ตํ•ด ๋ฟŒ๋ ค๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

gradlew compileQuerydsl

๋ฅผ ์ž…๋ ฅํ•ด์ฃผ๋ฉด

์ž๋™์œผ๋กœ gradle์„ ํ†ตํ•ด Qclass๋ฅผ ์ƒ์„ฑํ•ด์ค€๋‹ค.

์œ„์˜ ์„ค์ •์„ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ผํ–ˆ๋‹ค๋ฉด ๋‹ค์Œ ์œ„์น˜์—์„œ Qclass๋“ค์„ ํ™•์ธํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ“„ QueryDsl ์‚ฌ์šฉํ•˜๊ธฐ

โŒจ๏ธ QueryDsl Repository

public interface BoardRepositoryCustom {
    Page<BoardDto> findBoardPage(Pageable pageable);
}

@Repository
@RequiredArgsConstructor
public class BoardRepositoryImpl implements BoardRepositoryCustom{
    private final QueryDslConfig qd;

    @Override
    public Page<BoardDto> findBoardPage(Pageable pageable) {

        List<BoardDto> content = qd.query()
                .select(Projections.constructor(BoardDto.class,
                        board.boardId,
                        board.title,
                        board.content,
                        board.writer,
                        ExpressionUtils.as(
                                JPAExpressions.select(comment1.board.count())
                                .from(comment1)
                                .where(comment1.board.boardId.eq(board.boardId)),"commentCount"
                        ),
                        board.heart,
                        board.modifiedAt,
                        board.createdAt
                ))
                .from(board)
                .orderBy(board.boardId.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        for(BoardDto dto : content){
            List<Comment> comments = qd.query()
                    .selectFrom(comment1)
                    .orderBy(comment1.commentId.desc())
                    .limit(5L)
                    .where(comment1.board.boardId.eq(dto.getBoardId()))
                    .fetch();
            dto.setComments(comments);
        }

        Long total = qd.query()
                .from(board)
                .stream().count();
        return new PageImpl<>(content, pageable, total);
    }
}

QueryDsl์„ ์ ์šฉํ•˜์—ฌ ์‚ฌ์šฉํ•  BoardRepositoryCustom ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ค๊ณ  ๊ตฌํ˜„์ฒด๋ฅผ ์ •์˜ํ•ด์ฃผ์—ˆ๋‹ค.

JPA๋ณด๋‹ค QueryDsl์„ ์‚ฌ์šฉํ•ด๋ณด๋‹ˆ ์ •๋ง๋กœ ์ฟผ๋ฆฌ๋ฅผ ์ง ๋‹ค๊ธฐ ๋ณด๋‹ค๋Š” java ๋ฌธ๋ฒ•์œผ๋กœ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๋Š๋‚Œ์ด ๋‚˜์„œ ๋” ์žฌ๋ฐŒ์—ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋ฐ˜ํ™˜ํ• ๋•Œ Entity๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. ๋Œ€์ฒด๋กœ๋Š” Tuple๊ณผ Dto ์ƒ์„ฑ์ด ์žˆ๋Š”๋ฐ. Tuple์€ ๋งˆ์น˜ Map์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ์ฒด๋ฅผ ์ „๋‹ฌํ•˜๋Š” ๊ธฐ๋ถ„์ด๋ผ Dto๋ฅผ ๋”ฐ๋กœ ์ •์˜ํ•ด์„œ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ–ˆ๋‹ค.

๋˜ ์—ฌ๊ธฐ์„œ QueryDsl์—์„œ ์ •์˜ํ•˜๋Š” @QueryProjection ์–ด๋…ธํ…Œ์ด์…˜์„ Entity๋‚˜ Dto ์ƒ์„ฑ์ž์— ๋ถ™์—ฌ์ฃผ๋ฉด ๋˜๋Š”๋ฐ. ์ด๊ฒƒ ๋˜ํ•œ ๋„ˆ๋ฌด QueryDsl ์˜์กด๋„๊ฐ€ ๋†’์•„์ง€๋Š”๊ฑฐ ๊ฐ™์•„์„œ ๊ทธ๋ƒฅ ๋‚ด๊ฐ€ ๋”ฐ๋กœ ์ •์˜ํ•œ Dto ๊ฐ์ฒด์— ๊ฐ’์„ ๋„ฃ์–ด์ฃผ๋Š” ๊ฒƒ์œผ๋กœ ์ •์˜ํ–ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  Projections.constructor('์ƒ์„ฑํ•  ๊ฐ์ฒด class', ์ƒ์„ฑ์ž ๋งค๊ฐœ๋ณ€์ˆ˜...) ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•  ํƒ€์ž…์„ ์ •์˜ํ–ˆ๊ณ  ExpressionUtils.as('์ฟผ๋ฆฌ', 'alias')๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ณ„์นญ์„ ์ •์˜ํ•ด์ฃผ์—ˆ๋‹ค.

์ฟผ๋ฆฌ ๋ถ€๋ถ„์—๋Š” JPAExpressions.select()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ธŒ์ฟผ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์–ด ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

for(BoardDto dto : content){...} ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น ๋ถ€๋ถ„์„ ํ†ตํ•ด list ํ˜•ํƒœ์˜ ๋Œ“๊ธ€ ํ…Œ์ด๋ธ”์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ด์™€ ๋„ฃ์–ด์ฃผ์—ˆ๋‹ค. ์ด๋ถ€๋ถ„์€ ๋‚˜์ค‘์— 5๋ฒˆ ์กฐํšŒ๊ฐ€ ์•„๋‹Œ 1๋ฒˆ ์กฐํšŒ๋กœ ์ค„์ผ ์ˆ˜ ์žˆ์„๊ฑฐ ๊ฐ™๋‹ค. ๋ฆฌํŒฉํ† ๋ง ๋Œ€์ƒ์œผ๋กœ ์ •ํ•ด๋‘๊ณ  ์ˆ˜์ •ํ•˜๋Š” ๊ธ€์„ ๋‹ค์Œ์— ์˜ฌ๋ ค์•ผ๊ฒ ๋‹ค.

โŒจ๏ธ ๊ธฐ๋ณธ JPA ์‚ฌ์šฉ

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

๊ธฐ๋ณธ์ ์ธ JPA์˜ ๋ฉ”์„œ๋“œ๋“ค์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ •์˜ํ•ด์ค€๋‹ค. ๊ทธ๋ฆฌ๊ณ  BoardRepositoryCustom ์šฐ๋ฆฌ๊ฐ€ ์ •์˜ํ•œ Repository๋ฅผ ์ถ”๊ฐ€๋กœ ์ ์šฉํ•ด์ฃผ์–ด Service์—์„œ ํ•˜๋‚˜์˜ Repository๋ฅผ ์ฃผ์ž…ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์ž.

โŒจ๏ธ Service ๊ตฌํ˜„

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class BoardServiceImpl implements BoardService{
    private final BoardRepository boardRepository;

    @Override
    public GetBoardResponse getBoard(Pageable page) {
        Page<BoardDto> findAll = boardRepository.findBoardPage(page);
        return GetBoardResponse.builder()
                .last(findAll.isLast())
                .empty(findAll.isEmpty())
                .totalPage(findAll.getTotalPages())
                .totalElements(findAll.getTotalElements())
                .boardList(findAll.getContent())
                .numberOfElements(findAll.getNumberOfElements())
                .build();
    }
}

๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด์„œ ํ™•์ธํ•ด๋ณด๋ฉด


@SpringBootTest
@Execution(ExecutionMode.SAME_THREAD)
class BoardServiceImplTest {

    @Autowired
    private BoardService boardService;

    @Test
    @DisplayName("๊ฒŒ์‹œ๊ธ€ ํŽ˜์ด์ง• ์„ฑ๊ณตํ•œ๋‹ค.")
    void getBoardSuccess() throws Exception {
        //given
        for(int i=1; i<=20; i++){
            PostBoard postBoard = new PostBoard("์ œ๋ชฉ" + i, "๋‚ด์šฉ" + i);
            boardRepository.save(Board.createBoard(postBoard));
        }

        Pageable pageable = Pageable.ofSize(5);
        pageable = pageable.next();
        pageable = pageable.next();

        //when
        GetBoardResponse board = boardService.getBoard(pageable);

        //then
        Stream<String> title = board.getBoardList().stream().filter(b -> b.getContent().equals("๋‚ด์šฉ10")).map(b -> b.getTitle());
        assertTrue(title.count() == 1);
    }
}

์ •์ƒ์ ์œผ๋กœ ํŽ˜์ด์ง•ํ•œ ๋‚ด์šฉ์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!

๐Ÿ‘ ํŽ˜์ด์ง• ์ฟผ๋ฆฌ ๋ฆฌํŒฉํ† ๋ง

์œ„ ์ฟผ๋ฆฌ๋กœ ์‹คํ–‰ํ•˜๋ฉด

๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜๋‚˜์˜ ํŽ˜์ด์ง• ์š”์ฒญ๋‹น 5๊ฐœ์˜ ๋Œ“๊ธ€ ์ฐพ๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. ํ•ด๋‹น ์ฟผ๋ฆฌ๊ฐ€ ๋งŒ์•ฝ ๋Œ“๊ธ€์„ 10๊ฐœ ์ฐพ๋Š” ์ฟผ๋ฆฌ๋ผ๋ฉด 10๊ฐœ์˜ ์ฟผ๋ฆฌ๊ฐ€ ์ถ”๊ฐ€๋กœ ๋‚˜๊ฐ€๋Š” ๊ฒƒ์ด๋‹ค. ์ด๊ฒƒ์ด N+1 ๋ฌธ์ œ์ธ๋ฐ QueryDsl๋กœ ๋ณ€๊ฒฝํ•œ ์˜๋ฏธ๊ฐ€ ์—†๋‹ค. ๊ทธ๋ž˜์„œ ์ตœ๋Œ€ํ•œ ์ค„์—ฌ๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

@Repository
@RequiredArgsConstructor
public class BoardRepositoryImpl implements BoardRepositoryCustom{
    private final QueryDslConfig qd;

    @Override
    public Page<BoardDto> findBoardPage(Pageable pageable) {

        List<BoardDto> content = qd.query()
                .select(Projections.constructor(BoardDto.class,
                        board.boardId,
                        board.title,
                        board.content,
                        board.writer,
                        ExpressionUtils.as(
                                JPAExpressions.select(comment1.board.count())
                                .from(comment1)
                                .where(comment1.board.boardId.eq(board.boardId)),"commentCount"
                        ),
                        board.heart,
                        board.modifiedAt,
                        board.createdAt
                ))
                .from(board)
                .orderBy(board.boardId.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        ArrayList<Long> boardIdList = new ArrayList<>();

        for(BoardDto dto : content){
            boardIdList.add(dto.getBoardId());
        }

        List<CommentDto> findComment = qd.query().select(Projections.constructor(CommentDto.class,
                    comment1.commentId,
                    comment1.memberId,
                    comment1.board.boardId,
                    comment1.comment,
                    comment1.modifiedAt,
                    comment1.createdAt
                ))
                .from(comment1)
                .where(comment1.board.boardId.in(boardIdList))
                .fetch();

        for(BoardDto dto : content){
            Stream<CommentDto> commentStream = findComment.stream().filter(c -> c.getBoardId() == dto.getBoardId());
            List<CommentDto> comments = commentStream.collect(Collectors.toList());
            dto.setComments(comments);
        }

        Long total = qd.query()
                .from(board)
                .stream().count();
        return new PageImpl<>(content, pageable, total);
    }
}

์ฟผ๋ฆฌ๊ฐ€ board๋ฅผ ์ตœ์ดˆ๋กœ select ํ•˜๋Š” ์ฟผ๋ฆฌ์™€ boardId๋ฅผ ๊ธฐ์ค€์œผ๋กœ Comment๋ฅผ selectํ•˜๋Š” ์ฟผ๋ฆฌ 2๊ฐœ๋กœ ์ค„์˜€๋‹ค. ์ด ์ฟผ๋ฆฌ๋Š” ๋Œ“๊ธ€์„ ๋ช‡๊ฐœ๋ฅผ ์ฐพ์•„๋„ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ข€๋” ๊ฐœ์„ ํ–ˆ๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค!

    @Test
    @DisplayName("๊ฒŒ์‹œ๊ธ€ ํŽ˜์ด์ง• ํ›„ ๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์„ฑ๊ณตํ•œ๋‹ค.")
    void getBoardSuccess2() throws Exception {
        //given
        for(int i=1; i<=20; i++){
            PostBoard postBoard = new PostBoard("์ œ๋ชฉ" + i, "๋‚ด์šฉ" + i);
            Board saveBoard = boardRepository.save(Board.of(postBoard));
            for(int j=1; j<=3; j++){
                commentRepository.save(Comment.of(0L, "๋Œ“๊ธ€"+j, saveBoard));
            }
        }

        Pageable pageable = Pageable.ofSize(5);
        pageable = pageable.next();
        pageable = pageable.next();

        //when
        GetBoardResponse board = boardService.getBoard(pageable);
    }

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค๋ณด๋ฉด

๋ฐ์ดํ„ฐ๋„ ๋‚ด๊ฐ€ ์ƒ๊ฐํ•œ๋Œ€๋กœ ์ž˜ ๋‚˜์˜ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

profile
์ฝ”๋”ฉ์„ ๊น”๋”ํ•˜๊ฒŒ ํ•˜๊ณ  ์‹ถ์–ดํ•˜๋Š” ์ดˆ๋ณด ๊ฐœ๋ฐœ์ž (ํŽธํ•˜๊ฒŒ ๊ธ€์„ ์“ฐ๊ธฐ์œ„ํ•ด ๋ฐ˜๋ง์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค! ์–‘ํ•ด ๋ถ€ํƒ๋“œ๋ ค์š”!) ํ˜„์žฌ KakaoVX ๊ทผ๋ฌด์ค‘์ž…๋‹ˆ๋‹ค!

0๊ฐœ์˜ ๋Œ“๊ธ€