카테고리 없음

게시판 기능 구현 3 : 게시판 홈(게시글 목록 페이지) 만들기 - 스프링, jsp, 오라클, mybatis

blogOwner 2024. 3. 24. 20:34

 

이제부터 위와 같은 게시판 홈을 만들것이다. 

나눠서 진행할 건데, 

일단은, 이번 포스팅에서는

이 부분과 페이지네이션 부분에 대해 정리해둘 것이다.

 

1 단계 :

아래는 게시판 홈을 클릭할 수 있는 a 태그와, 그 a태그를 클릭했을 때 발송되는 HTTP 요청메세지를 받는 컨트롤러이다. 

<a href="/board-home" id="board-btn" style="font-size: 13px;">
	BOARD
</a>
    @GetMapping("/board-home")
    public String boardHomeControllerMethod(@RequestParam(required = false, defaultValue = "1") Integer page, Model model, @RequestParam(required = false) String hecker) {
        if(hecker != null){
            model.addAttribute("hecker", hecker);
        }
        boardHomeService.findAllPosts(page, model);
        return "/contact/boardHome";
    }

이 컨트롤러에서 받은 매개변수를 보면, page 와 hecker 인데, hecker 는 나중에 다룰 것이다. 

page 라는 매개변수는 뭐냐면, 클라이언트(이 컨트롤러에 get요청을 한 쪽) 에서 전체 페이지 중 몇번째 페이지를 요구했는지에 대한 정보를 담고 있는 매개변수이다.

@RequestParam 으로 받을 때, required=false 로 해두었기 때문에, 안올 경우, defaultValue 1이 바인딩되게 되어 있다.

그래서, BoardHomeService 클래스의 findAllPosts 메서드를 호출하면서, page 매개변수와 model 을 넘겨줬고, 

갔다와서 boardHome.jsp 파일이 렌더링되도록 해주었다.

@Service
@RequiredArgsConstructor
@Slf4j
//@Transactional 안쓴다. 모두 select 쿼리니까.

public class BoardHomeService {

    private final PostsMapper postsMapper;
    private final PaginationService paginationService;
    private final ViewNumberChangeFromNullToZeroService viewNumberChangeFromNullToZeroService;

    public void findAllPosts(Integer page, Model model){
        Integer currentPage = page; // 현재 페이지
        Integer pageSize = 20; // 페이지당 보여질 post 의 수
        Integer totalPosts = postsMapper.findAllPostsCount(); // 모든 post 의 개수
        Integer pageGroupSize = 9; // 그룹당 페이지의 개수

        // 현재 페이지가 보여줘야 하는 PostsDTO 객체들을 담은 List자료구조
        int startRow = (currentPage - 1) * pageSize;
        List<PostsDTO> currentPagePosts = postsMapper.findCurrentPagePosts(startRow, pageSize);
        
        // 게시판 홈에서 현재 조회수가 없더라도 비어있지 않고 0 이 입력되도록 하는 서비스
        List<PostsDTO> currentPagePostsChanged = viewNumberChangeFromNullToZeroService.viewNumberChange(currentPagePosts);
        
        //currentPage,pageSize,totalWritings, pageGroupSize, currentPagePosts
        paginationService.pagination(currentPage,pageSize,totalPosts, pageGroupSize, currentPagePostsChanged, model);

    }

}

그리고, PaginationService 클래스의 pagination 이라는 메서드는 아래와 같이 생겼다. 

@Service
// @Transactional 안쓴다. db관련 코드 없음.
public class PaginationService {

    public void pagination (Integer currentPage, Integer pageSize, Integer totalPosts, Integer pageGroupSize, List<?> currentPagePosts, Model model){
        Integer totalPages = (int) Math.ceil((double) totalPosts / pageSize);

        Integer currentGroup = (int) Math.ceil((double) currentPage / pageGroupSize);
        Integer currentGroupFirstPage = (currentGroup - 1) * pageGroupSize + 1;
        Integer currentGroupLastPage = Math.min(currentGroupFirstPage + pageGroupSize - 1, totalPages);

        model.addAttribute("currentPagePosts", currentPagePosts);
        model.addAttribute("currentPage", currentPage);
        model.addAttribute("totalPages", totalPages);

        model.addAttribute("currentGroupFirstPage", currentGroupFirstPage);
        model.addAttribute("currentGroupLastPage", currentGroupLastPage);
        model.addAttribute("pageGroupSize", pageGroupSize);

    }

}

 

 

