상세 컨텐츠

본문 제목

[QueryDSL] 카테고리별 게시글 조회

JAVA/SPRING

by ranlan 2021. 4. 29. 03:31

본문

728x90

카테고리 리스트로 메뉴가 구성되고 메뉴를 눌렀을 때 해당 카테고리에 등록된 게시글을 조회하고 싶다.

조건이 들어간 쿼리를 작성하기 위해 QueryDSL을 이용하였다.

 

 

[참고]

2021.04.29 - [web/queryDSL] - queryDSL이란  

 

queryDSL이란

[Inflearn] 김영한 - 실전! Querydsl 실전! Querydsl - 인프런 | 강의 Querydsl의 기초부터 실무 활용까지 한번에 해결, 본 강의는 자바 백엔드 개발의 실전 코스를 완성하는 마지막 강의 입니다. 스프링 부

juran-devblog.tistory.com

2021.03.11 - [web/[study] jpa] - [JPA 기초] 프록시와 지연로딩

 

[JPA 기초] 프록시와 지연로딩

객체 조회 시 해당 객체가 참조하는 객체의 정보도 한번에 모두 불러와야 하는가? ex) MEMBER 객체가 TEAM 객체를 참조할 때, MEMBER 조회 시 TEAM도 함께 조회해야 하는가? 이전 코드 Member member = new Membe

juran-devblog.tistory.com

2021.04.21 - [web/[study] jpa] - [JPA 활용] API 개발과 성능 최적화(2)

 

[JPA 활용] API 개발과 성능 최적화(2)

