Coding 공부/IntelliJ

[IntelliJ_Springboot_MariaDB] PageRequest DTO, PageResponseDTO<E>, BoardService, BoardServiceImpl에 list()메서드, Test 코드, BoardController, layout/basic.html, board/list.html, 부트스트랩, 가변인자, RequestMapping, 자동 매개변수 바인딩

CBJH 2024. 5. 16.
728x90
반응형

1. HTML에서 주고 받는 객체 DTO 코드

1.1 PageRequest DTO

@Builder // Lombok의 빌더 패턴을 이용해서 객체를 생성할 수 있도록 해주는 어노테이션
@AllArgsConstructor // Lombok을 사용해 모든 필드 값을 파라미터로 받는 생성자를 자동으로 생성
@NoArgsConstructor // Lombok을 사용해 파라미터가 없는 기본 생성자를 자동으로 생성
@Data // Lombok을 사용해 getter, setter, toString, equals, hashCode 메서드를 자동으로 생성
public class PageRequestDTO { // HTML에서 검색 기능을 사용할 때 전달 받는 데이터 전송 객체(DTO)
    @Builder.Default
    private int page = 1; // 기본값으로 첫 페이지는 1로 설정
    @Builder.Default
    private int size = 10; // 기본값으로 페이지당 보여줄 게시글 수는 10으로 설정
    private String type; // 검색 타입 (예: t:title, c:contents, w:writer, tc:title+contents 등의 조합)
    private String keyword; // 검색 키워드
    private String link; // URL 링크 생성을 위한 필드

    public String[] getTypes() {
        if (type == null || type.isEmpty()) {
            return null; // 타입이 비어있으면 null 반환
        } else {
            return type.split(""); // 타입을 문자열 배열로 분리하여 반환
        }
    }

    public Pageable getPageable(String... props) {
        // 페이지 요청 정보를 생성, 페이지 번호는 0부터 시작하므로 1을 빼줌
        return PageRequest.of(this.page - 1, this.size, Sort.by(props).descending());
    }

    public String getLink() {
        if (link == null) {
            StringBuilder builder = new StringBuilder();
            builder.append("page=" + this.page); // URL에 페이지 정보 추가
            builder.append("&size=" + this.size); // URL에 페이지 사이즈 정보 추가
            if (type != null && type.length() > 0) {
                builder.append("&type=" + type); // URL에 검색 타입 정보 추가
            }
            if (keyword != null) {
                try {
                    builder.append("&keyword=" + URLEncoder.encode(keyword, "UTF-8")); // URL에 검색 키워드 정보 추가 (URL 인코딩)
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace(); // 인코딩 실패 시 에러 출력
                }
            }
            link = builder.toString(); // StringBuilder로 생성된 문자열을 link 필드에 저장
        }
        return link; // 완성된 링크 반환
    }
}
  • getPageable(String... props){}에서 String... props는 가변인자이다.
  • 가변인자는 해당 타입에 대해서 여러가지 값이 들어갈 수 있다. 자세한건 뒤에서 설명한다.
  • PageRequest 객체로 HTML에서 자료를 가져오게되면 PageResponse에서 화면에 보여주기 위해 자료 가공이 필요하다. 그 자료 가공을 위해 String[] getTypes(), Pageable getPageable(String... props), String getLink()를 클래스 메소드로 포함한다.
  • String[] getTypes() 메소드는 private String keyword 값을 BoardSearchImpl에서 SearchAll을 사용할 때 필요한 메서드이다. String값을 각각 한글자씩 나누어 String[] 타입으로 변환한다.
  • Pageable getPageable(String... props)  메서드는  매개변수 속성에 따라 내림차순으로 정렬한 Page<Board> 값을 반환받을 때 사용한다.(PageResponseDTO 클래스에서 사용)
  • String getLink() 메서드는 오늘은 예제코드에선 사용하지 않았지만, 쿼리 형식으로 데이터를 저장하고 검색한 Page 정보을 읽어오기 위해 필요하다.

 

1.2 PageResponseDTO<E>

@Getter // Lombok을 사용하여 모든 필드에 대한 getter 메소드를 자동으로 생성
@ToString // Lombok을 사용하여 toString() 메소드를 자동으로 생성
public class PageResponseDTO<E> {
    private int page; // 현재 페이지 번호
    private int size; // 페이지 당 표시할 항목 수
    private int total; // 전체 데이터 수
    