우리가 하려는 

이 부분과 페이지네이션을 위해서 필요한 데이터는 무엇일까?

즉 어떤 데이터를 model 에 담아서 boardHome.jsp 로 가야할까?

정확하게 6개의 데이터가 필요하다.

 

2단계 : 6개의 데이터 구하기

 

1. 현재 페이지에 보여질 게시글(PostsDTO)을 담은 List자료구조.

2. 현재 boardHome.jsp 파일에서 렌더링하고 있는 페이지가 "몇 페이지" 인지.

3. 총 페이지 수

4. 현재 페이지 그룹의 첫번째 페이지

5. 현재 페이지 그룹의 마지막 페이지

6. 몇개의 페이지를 하나의 그룹으로 묶었는지.

 

3번의 현재 페이지 그룹의 첫번째 페이지 라고 했는데, 현재 페이지 그룹 이라는 건 무엇일까?

일정한 개수의 게시글을 묶은 걸 하나의 페이지라고 하고, 

일정한 개수의 페이지를 묶은 걸 페이지 그룹이라고 한다. 

 

이 6개를 구해서, model 에 담아주는 걸 목적으로 두고, 어떻게 해야 저 6개를 model 에 담을 수 있을까? 를 고민해야한다.

2. 현재 boardHome.jsp 파일에서 렌더링하고 있는 페이지가 "몇 페이지" 인지. 즉, 현재페이지가 몇페이지인지.

이건, 매개변수로 받아왔지. 끝.

3. 총 페이지 수

이건, 일단, POSTS 테이블에 몇개의 게시글이 있는지를 알아야 한다. 

그런 다음, 한 페이지당 몇개의 게시글을 보여줄지 정해야 한다. 한 페이지당 몇개의 게시글을 보여줄지 정한 것이 pageSize 라는 변수에 담긴 20 이라는 Integer 값이다.

그런 다음, 총 게시글 수 /  pageSize 라고 하면, 총 몇 개의 페이지가 나오는지 알게 된다. 

이 총 페이지 수를 구하는 흐름을 따라가보자. 

findAllPosts 라는 메서드에 있던 코드는 빨강색으로 표현하였고, pagination 이라는 메서드에 있던 코드는 파랑색으로 표현하였다. 

Integer totalPosts = postsMapper.findAllPostsCount(); // 모든 post 의 개수

이 코드로 POSTS 테이블의 모든 행의 개수를 얻어왔다. 따라서, totalPosts 라는 변수에는 총 게시글의 수가 들어있다.

더보기

// 마이바티스 xml 파일에 있는 findAllPostsCount 쿼리.

<select id="findAllPostsCount" resultType="Integer">
SELECT COUNT(*) FROM POSTS
WHERE DELETE_POST_FL = 'N'
</select>

Integer pageSize = 20; // 페이지당 보여질 post 의 수

이제, totalPosts / pageSize 라고 하면, 총 페이지 수가 보여지겠지. 

Integer totalPages = (int) Math.ceil((double) totalPosts / pageSize);

totalPages 라는 변수에 들어있는 값은, 총 페이지 수 이다. 

어떻게 한 거냐면, 현재 매개변수로 받은 totalPosts 와 pageSize 는 정수값이므로, 만약, 53/20 인 경우, 2.xxx 이렇게 소수점이 나오게 되는데, 그냥 소수점은 버리고 정수값인 2만 남기게 된다. 

그래서, totalPosts 와 pageSize 둘 중에 하나를 double 타입으로 바꿔주어 나눗셈을 진행하였다. 그럼, (double) totalPosts / pageSize 이 부분의 결과가 2.xxx 이렇게 소수점으로 나오겠지? 

