728x90

게시판을 만들다 보니 결국 필요한 게 페이지네이션이다.
데이터가 많아지면 한 번에 다 가져오는 건 말이 안 되고, 직접 LIMIT / OFFSET을 써서 페이징을 구현해보기로 했다.
이번 글에서는 Spring Boot + MyBatis 환경에서 게시글 목록 페이지네이션을 어떻게 설계하고 구현했는지 정리했다!
1. 요청 DTO 정의
// GetPostListReq.java
@Getter
@Setter
public class GetPostListReq {
private String title; // 제목 검색
@Min(value = 1, message = "페이지는 1 이상이어야 합니다.")
private int page = 1; // 페이지
@Min(value = 1, message = "페이지 크기는 1 이상이어야 합니다.")
private int size = 10; // 페이지 크기
public int getOffset() {
return (page - 1) * size;
}
}
valid를 설정해 주면 조회 시 오류메세지가 뜬다.

2. 목록 조회 쿼리 작성
게시글 목록 조회
- title 값이 있으면 검색 조건 추가
size→ 한 페이지당 개수offset→ 몇 개 건너뛸지
// PostMapper.xml
<select id="getPostList" resultType="GetPostListRow">
SELECT
p.post_id,
p.title,
p.content,
p.date_created,
p.date_updated,
u.user_id,
u.name AS user_name
FROM posts p
JOIN users u ON p.user_id = u.user_id
WHERE p.del_yn = 'N'
AND u.del_yn = 'N'
<if test="title != null and title != ''">
AND p.title LIKE CONCAT('%', #{title}, '%')
</if>
LIMIT #{size}
OFFSET #{offset}
</select>
전체 게시글 수 조회
<select id="getPostCount" resultType="int">
SELECT COUNT(*)
FROM posts p
JOIN users u ON p.user_id = u.user_id
WHERE p.del_yn = 'N'
AND u.del_yn = 'N'
<if test="title != null and title != ''">
AND p.title LIKE CONCAT('%', #{title}, '%')
</if>
</select>
목록 조회 시 join 했던 것 처럼 전체 게시글 수 조회할 때도 조건이 같아야 한다. 반드시 동일해야 페이지 수가 정확하다!!
XML의 select id="getPostList" , select id="getPostCount">와 연결해주기 위해 Mapper 내에 SQL 매핑 메서드를 써줘야 한다.
// PostMapper.java
@Mapper
public interface PostMapper {
int getPostCount(@Param("req") GetPostListReq req);
List<GetPostListRow> getPostList(@Param("req") GetPostListReq req);
}
3. Row DTO
DB 결과 그대로 담는 DTO
@Getter
@Setter
public class GetPostListRow {
private Long postId; // 게시글 ID
private Long userId; // 사용자 ID
private String userName; // 사용자 이름
private String title; // 제목
private String content; // 내용
private LocalDateTime dateCreated; // 생성일
private LocalDateTime dateUpdated; // 최종 수정일
}
4. 응답 DTO
나는 아래처럼 user 객체 안에 userId 와 userName 을 넣고 싶었다.
"user": {
"userId": 3,
"userName": "홍길동"
},
// GetPostListRes.java
@Getter
@Builder
public class GetPostListRes {
private Long postId;
private UserInfo user;
private String title;
private String content;
private LocalDateTime dateCreated;
private LocalDateTime dateUpdated;
public static GetPostListRes of(GetPostListRow row) {
UserInfo userInfo = UserInfo.builder()
.userId(row.getUserId())
.userName(row.getUserName())
.build();
return GetPostListRes.builder()
.postId(row.getPostId())
.user(userInfo)
.title(row.getTitle())
.content(row.getContent())
.dateCreated(row.getDateCreated())
.dateUpdated(row.getDateUpdated())
.build();
}
@Getter
@Builder
public static class UserInfo {
private Long userId;
private String userName;
}
}
- 내부 static 클래스 UserInfo를 만든다.
- Row에 흩어져 있는 유저 필드를 user 객체로 묶는다.
5. 공통 페이징 응답 DTO
나중에 게시글 뿐만 아니라 댓글 리스트, 유저 리스트 등 리스트가 많아질 때를 대비해 공통적으로 적용될 수 있는 response를 만들고 싶었다.
// PageRes.java
@Getter
public class PageRes<T> {
private List<T> list;
private int totalCount;
private int page;
private int size;
private PageRes(List<T> list, int totalCount, int page, int size) {
this.list = list;
this.totalCount = totalCount;
this.page = page;
this.size = size;
}
public static <T> PageRes<T> of(
List<T> list, // 리스트
int totalCount, // 전체 게시글 수
int page, // 현재 페이지 번호
int size // 현재 페이지에 보여지는 게시글 수) {
return new PageRes<>(list, totalCount, page, size);
}
}
제네릭 <T> 의미: 아래처럼 쓸 수 있다.
→ 리스트 안에 어떤 타입이 들어올지 유연하게 처리
PageRes<GetPostListRes>
PageRes<UserRes>
PageRes<CommentRes>
6. Service
public PageRes<GetPostListRes> getPostList(GetPostListReq req) {
int totalCount = postMapper.getPostCount(req);
List<GetPostListRow> rows = postMapper.getPostList(req);
List<GetPostListRes> list = rows.stream()
.map(GetPostListRes::of) // map(row -> GetPostListRes.of(row))
.toList();
return PageRes.of(list, totalCount, req.getPage(), req.getSize());
}
- 위에서 만든 공통 페이지 응답을 써준다.
- 전체 개수 조회, 실제 데이터 조회 후 내가 만든 GetPostListRes 형태로 반환을 해준다. (GetPostListRow → GetPostListRes)
.map(GetPostListRes::of)
.map(row -> GetPostListRes.of(row))와 같은 의미- rows를 하나씩 꺼냄
- of()로 변환
- 리스트로 모음
정리하자면...
Controller
↓
Service (여기)
1. totalCount 조회
2. 목록 조회
3. Row → Res 변환
4. PageRes로 감싸기
↓
Controller → JSON 응답
7. Controller
마지막 GET /list 요청을 처리하는 컨트롤러까지 작성해주면 끝이다.
(여기서 @Valid 를 설정해서 맨 위 사진에 오류메세지가 떴었던 것임!)
@GetMapping("/list")
public ResponseEntity<Object> getPostList(
@AuthenticationPrincipal MemberVO auth,
@Valid @ModelAttribute GetPostListReq req) {
PageRes<GetPostListRes> PageRes = postService.getPostList(req);
return ApiRes.success(PageRes).ok();
}
내가 설계한대로 잘 나온다 🎉🎉
- user 객체
- params로 page, size 받기
- 응답에 totalCount, page, size 나옴

'✨BACKEND > 📍Java' 카테고리의 다른 글
| Spring Boot에서 Swagger(OpenAPI) 문서 만들기 (0) | 2026.03.09 |
|---|