    private int start; // 화면에 표시될 시작 페이지 번호
    private int end; // 화면에 표시될 끝 페이지 번호
    
    private boolean prev; // 이전 페이지 그룹의 존재 여부를 나타내는 플래그
    private boolean next; // 다음 페이지 그룹의 존재 여부를 나타내는 플래그
    private List<E> dtoList; // 현재 페이지에 표시할 데이터 목록, 제네릭을 사용하여 유연하게 데이터 타입 지정

    // 생성자
    public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total){
        if(total<=0){
            return; // 전체 데이터 수가 0 이하이면 초기화를 중단
        }
        this.page = pageRequestDTO.getPage(); // 요청된 페이지 번호 초기화
        this.size = pageRequestDTO.getSize(); // 페이지 당 데이터 수 초기화

        this.end = (int)Math.ceil(this.page / 10.0) * 10; // 현재 페이지 기준으로 끝 페이지 번호 계산
        this.start = this.end - 9; // 시작 페이지 번호 계산

        int last = (int)(Math.ceil(total / (double)size)); // 전체 데이터를 기준으로 계산된 마지막 페이지 번호
        this.end = Math.min(end, last); // 계산된 끝 페이지 번호와 마지막 페이지 중 작은 값으로 끝 페이지 번호 설정

        this.prev = this.start > 1; // 시작 페이지 번호가 1보다 크면 이전 페이지 그룹 존재
        this.next = this.end * size < total; // 계산된 끝 페이지의 데이터 수가 전체 데이터 수보다 작으면 다음 페이지 그룹 존재
    }
}
  • Setter나 Builder 없이 생성자를 통해 PageResponse<E>의 객체 인스턴스를 생성한다.
  • PageRequestDTO, List<E> dtoList : MemberDTO리스트 또는 BoardDTO리스트, Page<Board>의 getTotalElement()메서드로 반환 받는 total 값을 인자로 받아 생성한다.
  • HTML에서 PageRequestDTO로 받은 값을 토대로, SpringBoot에서 DB에 접근해 얻은 자료를 가공해서 PageResponseDTO로 다시 HTML에 보내는 객체이다.

 

2. BoardService, BoardServiceImpl에 list()메서드 추가

public class BoardServiceImpl implements BoardService{
    private final ModelMapper modelMapper;
    private final BoardRepository boardRepository;
    
    @Override
    public PageResponseDTO<BoardDTO> list(PageRequestDTO pageRequestDTO){
       // PageRequestDTO에서 검색 타입과 키워드를 가져온다.
       String[] types = pageRequestDTO.getTypes();
       String keyword = pageRequestDTO.getKeyword();
    
       // PageRequestDTO를 이용하여 Pageable 객체를 생성한다. 여기서는 "bno" 속성에 따라 내림차순 정렬을 적용한다.
       Pageable pageable = pageRequestDTO.getPageable("bno");

       // boardRepository의 searchAll 메서드를 호출하여 필요한 파라미터와 함께 Page<Board> 객체를 반환받는다.
       // 이 메서드는 데이터베이스에서 필터링과 페이징이 적용된 결과를 조회한다.
       Page<Board> result = boardRepository.searchAll(types, keyword, pageable);

       // 조회된 Board 객체 리스트를 BoardDTO 리스트로 변환한다.
       List<BoardDTO> dtoList = result.getContent().stream()
            .map(board -> modelMapper.map(board, BoardDTO.class)) // 각 Board 객체를 BoardDTO로 매핑
            .collect(Collectors.toList()); // 스트림 결과를 List로 수집

       // PageResponseDTO의 빌더를 사용하여 응답 DTO를 생성한다.
       return PageResponseDTO.<BoardDTO>withAll() // 제네릭 타입 <BoardDTO> 지정
            .pageRequestDTO(pageRequestDTO) // PageRequestDTO 객체 설정
            .dtoList(dtoList) // 변환된 DTO 리스트 설정
            .total((int)result.getTotalElements()) // 전체 요소 수 설정
            .build(); // DTO 객체 생성 완료
	}
}
  • PageRequestDTO를 매개변수로 받아 BoardRepository로 DB에 접근해 Board 정보를 Page<Board> 컬렉션에 담고 List<BoardDTO>로 매핑하여 PageResponseDTO를 return하는 서비스의 list함수 코드이다.

 