여기서, Math.ceil 함수를 써서 무조건 올림처리하도록 했다. 예를 들어, (double) totalPosts / pageSize 의 연산결과 2.1 이 나오든, 2.9가 나오든 무조건 3.0 이 되도록 하였다. 그리고 나서 이제 다시 (int) 로 형변환해줘서 totalPages 라는 변수에 담았다. 

 

6. 몇개의 페이지를 하나의 그룹으로 묶었는지.

        Integer pageGroupSize = 9; // 그룹당 페이지의 개수

이 부분이다.

 

4. 현재 페이지가 속한 페이지그룹의 첫번째 페이지

이걸 구하려면, 일단 현재 페이지가 속한 페이지그룹을 알아야 한다. 

 

어떻게 해야, 현재 페이지가 속한 페이지그룹을 알 수 있을까?

현재 페이지 / pageGroupSize(하나의 페이지그룹 당 페이지 개수 )

 

코드에서 흐름을 따라가보자.

현재 요청한 페이지가 몇페이지인지는 currentPage 라는 변수에 담겨있다. 

Integer currentPage = page; // 현재 페이지

 

Integer pageGroupSize = 9; // 그룹당 페이지의 개수

예를 들어, 클라이언트가 12번째 페이지를 요구했는데, 그룹당 페이지 사이즈가 9인 경우라면, 

12/9 인데, Integer 값이라서, 1이 될거 아니야? 그래서, 아까처럼 하나를 double 타입으로 바꾸고, Math.ceil 을 이용해야겠네.

Integer currentGroup = (int) Math.ceil((double) currentPage / pageGroupSize);

이렇게 말이다.

 

이제, 현재 페이지가 속한 그룹을 알았으니, 어떻게 하면 현재 페이지가 속한 페이지그룹의 첫번째 페이지를 알 수 있을까? 

((현재 페이지가 속한 그룹 - 1) * 그룹당 페이지 개수) + 1 이 라고 하면 되겠지.

((currentGroup -1 ) * pageGroupSize) + 1 이렇게 말이다. 

Integer currentGroupFirstPage = (currentGroup - 1) * pageGroupSize + 1;

이렇게 말이다. 

 

5. 현재 페이지 그룹의 마지막 페이지

이건?

현재 페이지가 속한 페이지그룹의 첫번째 페이지 + 그룹당 페이지 개수 - 1 이라고 하면 되겠네.  

Integer currentGroupLastPage = Math.min(currentGroupFirstPage + pageGroupSize - 1, totalPages);

근데, 여기 보면, Math.min 을 사용하고 있는데, 이거 왜그러냐면, 

Math.min( A , B ) 라고 하면 둘 중에 더 작은게 선택되어져서 return 되어진다. 

즉, "현재 페이지가 속한 페이지그룹의 첫번째 페이지 + 그룹당 페이지 개수 - 1 " 과 "페이지 총 개수" 중 더 작은게 리턴되어진다. 그래서, 예를 들어, "현재 페이지가 속한 페이지그룹의 첫번째 페이지 + 그룹당 페이지 개수 - 1 " 를 계산한 값이 20인데, 현재 총 페이지 개수가 15페이지인 경우, "페이지 총 개수" 가 선택되어져서 현재 페이지 그룹의 마지막 페이지가 될 수 있게 되는 것이다. 

 

이제 마지막 하이라이트가 남았다. 

1. 현재 페이지에 보여질 게시글(PostsDTO)을 담은 List자료구조.

일단, PostsDTO 라는 클래스는 아래와 같이 생겼다.

더보기

@Data
public class PostsDTO {
//posts 테이블
private Integer id;
private Integer memberId;
private String title;
private Integer isPrivate;
private String writingDate;
private String content;
private String deletePostFl;


//post_view 테이블
private Integer viewNumber;

//post_like 테이블
private Integer likeNumber;

//post_password 테이블
private String postPassword;

private String userId;
}

현재 페이지에 보여질 게시글(PostsDTO)을 담은 List자료구조 를 구하기 위해서, 필요한 재료가 두 가지있다. 

일단, 현재 페이지에 보여질 게시글들을 POSTS 테이블에서 가져와야 하는데, 

