카테고리 리스트로 메뉴가 구성되고 메뉴를 눌렀을 때 해당 카테고리에 등록된 게시글을 조회하고 싶다.
조건이 들어간 쿼리를 작성하기 위해 QueryDSL을 이용하였다.
[참고]
2021.04.29 - [web/queryDSL] - queryDSL이란
2021.03.11 - [web/[study] jpa] - [JPA 기초] 프록시와 지연로딩
2021.04.21 - [web/[study] jpa] - [JPA 활용] API 개발과 성능 최적화(2)
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로 반환하는 경우가 많아 빌더 패턴을 이용하였다.
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문제 가 발생하였다.
다시 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: 로그인한 사용자 아이디
~ 끝 ~
[ERROR] 스프링부트 UnsatisfiedDependencyException (0) | 2021.08.24 |
---|---|
[QueryDSL] 동적쿼리로 사용자 조회하기 (0) | 2021.05.18 |
[JPA] 사용자 비밀번호 확인과 정보 수정 (0) | 2021.04.29 |
[ERROR] 스프링부트 테스트 IllegalStateException (0) | 2021.04.29 |
[JPA] 회원가입시 아이디 중복검사 (0) | 2021.04.20 |
댓글 영역