3. Test 코드

@SpringBootTest
@Log4j2
public class BoardServiceTests{
    @Autowired
    private BoardService boardService;


    @Test
    public void testList() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .type("tcw")
                .keyword("3")
                .page(1)
                .size(10)
                .build();
        PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
        for(BoardDTO element : responseDTO.getDtoList()){
            log.info(element.toString());
        }
    }
}
  • 테스트를 위해 HTML에서 PageRequestDTO를 받았다고 가정하고, Builder로 생성한 후 BoardService의 list함수를 실행해 콘솔 로그창에 출력한다.

 

4.  HTML과 컨트롤러로 연결

4.1 BoardController

@Controller
@RequestMapping("/board")
@Log4j2
@RequiredArgsConstructor
public class BoardController {
    private final BoardService boardService;

    @GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model) {
        PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
        log.info(responseDTO);
        model.addAttribute("responseDTO", responseDTO);
    }
}
  • @RequestMapping 어노테이션은 해당 컨트롤러의 root 페이지를 설정한다. 밑에 자세히 설명한다.
  • /board/list 링크를 열어 HTTP 접근을 확인하면 GetMapping하여 list()함수를 실행한다. (board/list.html 파일을 화면에 보여준다)
  • PageRequestDTO를 받아 responseDTO로 model.addAttribute해서 객체를 전달한다. (JSON 타입이 아니므로 @Controller 어노테이션을 사용한다)

 

4.2 layout/basic.html

<!DOCTYPE html>
<html xmlns:layout="http://www.ultaq.net.nz/thymeleaf/layout"
        xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="" />
    <meta name="author" content="" />
    <title>Simple Sidebar - Start Bootstrap Template</title>
    <!-- Favicon-->
    <link rel="icon" type="image/x-icon" th:href="@{/assets/favicon.ico}" />
    <!-- Core theme CSS (includes Bootstrap)-->
    <link th:href="@{/css/styles.css}" rel="stylesheet" />
</head>
<body>
<div class="d-flex" id="wrapper">
    <!-- Sidebar-->
    <div class="border-end bg-white" id="sidebar-wrapper">
        <div class="sidebar-heading border-bottom bg-light">Start Bootstrap</div>
        <div class="list-group list-group-flush">
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Dashboard</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Shortcuts</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Overview</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Events</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Profile</a>
            <a class="list-group-item list-group-item-action list-group-item-light p-3" href="#!">Status</a>
        </div>
    </div>
    <!-- Page content wrapper-->
    <div id="page-content-wrapper">
        <!-- Top navigation-->
        <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
            <div class="container-fluid">
                <button class="btn btn-primary" id="sidebarToggle">Toggle Menu</button>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button>
                <div class="collapse navbar-collapse" id="navbarSupportedContent">
                    <ul class="navbar-nav ms-auto mt-2 mt-lg-0">
                        <li class="nav-item active"><a class="nav-link" href="#!">Home</a></li>
                        <li class="nav-item"><a class="nav-link" href="#!">Link</a></li>
                        <li class="nav-item dropdown">
                            <a class="nav-link dropdown-toggle" id="navbarDropdown" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
                            <div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
                                <a class="dropdown-item" href="#!">Action</a>
                                <a class="dropdown-item" href="#!">Another action</a>
                                <div class="dropdown-divider"></div>
                                <a class="dropdown-item" href="#!">Something else here</a>
                            </div>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
        <!-- Page content-->
        <div class="container-fluid" layout:fragment="content"> <!--레이아웃 영역의 이름 : content-->
            <h1 class="mt-4">Simple Sidebar</h1>
            <p>The starting state of the menu will appear collapsed on smaller screens, and will appear non-collapsed on larger screens. When toggled using the button below, the menu will change.</p>
            <p>
                Make sure to keep all page content within the
                <code>#page-content-wrapper</code>
                . The top navbar is optional, and just for demonstration. Just create an element with the
                <code>#sidebarToggle</code>
                ID which will toggle the menu when clicked.
            </p>
        </div>
    </div>
</div>
<!-- Bootstrap core JS-->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Core theme JS-->
<script th:src="@{/js/scripts.js}"></script>
<th:block layout:fragment="scrpit"><!--스크립트 추가시 이 부분에 넣어진다.-->

