본문 바로가기
✨BACKEND/📍Java

Spring Boot + MyBatis 게시판 페이지네이션 구현 (LIMIT, OFFSET)

by 짱돌보리 2026. 3. 3.
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