컬렉션 조회 최적화 V1. 엔티티 직접 노출 V2. 엔티티를 DTO로 반환 V3. 페치조인 최적화 public List findAllV3() { return em.createQuery( "select distinct o from Order o" + " join fetch o.member m" + " j..

juran-devblog.tistory.com


 

 

DTO

BoardDto

@Getter
@NoArgsConstructor
public class BoardDto {

    private Long boardNo;
    private String boardTitle;
    private String boardContent;
    private Long memberNo;
    private String memberName;
    private String memberId;
    private String regDate;
    private int boardViewCnt;
    private int boardRcmdCnt;

    @Builder
    public BoardDto(Board board) {
        // Board -> BoardDto
        this.boardNo = board.getBoardNo();
        this.boardTitle = board.getBoardTitle();
        this.boardContent = board.getBoardContent();
        this.memberNo = board.getMember().getMemberNo();
        this.memberName = board.getMember().getMemberNm();
        this.memberId = board.getMember().getMemberId();
        this.boardViewCnt = board.getBoardViewCnt();
        this.boardRcmdCnt = board.getBoardRcmdCnt();
        this.regDate = board.getRegDate().toString();
    }
}

Board로 조회 결과를 받고 Dto로 반환하는 경우가 많아 빌더 패턴을 이용하였다.

 

 

구현(1) JpaRepository

querydsl로 구현하기 전 문득 spring data jpa가 간단한 검색 기능을 제공하지 않을까? 싶어 생각나는 대로 그냥 써봤는데..

 

BoardRepository

List<Board> findAllByCategory_CategoryNo(long categoryNo);

spirng data jpa는 findBy나 findAllBy뒤에 필드명을 붙이면 해당 필드로 검색이 가능하다.

단순히 해당 엔티티의 필드명 뿐 아니라 위처럼 연관관계에 있는 엔티티로도 join연산을 통해 검색할 수 있다 !!

 

BoardService

@Transactional
public List<BoardDto> findBoardByCategory(long categoryNo) {
    List<Board> boards = boardRepository.findAllByCategory_CategoryNo(categoryNo);
    
    // Board -> BoardDto
    return boards.stream().map(b -> new BoardDto(b)).collect(Collectors.toList());
}

List<Board>로 받은 값을 List<BoardDto>로 바꿔 반환

 

BoardApiController

@GetMapping("list/{categoryNo}")
public ResponseEntity<?> boardListByCategory(@PathVariable(name = "categoryNo") long categoryNo) {

    ApiResponse apiResponse = new ApiResponse(true, "카테고리별 게시물 조회");
    apiResponse.putData("boardList", boardService.findBoardByCategory(categoryNo));

    return ResponseEntity.ok(apiResponse);
}

 

>> 실행되는 쿼리

select
        board0_.board_no as board_no1_0_,
        board0_.reg_date as reg_date2_0_,
        board0_.update_date as update_d3_0_,
        board0_.board_content as board_co4_0_,
        board0_.board_rcmd_cnt as board_rc5_0_,
        board0_.board_title as board_ti6_0_,
        board0_.board_view_cnt as board_vi7_0_,
        board0_.category_no as category8_0_,
        board0_.member_no as member_n9_0_ 
    from
        board board0_ 
    left outer join
        category category1_ 
            on board0_.category_no=category1_.category_no 
    where
        category1_.category_no=?
        

select
        category0_.category_no as category1_1_0_,
        category0_.category_name as category2_1_0_ 
    from
        category category0_ 
    where
        category0_.category_no=?
        
        
select
        member0_.member_no as member_n1_3_0_,
        member0_.reg_date as reg_date2_3_0_,
        member0_.update_date as update_d3_3_0_,
        member0_.member_email as member_e4_3_0_,
        member0_.member_id as member_i5_3_0_,
        member0_.member_nm as member_n6_3_0_,
        member0_.member_pw as member_p7_3_0_,
        member0_.member_role as member_r8_3_0_,
        member0_.member_tell as member_t9_3_0_,
        member0_.member_yn as member_10_3_0_ 
    from
        member member0_ 
    where
        member0_.member_no=?

 

간편하긴 했지만 문제가 있었다 😢

지연로딩으로 인하여 쿼리가 여러번 날아갔을 뿐 아니라 N+1문제 가 발생하였다.

 

 

구현(2) querydsl

다시 QueryDSL로 돌아와서 JPARepository를 이용할 때 QueryDSL을 어떻게 써야하는지부터 알아보자 (서비스와 컨트롤러는 동일)

 

BoardRepositoryCustom

- querydsl 을 쓰는 메서드들을 정의하는 레퍼지토리 인터페이스

public interface BoardRepositoryCustom {

    List<BoardDto> findBoardAllByCategoryNo(long categoryNo);
}

 

BoardRepositoryImpl

- 위의 BoardRepsitoryCustom를 상속받아 해당 인터페이스에 정의된 메서드들을 queryDSL 을 이용하여 구현(오버라이딩)

@RequiredArgsConstructor
public class BoardRepositoryImpl implements BoardRepositoryCustom {

    private final EntityManager em;

    @Override
    public List<BoardDto> findBoardAllByCategoryNo(long categoryNo) {
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

        List<Board> boards = queryFactory
                .selectFrom(board)
                .where(board.category.categoryNo.eq(categoryNo))
                .fetch();

        return boards.stream().map(b -> new BoardDto(b)).collect(Collectors.toList());
    }

}

이렇게 작성하면 아까와 같은 똑같은 문제가 발생한다..

바로 지연로딩으로 인한 N+1 문제

 

이를 해결하기 위해서는 fetch join 을 이용해야 한다.

 

BoardRepositoryImpl

@Override
public List<BoardDto> findBoardAllByCategoryNo(long categoryNo) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    List<Board> boards = queryFactory
            .selectFrom(board)
            .leftJoin(board.category, category).fetchJoin()
            .leftJoin(board.member, member).fetchJoin()
            .where(board.category.categoryNo.eq(categoryNo))
            .fetch();

    return boards.stream().map(b -> new BoardDto(b)).collect(Collectors.toList());
}

서비스단에서 DTO로 변환할지 레퍼지토리에서 반환할 때 변환할 지 고민했었는데 일단 이렇게 작성하였다.

 

* 하지만 레퍼지토리는 DAO이기도하고.. 한번 작성된 쿼리의 재사용성을 위해 DTO로 변환하는 로직은 서비스에 포함되는게 더 맞는 것 같다(고 생각된다..)

 

실행되는 쿼리

select
        board0_.board_no as board_no1_0_0_,
        category1_.category_no as category1_1_1_,
        member2_.member_no as member_n1_3_2_,
        board0_.reg_date as reg_date2_0_0_,
        board0_.update_date as update_d3_0_0_,
        board0_.board_content as board_co4_0_0_,
        board0_.board_rcmd_cnt as board_rc5_0_0_,
        board0_.board_title as board_ti6_0_0_,
        board0_.board_view_cnt as board_vi7_0_0_,
        board0_.category_no as category8_0_0_,
        board0_.member_no as member_n9_0_0_,
        category1_.category_name as category2_1_1_,
        member2_.reg_date as reg_date2_3_2_,
        member2_.update_date as update_d3_3_2_,
        member2_.member_email as member_e4_3_2_,
        member2_.member_id as member_i5_3_2_,
        member2_.member_nm as member_n6_3_2_,
        member2_.member_pw as member_p7_3_2_,
        member2_.member_role as member_r8_3_2_,
        member2_.member_tell as member_t9_3_2_,
        member2_.member_yn as member_10_3_2_ 
    from
        board board0_ 
    left outer join
        category category1_ 
            on board0_.category_no=category1_.category_no 
    left outer join
        member member2_ 
            on board0_.member_no=member2_.member_no 
    where
        board0_.category_no=?

이렇게 연관관계의 모든 필드가 select절에 포함되어 하나의 쿼리로 결과를 조회할 수 있다.

 

* 조회하는 필드의 수를 줄이기 위해 select절에 사용할 필드들만 작성하기도 하는데 필드 수가 대단히 많은 것이 아니면 조회하는 필드 수는 쿼리 성능에 크으게 영향을 주지는 않는다고 한다.

 

페치 조인에 대한 개념이 막연했는데 직접 작성하고 쿼리를 비교해보니 이해가 확실히 됐다. 역시 강의만 들어서는 소용업따 직접 해보는게 답..! querydsl 사용하여 이것저것 필요한 로직들을 가볍게 구현중인데 나름 재밌는것 같기도 ㅎㅅㅎ

 

 

 

++) 추가

