vue + jpa project (10) - 게시판 검색조건 백엔드 처리 본문

프로그램/Vue.js

vue + jpa project (10) - 게시판 검색조건 백엔드 처리

반응형

앞서 보다 효율적인 조건처리 방식에 대해서 Query DSL을  이용하기로 하였다. 

 

Query DSL을 이용하기 위하여 별도의 Repository를 구성하고 

조회조건을 받아들이기 위해서 기존의 Dto에 필드를 추가하고 비교 조건을 구현한다.

그리고 서비스 단과 컨트롤 단에 메소드를 각각 추가하여 연동하도록 한다.

 

 

 

1. BoardDto 수정

   BoardDto를 이용하여 검색조건으로도 사용하기 위함이다. 이렇게 하는 이유는 해당 Dto에 조회를 하는 항목이 이미

  들어있기 때문에 추가 조회조건이 생겼을 때 유연하게 대처하기 위함이다.

 

  우선, 기존 사용했던 항목은 boardNo 부터 updDate  까지이고, 여기에서 추가적으로 3개 항목을 더 추가했다. 

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class BoardDto implements Serializable {
    
    private static final long serialVersionUID = 3056502198773946984L;

    private Long boardNo;
    private String title;
    private String writer;
    private String content;
    private String pictureUrl;
    private String regDate;
    private String updDate;
    
    private String searchKey;		// 추가된 항목
    private String searchValue;		// 추가된 항목
    private String searchCondition;	// 추가된 항목
    
    // where 절 처리 메소드
    public BooleanExpression makeSearchCondition(Object qEntity, String gubun) {

        if(qEntity instanceof QBoardEntity) {
            
            if("boardNo".equals(gubun) && boardNo != null && boardNo > 0) {
                    return ((QBoardEntity)qEntity).boardNo.eq(boardNo);
            } else if("writer".equals(gubun) && StringUtils.hasLength(writer)) {
                    return ((QBoardEntity)qEntity).writer.contains(writer);
            } else if("regDate".equals(gubun) && StringUtils.hasLength(regDate)) {

                if("regDateGT".equals(searchCondition)) {
                    return ((QBoardEntity)qEntity).regDate.gt(LocalDateTime.of(LocalDate.parse(regDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")), LocalTime.MIN));
                }
                
                if("regDateGE".equals(searchCondition)) {
                    return ((QBoardEntity)qEntity).regDate.goe(LocalDateTime.of(LocalDate.parse(regDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")), LocalTime.MIN));
                }
            }
            
            if("writer".equals(gubun) && "writer".equals(searchKey)) {
                if(StringUtils.hasLength(searchValue)) {
                    return ((QBoardEntity)qEntity).writer.contains(searchValue);
                }
            } else if ("title".equals(gubun) && "title".equals(searchKey)) {
                if(StringUtils.hasLength(searchValue)) {
                    return ((QBoardEntity)qEntity).title.contains(searchValue);
                }
            } else if ("content".equals(gubun) && "content".equals(searchKey)) {
                if(StringUtils.hasLength(searchValue)) {
                    return ((QBoardEntity)qEntity).content.contains(searchValue);
                }
            }
        }
        
        return null;
    }
}

  그리고, makeSearchCondition 메소드를 보면 (일단 일부만 샘플로 추가했다. ) 

  우선 instanceof를 이용하여 QEntity 가 사용하는 것에 맞는 것인지를 우선 판단하고, 

  기존 항목의 조회조건이 들어올 경우, 그에 맞는 조회조건을 생성하도록 하였으며, 

  아래쪽은 지금 현재 조회 조건으로 처리하기 위하여 추가했다.  (검색조건이 Dto항목과 달라서 별도 처리로 가져감)

  현재는 아래쪽 조건을 탈 예정이라 기본 Dto 항목에 대한 비교 및 처리 부분은 처리가 안된다. 

  향후에 위의 조건으로 프론트에서 값을 전달시켜서 확인해보면 보다 인지가 빠르게 될 것이다.

 

  다른 개발자의 정리된 사이트를 보면 개별 항목별로 BooleanExpression 메소드를 사용하여 동적 쿼리가 적용되도록 

  한 것을 많이 보았지만, 나에게는 조금 코딩에 대한 번거로움과 항목이 더 추가되었을때에 개별적으로 수정을 해야하는 

  단점이 있어 보여서  공통 메소드로 구현하였다.

 

  어떻게 보면 이 메소드가 Query DSL 사용시의 동적 쿼리의 핵심이 아닐까 싶다. 

 

2. BoardRepositoryCondition 생성
   Query DSL을 사용하면 interface 가 아닌 Class로 별도 만들어야만 처리가 가능하다. 

   기존 JPA Repository를 사용하는 것이 아니라 QBoardEntity 를 가지고 JPAQueryFactory 를 이용하여 처리를 해야하기 

   때문이다. 

 

   아래는 소스이다.    

@RequiredArgsConstructor
@Repository
public class BoardRepositoryCondition {
    
    @PersistenceContext
    private EntityManager em;

    public Page<BoardEntity> searchBoardCondition(Pageable pageable, BoardDto boardDto) {
        QBoardEntity boardEntity = QBoardEntity.boardEntity;
        JPAQueryFactory queryFactory = new JPAQueryFactory(em);

        JPAQuery<BoardEntity> query = queryFactory.selectFrom(boardEntity)
                .where(
                        boardDto.makeSearchCondition(boardEntity, "boardNo"),
                        boardDto.makeSearchCondition(boardEntity, "writer"),
                        boardDto.makeSearchCondition(boardEntity, "title"),
                        boardDto.makeSearchCondition(boardEntity, "content"),
                        boardDto.makeSearchCondition(boardEntity, "regDate")
                      );

        long total = query.stream().count();
        
        // 컨트롤러에 기준을 가지고 OrderBy를 생성함 
        OrderSpecifier<?>[] orders = pageable.getSort().stream().map(order -> {
          Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
          PathBuilder orderByExpression = new PathBuilder<>(BoardEntity.class, order.getProperty());
          return new OrderSpecifier<>(direction, orderByExpression);
        }).toArray(OrderSpecifier[]::new);

        List<BoardEntity> results = query
                .where(
                        boardDto.makeSearchCondition(boardEntity, "boardNo"),
                        boardDto.makeSearchCondition(boardEntity, "writer"),
                        boardDto.makeSearchCondition(boardEntity, "title"),
                        boardDto.makeSearchCondition(boardEntity, "content"),
                        boardDto.makeSearchCondition(boardEntity, "regDate")
                      )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(orders)
                .fetch();

        return new PageImpl<>(results, pageable, total);
    }

}

 내용을 보면 기존에 generated 폴더에 생성된 QBoardEntity 객체를 가져오고, 그다음 EntityManager 를 이용하여

 JPAQueryFactory 를 생성시킨 뒤에 위의 엔티티를 조회하면서 where 절에서 각각의 파라미터를 처리하도록 하였다.

 

일단 나중에 컨트롤러로 넘어간 자료를 toString()을 이용하여 본다면 아래와 같이 넘어 간다고 가정한다.

BoardDto(boardNo=null, title=null, writer=null, content=null, pictureUrl=null, regDate=null, updDate=null,

searchKey=writer, searchValue=8, searchCondition=null) searchKey와 searchValue 값만 있다.

BoardDto에 조회조건이 넘어간 게 earchKey와 searchValue 만 있으니 위의 where 절에 사용된 건 "writer" 메소드 뿐이다.

나머지는 null 이 리턴되면서 조회조건에서 빠진다. 

 

즉 최종 where 절에 적용 되는 부분은 아래 소스 쪽이다. 다른 건 조건에 맞지않아서 모두 null 을 리턴한다.

            if("writer".equals(gubun) && "writer".equals(searchKey)) {
                if(StringUtils.hasLength(searchValue)) {
                    return ((QBoardEntity)qEntity).writer.contains(searchValue);
                }
            }

[처리된 쿼리]
Hibernate: select b1_0.board_no,b1_0.content,b1_0.picture_url,b1_0.reg_date,b1_0.title,b1_0.upd_date,b1_0.writer from board b1_0 where b1_0.writer like ? escape '!'

where 절은 일단 5개만 하였으나 조회조건 메소드와 병행하여 여러개를 미리 만들어 놓고 where 절에 모두 호출하는

메소드를 미리 만들어 놓고 사용한다면 추후에 백엔드를 수시로 고치는 일이 줄어들 것이다.


이제  '// 컨트롤러에 기준을 가지고 OrderBy를 생성함'  아래쪽을 보면 컨트롤러에서 지정한 sort 기준을 그대로 적용하기

위한 처리 부분이다. 이 부분은 최종 Paging 처리를 위한 query에서 orderBy 쪽에 사용하기 위한 부분이다. 

그래서 일단 컨트롤러 소스 일부를 먼저 보겠다. 

    @GetMapping("/board/list_paging_search")
    public Header<List<BoardDto>> boardList_paging_search(
     @SortDefault.SortDefaults({
     @SortDefault(sort = {"boardNo"}, direction = Sort.Direction.ASC),
     @SortDefault(sort = {"writer"}, direction = Sort.Direction.DESC)})
     @PageableDefault Pageable pageable
            , BoardDto boardDto)

여기에서 다중 필드 소트를 예시로 했다. 하나는 워낙 많아서 조금 번거로운 멀티 필드 소트를 적용했다.

여기를 보면 sort 대상 필드는 boardNO, writer 이고 각각이 sorting은 ASC, DESC 로 하였다. 

이렇게 명시하여 Pageable로 대입이 되었고, 그 자료가 위의 repository 메소드로 전달이 된다. (아직 서비스도 없지만)

그래서  stream을 이용하여 sort의 개수만큼 동작하면서 최종엔 OrderSpecifier 의 배열로 적재가 된다.

(다만 여기서 다른 entity와의 join이 들어간다면 조금 더 복잡해 지거나 다른 방법을 강구해야 할 수 있다)


최종 results 에 기본 쿼리와 where, offset, limit, orderby까지 적용된 데이터가 적재된다.

 

3. BoardService 수정
   BoaardDto 파라미터가 추가된 메소드를 추가하였다. 이 부분에서 기존과 달라진 부분은 위에서 새로 만든 

   Repository 의 searchBoardCondition 을 호출하는 것이다.

   아래는 소스이다.    

    // 상단에 추가
    private final BoardRepositoryCondition boardRepositoryCondition;

    /**
     * 게시글 목록 조회(조건포함)
     */
    public Header<List<BoardDto>> getBoardList(Pageable pageable, BoardDto boardDto) {

    	Page<BoardEntity> boardEntities = boardRepositoryCondition.searchBoardCondition(pageable, boardDto);
    	List<BoardDto> dtoList = new ArrayList<>();
    	
    	for (BoardEntity entity : boardEntities) {
            dtoList.add(entityToDto(entity));
        }
    	
    	Pagination pagination = new Pagination(
    			(int)boardEntities.getTotalElements()
    			, pageable.getPageNumber() + 1
    			, pageable.getPageSize()
    			, 10
    	);

    	return Header.OK(dtoList, pagination);
    }

 

4. BoardController 수정
   기존 소스를 유지를 위해서 별도의 메소드를 만들고 맵핑도 따로 만들었다

   그리고 @SortDefault 와 @PageableDefault 어노테이션을 이용하여 미리 설정하였고, BoardDto를 조회조건으로 

   받는 역할을 하기위해서 파라미터로 추가하였다. 

    @GetMapping("/board/list_paging_search")
    public Header<List<BoardDto>> boardList_paging_search(
            @SortDefault.SortDefaults({
                @SortDefault(sort = {"boardNo"}, direction = Sort.Direction.DESC),
                @SortDefault(sort = {"writer"}, direction = Sort.Direction.ASC)})
            @PageableDefault Pageable pageable
            , BoardDto boardDto)
    {
        System.out.println("~~~~~>" + boardDto.toString());
        System.out.println("~~~~~>" + pageable.getSort());
        System.out.println("~~~~~>" + pageable.getPageNumber());
        System.out.println("~~~~~>" + pageable.getPageSize());
        return boardService.getBoardList(pageable, boardDto); 
    }

System.out.println 으로 몇가지를 찍어보았다. 그래야 어떻게 값이 넘어오는지를 알 수 있기 때문이다. 

그리고 새로운 서비스 메소드를 호출한다. 

 

이것으로 일단 백엔드 처리 부분을 설명하였다. 

다음 장에서 프론트 처리 부분을 마저 설명하겠다.

반응형

프로그램/Vue.js Related Articles

MORE

Comments