티스토리 뷰

솔직히 이번 파트가 제일 오래 걸린거 같다

쿼리에 익숙하지 않아서인지 한줄 한줄 해석하는데 시간이 오래 걸렸다.

요구사항을 보면
1. 제목/작성자/내용 텍스트 검색
2. 카테고리 필터
3. 등록일 검색, 디폴트 최근 1년
4. 페이징 (10개씩, 총 건수)
5. 목록에 첨부파일 유무 표시

이렇게 되있었다.

 

동적 SQL을 사용했는데, 조건이 있을 수도 있고 없을수도 있으니, SQL을 상황에 따라 조립해야 했다.

//"selectBoardList라는 이름의 SQL이고, 실행 결과 각 행을 BoardEntity 객체에 담아라"는 선언.
<select id="selectBoardList" resultType="com.study.board.entity.BoardEntity">
//board 테이블(별명 b)에서 가져올 컬럼들 나열. BoardEntity의 필드 이름과 매칭돼서 자동으로 채워짐.
  SELECT b.id, b.category_id, b.title, b.writer, b.content, b.view_count, b.created_at, b.updated_at,
         EXISTS (SELECT 1 FROM files f WHERE f.board_id = b.id) AS has_attachment
  FROM board b
  <where>
  //"파라미터로 넘어온 condition 객체의 categoryId가 null이 아니면, AND category_id = (그 값) 조건을 SQL에 끼워 넣어라." 
  //null이면 이 줄 자체가 통째로 빠짐.
    <if test="condition.categoryId != null">
      AND b.category_id = #{condition.categoryId}
    </if>
      //"keyword가 null도 아니고 빈 문자열도 아니면" 끼워 넣는 조건. 
      //LIKE '%키워드%'는 그 단어가 앞뒤로 뭐가 붙든 상관없이 포함만 되면 매칭되게 하는 문법. 
      //제목/작성자/내용 세 곳 중 어디든 하나만 매칭돼도 되게 OR로 묶고, 전체를 괄호로 감싸서 카테고리 조건과는 AND로 분리되게 함.
    <if test="condition.keyword != null and condition.keyword != ''">
      AND (b.title LIKE CONCAT('%', #{condition.keyword}, '%')
           OR b.writer LIKE CONCAT('%', #{condition.keyword}, '%')
           OR b.content LIKE CONCAT('%', #{condition.keyword}, '%'))
    </if>
    //시작일이 있으면 "그 날짜 이상"이라는 조건 추가. &gt;는 XML에서 >를 못 그대로 쓰니까 대신 쓰는 표기.
    <if test="condition.startDate != null">
      AND b.created_at &gt;= #{condition.startDate}
    </if>
    //종료일이 있으면, "그 날짜의 다음 날 0시 미만"으로 조건을 잡음. 이렇게 하는 이유는 created_at이 시분초까지 있는 값이라
    //그냥 <= endDate만 쓰면 종료일 당일 자정 이후에 쓴 글이 빠지는 문제가 생기기 때문.
    <if test="condition.endDate != null">
      AND b.created_at &lt; DATE_ADD(#{condition.endDate}, INTERVAL 1 DAY)
    </if>
  </where>
  //작성일이 최신인 순서로 정렬하고, 혹시 작성일이 완전히 같은 행이 여러 개면 id가 큰 순서로 한 번 더 정렬(순서를 안정적으로 고정하는 보조 기준).
  ORDER BY b.created_at DESC, b.id DESC
  //정렬까지 끝난 결과 중에서, offset만큼 앞에서부터 건너뛰고, 그다음부터 size개만 잘라서 가져와라. 이게 페이지네이션의 실체
  LIMIT #{condition.size} OFFSET #{offset}
</select>

 

<if test="...">는 조건이 참일 때만 안의 SQL을 끼워 넣고, <where>는 조립된 조건들 맨 앞의 불필요한 AND를 알아서 지워준다. 

EXISTS (...)는 값의 내용엔 관심 없고 "행이 존재하는지"만 boolean으로 알려주는 서브쿼리라서, 첨부파일 유무 체크에 딱 맞았다.

 

페이징은 두 쿼리가 필요하다
LIMIT으로 잘라낸 목록만으로는 전체 건수를 알 수 없어서, COUNT(*) 쿼리를 별도로 하나 더 돌려야 했다. 목록용 쿼리와 카운트용 쿼리에 WHERE 조건이 똑같이 들어가야 검색 결과 개수와 총 건수가 일치한다.

 

//BoardService.java
public BoardListResponse getBoardList(BoardSearchCondition condition) {
  // 등록일 종료값이 없으면 오늘, 시작값이 없으면 종료값 기준 1년 전 (요구사항: 디폴트 값이 최근 1년)
  LocalDate resolvedEndDate = (condition.endDate() != null) ? condition.endDate() : LocalDate.now();
  // 종료일을 먼저 구해놔야 시작일을 구할 수 있음
  LocalDate resolvedStartDate = (condition.startDate() != null) ? condition.startDate() : resolvedEndDate.minusYears(1);

  BoardSearchCondition resolvedCondition = new BoardSearchCondition(
      condition.categoryId(), condition.keyword(), resolvedStartDate, resolvedEndDate, condition.page(), condition.size()
  );

  List<BoardResponse> boards = boardRepository.selectBoardList(resolvedCondition, resolvedCondition.offset()).stream()
      .map(boardMapper::toResponse)
      .toList();
  long totalCount = boardRepository.countBoardList(resolvedCondition);
  return new BoardListResponse(boards, totalCount);
}

 

등록일 기본값(없으면 최근 1년) 계산은 Controller가 아니라 Service에 뒀다. Controller는 요청을 그대로 전달만 하고 빈 값을 뭘로 채울지 판단하는 건 비즈니스 로직이라 Service의 몫이라고 정리했다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/08   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함