</th:block>
</body>
</html>
  • th:href="@{/assets/favicon.ico}"는 프로젝트 내의 resources/static/assets 디렉터리 아래에 있는 favicon.ico 파일을 웹 페이지의 파비콘(즐겨찾기 아이콘)으로 지정하는 것
  • 부트 스트랩에서 제공하는 코드이다. 부트 스트랩에 대해선 밑에서 자세히 설명한다.

 

4.3 board/list.html

<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:th="http://www.thymeleaf.org"
      layout:decorate="~{layout/basic.html}">

<div layout:fragment="content">
    <div class="row mt-3">
        <div class="col">
            <div class="card">
                <div class="card-header">
                    Board List
                </div>
                <div class="card-body">
                    <h5 class="card-title">Board List</h5>
                    <table class="table">
                        <thead>
                        <tr>
                            <th scope="col">Bno</th>
                            <th scope="col">Title</th>
                            <th scope="col">Writer</th>
                            <th scope="col">RegDate</th>
                        </tr>
                        </thead>
                        <tbody>
                        <tr th:each="dto:${responseDTO.dtoList}">
                            <th scope="row">[[${dto.bno}]]</th>
                            <td>[[${dto.title}]]</td>
                            <td>[[${dto.writer}]]</td>
                            <td>[[${dto.regDate}]]</td>
                        </tr>
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</div>

<script layout:fragment="script" th:inline="javascript">
    console.log("script......")
</script>

</html>
  • HTML 템플릿에서 PageRequestDTO를 명시적으로 보내는 부분이 없음에도, 컨트롤러에서 PageRequestDTO를 파라미터로 받아 화면에 출력해준다. 해당 자세한 내용은 밑에 설명한다.
  • 결과적으론 전달받은 PageRequestDTO값이 없어 디폴트 값이 전달받아 모든 자료 중에서 제일 최근 10개의 자료를 1페이지에 담아 PageResponseDTO로 보내준다.

화면 출력 결과

 

 

5. 더 알아가기

 

5.1 부트스트랩 

  • Bootstrap은 가장 인기 있는 프론트엔드 프레임워크 중 하나로, 반응형 웹 디자인을 손쉽게 구현할 수 있게 돕는 도구입니다. HTML, CSS, JavaScript로 구성되어 있으며, 웹 개발자가 빠르고 쉽게 아름답고 효율적인 웹 사이트나 애플리케이션을 만들 수 있도록 다양한 디자인 요소와 기능을 제공합니다.
  • 링크 : https://startbootstrap.com/template/simple-sidebar#google_vignette

  • Free Download 클릭해 알집 파일을 받는다.
  • 알집을 풀고 intelliJ프로젝트 소스 파일 src/main/resources/static 경로에 압축을 풀어 나온 assets, css, js 폴더를 넣어주면 된다.

5.2 가변인자

  • String... props는 Java에서 가변 인자(varargs)를 나타냅니다. 가변 인자는 메서드에 여러 개의 인자를 같은 타입으로 전달할 수 있게 해주며, 호출하는 쪽에서 인자의 개수를 유연하게 조절할 수 있습니다. 내부적으로는 배열로 처리됩니다.
  • String... props는 메서드에 0개 이상의 문자열을 인자로 전달할 수 있음을 의미합니다. 호출 시 getPageable("name", "date")처럼 여러 문자열을 인자로 제공할 수 있으며, 이 문자열들은 Sort.by(props).descending()에 의해 내림차순 정렬 기준으로 사용됩니다.
  • 예를 들어, props에 "name", "date"가 전달되면, 이 메서드는 name과 date 필드를 기준으로 내림차순 정렬 기준을 설정합니다. 이 정렬 기준은 PageRequest.of 메서드를 통해 생성되는 Pageable 객체에 적용되어, 데이터를 페이지로 나누어 요청할 때 해당 정렬 순서를 따르게 됩니다. 이렇게 가변 인자를 사용함으로써, 메서드 호출 시 정렬 기준으로 삼을 필드를 유연하게 지정할 수 있습니다.

 

