티스토리 뷰
솔직히 이번 파트가 제일 오래 걸린거 같다
쿼리에 익숙하지 않아서인지 한줄 한줄 해석하는데 시간이 오래 걸렸다.
요구사항을 보면
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>
//시작일이 있으면 "그 날짜 이상"이라는 조건 추가. >는 XML에서 >를 못 그대로 쓰니까 대신 쓰는 표기.
<if test="condition.startDate != null">
AND b.created_at >= #{condition.startDate}
</if>
//종료일이 있으면, "그 날짜의 다음 날 0시 미만"으로 조건을 잡음. 이렇게 하는 이유는 created_at이 시분초까지 있는 값이라
//그냥 <= endDate만 쓰면 종료일 당일 자정 이후에 쓴 글이 빠지는 문제가 생기기 때문.
<if test="condition.endDate != null">
AND b.created_at < 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의 몫이라고 정리했다.
'게시판 만들기 > 4주차' 카테고리의 다른 글
| 만들자 게시판 4주차 (4) - 게시글과 첨부파일 연동하기 (0) | 2026.07.25 |
|---|---|
| 만들자 게시판 4주차 (2) - 유효성 검증 추가 (1) | 2026.07.24 |
| 만들자 게시판 4주차 (1) - 첨부파일 삭제 API 구현 (0) | 2026.07.24 |