POSTS 테이블의 행을 POSTS 테이블의 PK 인 id 를 기준으로 내림차순(게시물을 최신순으로 조회해야 하니까, 오름차순이 아니라, 내림차순으로 정렬했다.)으로 쭉 정렬한다음에, OFFSET FETCH 구문을 이용할 것이다. 

일단 쿼리부터 보여주면, 아래와 같다. 

더보기

<!--최신순 조회 -->
<select id="findCurrentPagePosts" resultType="PostsDTO">
SELECT posts.id, posts.member_id, posts.title, posts.is_private, TO_CHAR(posts.writing_date, 'YYYY-MM-DD') writing_date, posts.content, posts.delete_post_fl, <!-- 보라색 부분은 모두 post 테이블에 관한 컬럼이다. 중간에 sysdate 로 넣어둔 걸 TO_CHAR 로 String 타입으로 바꾸기 위해서 posts.* 이라고 못쓰고 이렇게 하나하나 꺼내온 것 뿐이다. -->

post_view.view_number, member.user_id
FROM posts

LEFT OUTER JOIN post_view ON posts.id = post_view.post_id
INNER JOIN member ON posts.member_id = member.id
WHERE posts.DELETE_POST_FL = 'N'
ORDER BY posts.id DESC
OFFSET #{startRow} ROWS
FETCH NEXT #{pageSize} ROWS ONLY
</select>

조인하는 부분에 대한 정리는 일단 미루고, 보라색 부분과 초록색 부분 만 보자. 

POSTS 테이블의 delete_post_fl 의 값이 'N' 인 행들을 쭉 조회해서 id 컬럼값을 기준으로 내림차순 정렬을 한다. 이게 보라색 부분이다.

이렇게 정렬되어 조회된 부분을 "Result Set"이라고 부르겠다. 

초록색 부분까지 합치면 어떻게 되냐면, 그렇게 "Result Set" 중, startRow 번째 행부터 pageSize 만큼의 행을 조회해라 라는 뜻이 된다.

여기서 주의해야 할건, OFFSET 20 ROWS 라고 하면, "Result Set" 중 21번째 행부터 가져오기 시작한다는 것이다.

 

그리고, 조인한 나머지 검정색 부분들은, 그 조회수를 나타내기 위한 post_view 테이블의 view_number 와, 누가 작성했는지 나타내기 위한 member 테이블의 user_id 컬럼값이다.

그리고, 조인할 때에는 member 테이블과는 굳이 LEFT OUTER JOIN 을 안해줘도 되서(왜냐면, 회원이 아닌 사람은 글을 작성할 수 없도록 인터셉터로 해둘것이기때문) INNER JOIN 으로 그냥 해뒀고, POST_VIEW 테이블과는 LEFT OUTER JOIN 을 반드시 해야되서 LEFT OUTER JOIN 으로 해두었다. 왜?

왜냐면, POSTS 테이블을 삽입할 당시(작성자가 게시글을 작성해서 제출할 당시) POSTS 테이블만(비밀번호를 작성한 경우 POST_PASSWORD 테이블까지만) 삽입하도록 해두었기 때문에, 만약 INNER JOIN 으로 해둔다면, POSTS 테이블에 대응되는 POST_VIEW 테이블의 행(POSTS 테이블의 id 컬럼과 일치하는 post_id 컬럼값을 지닌 POST_VIEW 테이블의 행)이 없기 때문에, 그 행 자체가 OFFSET FETCH 의 대상이 되는 "Result Set" 에서 빠지게 된다.

따라서, POSTS 테이블에 대응되는 행이 POST_VIEW 테이블에 없더라도, 그 행이 OFFSET FETCH 의 대상이 되는 "Result Set"에 포함되도록 LEFT JOIN 을 해둔 것이다. 

 

그래서 지금 현재 페이지에 보여질 게시글(PostsDTO)을 담은 List자료구조 를 구하기 위해 필요한 두가지 재료는 뭐냐면, 

위 쿼리 중 보라색 쿼리부분을 통해 조회된 전체 행 중에 "몇번째부터(#{startRow})" 시작해서 "몇 개(#{pageSize})"를 가져올 것인가? 이거든. 

 

몇번째부터 시작해야 할까? 