5.3 RequestMapping

  • @RequestMapping("/board") 어노테이션이 사용된 경우의 주요 특징과 기능은 다음과 같습니다:
  1. URL 경로 지정:
    • 이 어노테이션은 /board라는 URL 경로에 대한 요청을 처리하도록 설정합니다. 즉, 웹 브라우저나 클라이언트에서 http://yourdomain.com/board로 요청을 보내면, 해당 어노테이션을 포함하는 메소드가 이를 처리합니다.
  2. 클래스 레벨 vs. 메소드 레벨:
    • 클래스 레벨에 @RequestMapping("/board")를 선언하면, 해당 클래스 내의 모든 메소드가 기본적으로 /board 경로를 기반으로 요청을 받습니다. 예를 들어, 클래스 레벨에 @RequestMapping("/board")가 있고, 메소드 레벨에 @RequestMapping("/list")가 있다면, /board/list 경로에 대한 요청을 해당 메소드가 처리하게 됩니다.
    • 메소드 레벨에만 @RequestMapping("/board")가 선언된 경우, 그 메소드는 /board에 대한 요청만 처리합니다.
  3. HTTP 메소드 지정:
    • @RequestMapping은 method 속성을 통해 HTTP 요청 메소드를 지정할 수 있습니다. 예를 들어, @RequestMapping(value = "/board", method = RequestMethod.GET)는 GET 요청만을 /board 경로에서 처리하도록 설정합니다.
    • 지정하지 않을 경우, 모든 HTTP 메소드(GET, POST, PUT, DELETE 등)에 대해 해당 경로가 반응할 수 있습니다.
  4. 다중 경로 지정:
    • @RequestMapping은 하나 이상의 URL 경로를 지정할 수 있습니다. 예를 들어, @RequestMapping(value = {"/board", "/b"})는 /board와 /b 두 경로 모두에 대해 같은 메소드를 사용하여 요청을 처리합니다.
  5. 파라미터와 헤더 조건:
    • @RequestMapping은 params와 headers 속성을 이용하여 요청에 포함된 파라미터나 헤더에 따라 메소드 호출을 제한할 수 있습니다. 이는 특정 조건을 충족하는 요청에 대해서만 메소드가 반응하도록 할 수 있습니다.

 

5.4 HTML 템플릿에서 PageRequestDTO를 명시적으로 보내는 부분이 없음에도, 컨트롤러에서 PageRequestDTO를 파라미터로 어떻게 받을 수 있는지에 대한 건

 

  • Spring MVC에서 컨트롤러 메서드는 요청 매개변수(request parameters)를 자동으로 객체에 바인딩 할 수 있는 기능을 제공합니다. 이는 @ModelAttribute라는 어노테이션을 사용하여 묵시적으로 수행됩니다(명시적으로 선언하지 않아도 Spring이 내부적으로 처리). 여기서 PageRequestDTO가 이 역할을 합니다.

 

  • 자동 매개변수 바인딩
    • PageRequestDTO는 페이지 번호, 페이지 크기 등 페이징을 위한 정보를 포함할 수 있는 필드를 가지고 있습니다. 사용자가 웹 페이지를 통해 게시판 목록을 요청할 때, URL에 이러한 정보를 쿼리 스트링(query string)으로 포함시킬 수 있습니다. 예를 들어, 페이지 요청 URL이 다음과 같을 수 있습니다:
/board/list?page=2&size=10
  • 이 URL에서 page와 size는 PageRequestDTO 객체의 필드와 자동으로 매핑됩니다. 따라서 list 메서드가 호출될 때, Spring MVC는 이 요청 매개변수를 PageRequestDTO 객체에 자동으로 바인딩하고 이 객체를 메서드의 매개변수로 전달합니다.

 

  • Thymeleaf와의 연결
    • Thymeleaf 템플릿에서 별도로 PageRequestDTO를 보내는 코드가 보이지 않는 경우, 일반적으로 페이징 처리가 URL 매개변수를 통해 이루어집니다. 페이지 링크나 폼 제출을 통해 이 매개변수들이 URL에 포함되어 컨트롤러에 전달될 것입니다.
  • Thymeleaf 템플릿에서 페이징 처리를 위한 링크 예시는 다음과 같을 수 있습니다:
<a th:href="@{/board/list(page=${responseDTO.currentPage - 1}, size=${responseDTO.size})}">Previous</a>
<a th:href="@{/board/list(page=${responseDTO.currentPage + 1}, size=${responseDTO.size})}">Next</a>
  • 위 예시처럼, Thymeleaf 템플릿 내에서 페이지 링크를 생성할 때, page와 size 매개변수가 URL에 포함되어 PageRequestDTO로 자동 매핑되는 방식으로 작동합니다.

 

 

댓글