재밌는 김에 하나 더

 

▷ 마이페이지에서 본인이 작성한 게시글 관리하기

BoardRepositoryCustom

public interface BoardRepositoryCustom {

    // 사용자가 작성한 게시글 전체 조회
    List<BoardDto> findBoardAllByMemberId(String memberId);

    // 사용자가 작성한 게시글 카테고리별 조회
    List<BoardDto> findBoardAllByMemberIdAndCategoryNo(String memberId, long categoryNo);

}

BoardRepositoryImpl

// 사용자가 작성한 게시글 전체 조회
@Override
public List<BoardDto> findBoardAllByMemberId(String memberId) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    List<Board> boards = queryFactory
            .selectFrom(board)
            .leftJoin(board.category, category).fetchJoin()
            .leftJoin(board.member, member).fetchJoin()
            .where(board.member.memberId.eq(memberId))
            .fetch();

    return boards.stream().map(b -> new BoardDto(b)).collect(Collectors.toList());
}

// 사용자가 작성한 게시글 카테고리별 조회
@Override
public List<BoardDto> findBoardAllByMemberIdAndCategoryNo(String memberId, long categoryNo) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    List<Board> boards = queryFactory
            .selectFrom(board)
            .leftJoin(board.category, category).fetchJoin()
            .leftJoin(board.member, member).fetchJoin()
            .where(board.member.memberId.eq(memberId).and(board.category.categoryNo.eq(categoryNo)))
            .fetch();

    return boards.stream().map(b -> new BoardDto(b)).collect(Collectors.toList());
}

BoardApiController

@GetMapping("list/me")
public ResponseEntity<?> boardListByMemberIde(Authentication authentication) {

    ApiResponse apiResponse = new ApiResponse(true, "본인이 작성한 게시물 조회");
    apiResponse.putData("boardList", boardService.findBoardByMemberId(authentication.getPrincipal().toString()));
    
    return ResponseEntity.ok(apiResponse);
}

@GetMapping("list/me/{categoryNo}")
public ResponseEntity<?> boardListByMemberIdAndCategory(Authentication authentication, @PathVariable(name = "categoryNo") long categoryNo) {

    ApiResponse apiResponse = new ApiResponse(true, "본인이 작성한 게시물 카테고리별 조회");
    apiResponse.putData("boardList", boardService.findBoardByMemberNameAndCategory(authentication.getPrincipal().toString(), categoryNo));
    
    return ResponseEntity.ok(apiResponse);
}

회원정보 수정과 동일하게 Authentication 객체로 현재 로그인한 사용자 정보를 가져온다.

- principal: 로그인한 사용자 아이디

 

~ 끝 ~

728x90

관련글 더보기

댓글 영역