앞서 말했듯, FETCH 20 ROWS 라고 하면, "Result Set" 중에 21번째 행부터 조회한다고 했기 때문에, 

#{startRow} 값으로는 (현재 페이지 - 1) * 페이지당 게시물 개수   가 오면 된다. ((현재 페이지 - 1) * 페이지당 게시물 개수) + 1 이 아니라는 것이다.

int startRow = (currentPage - 1) * pageSize;

코드 중 위 부분이다.

 

몇 개를 가져올 것인가?한 페이지당 보여질 게시물의 개수 만큼 가져오면 되겠지. 이미 구해놨잖아. 

 

Integer pageSize = 20; // 페이지당 보여질 post 의 수

 

그래서, List<PostsDTO> currentPagePosts = postsMapper.findCurrentPagePosts(startRow, pageSize);이렇게 findCurrentPagePosts 라는 PostsMapper 클래스 내 메서드를 호출하면서 startRow 와 pageSize 를 넘겨준 것이다. 

 

이렇게 해서

 

1. 현재 페이지에 보여질 게시글(PostsDTO)을 담은 List자료구조.

2. 현재 boardHome.jsp 파일에서 렌더링하고 있는 페이지가 "몇 페이지" 인지.

3. 총 페이지 수

4. 현재 페이지 그룹의 첫번째 페이지

5. 현재 페이지 그룹의 마지막 페이지

6. 몇개의 페이지를 하나의 그룹으로 묶었는지.

 

이렇게 model 에 담을 6가지를 모두 구하고 model 에 담아줬다.

근데, 아직 하나 남았다.

findAllPosts 메서드 코드 중

List<PostsDTO> currentPagePostsChanged = viewNumberChangeFromNullToZeroService.viewNumberChange(currentPagePosts);

 

이 부분인데, 이게 뭐냐면, 현재 POSTS 테이블에 행을 삽입할 때에, 조회가 한번도 안되는 경우, 굳이 POST_VIEW 테이블에 행을 추가할 필요가 없다고 판단하였기 때문에(실제로 조회되는 순간에 POST_VIEW 테이블에 행을 넣어야 데이터베이스 공간을 낭비하지 않는다고 판단했다.), 조회가 한번도 되지 않은 경우에는 null 이다. 그래서 이 경우, 게시판 홈 중 조회수 부분에 아무것도 표시되지 않게 되는데, 이를 방지하고자 currentPagePosts 라는 PostsDTO 객체를 담은 List자료구조 안에 있는 PostsDTO 객체들 중 viewNumber 필드가 null 인 놈들의 viewNumber 를 0으로 바꿔주는 역할을 하는 서비스 메서드이다. 아래와 같이 생겼다. 

@Service
@Slf4j
public class ViewNumberChangeFromNullToZeroService {

    public List<PostsDTO> viewNumberChange(List<PostsDTO> currentPagePosts){

        for (PostsDTO post : currentPagePosts) {
            if (post.getViewNumber() == null) {
                post.setViewNumber(0);
            }
        }
        return currentPagePosts;

    }
}

 

이제 pagination 메서드에서 model 에 방금 구한 6가지를 담아줬으니, boardHome.jsp 로 갈 것이다. 

다음 포스팅으로

 

 

----------------------------------------------------------------------------------------------------------------------

더보기

한번 더 강조하고 싶은 건, POSTS 테이블에 행을 삽입할 때, 아래에서 보듯이 writing_date 라는 컬럼에 SYSDATE 를 넣어줬었거든? 근데, 그냥 이걸 그대로 꺼내오면, Sun Mar 24 10:30:55 YAKT 2024 이런식의 데이터란 말이야. 그래서, TO_CHAR() 를 이용해서 원하는 형식의 문자열로 가져와야 하고, 가져와서 그 조회된 행을 담는 DTO 객체에서 TO_CHAR() 변환한 그 값을 담는 필드의 타입은 String 이어야 한다는 거야. 

그리고, 가져올 때 안가져와진다면, TO_CHAR() 를 호출하면서 별칭을 writing_date 이런식으로 컬럼의 본래 이름을 적어줘야 하는데, 안적어주진 않았나 생각해봐.