본문 바로가기
Back-End/Spring

[spring boot] 간단한 방명록 만들기 [2편]

by LeeGangEun 2022. 6. 16.

DTO > Entity 변환

서비스 계층에서는 파라미터를 DTO 타입으로 받기 때문에
이를 JPA로 처리하기 위해서는 entity 타입의 객체로 변환해야 한다.

GeustbookService

   public interface GuestbookService {
    //글등록
    Long register(GuestbookDTO dto);
   
   //DTO를 Entity로 변환
    default Guestbook dtoToEntity(GuestbookDTO dto){
        Guestbook entity = Guestbook.builder()
                .gno(dto.getGno())       // java 8버전부터는 인터페이스의 실제 내용을 갖는
                .title(dto.getTitle())   // 코드를 default라는 키워드로 생성할 수 있다.
                .content(dto.getContent())  // 이를 이용하면 기존에 추상 클래스를 통해서 전달하던
                .writer(dto.getWriter())  // 실제 코드를 인터페이스에 선언할 수 있다.
                .build();  // 이를 통해서 '인터페이스 -> 추상클래스 -> 구현클래스'의  형태로
        return entity;    // 구현되던 방식에서 추상 클래스를 생략하는 것이 가능해진다.
    }
}

GeustbookServiceImpl

@Service
@Log4j2
// 상속받는것은 끝에 impl를 붙이는게 관례, 서비스는 끝에 service를 붙이는게 관례이다.
public class GuestbookServiceImpl implements GuestbookService{
   @Override
    public Long register(GuestbookDTO dto){
        log.info("DTO------------------");
        log.info(dto);
        Guestbook entity = dtoToEntity(dto);
        log.info(entity);
        return null;
    }

GeustbookServiceTests

@SpringBootTest
public class GuestbookServiceTests {

    @Autowired
    private GuestbookService service;

    @Test
    public void testRegister(){
        GuestbookDTO guestbookDTO = GuestbookDTO.builder()
                .title("sample title..")
                .content("sample content ...")
                .writer("user0")
                .build();
        System.out.println(service.register(guestbookDTO));
    }

테스트 코드 작성 결과 testRegister()는 실제로 DB에 저장되지는 않지만
GuestbookDTO를 Guestbook엔티티로 변환한 결과를 확인할 수 있다.

이제 변환 작업에 문제가 없다는 것을 확인했다 ! 
GuestbookServiceImpl 클래스를 수정해서 실제로 DB에 처리가 완료되도록 해보자.

GeustbookServiceImpl

@Service
@Log4j2
@RequiredArgsConstructor // 의존성 자동 주입
public class GuestbookServiceImpl implements GuestbookService{

    private final GuestbookRepository repository; // 반드시 final로 선언

    @Override
    public Long register(GuestbookDTO dto){
        log.info("DTO------------------");
        log.info(dto);

        Guestbook entity = dtoToEntity(dto);

        log.info(entity);
        repository.save(entity);
        return entity.getGno();
    }

JPA 처리를 위해 Repository를 주입하고, 클래스 선언 시에 
@RequiredArgsConstructor를 이용해서 자동으로 주입한다.
register()의 내부에서는 save()를 통해 저장하고,
저장된 후에 해당 entity가 가지는 gno값을 반환한다. 

테스트 결과 위와 같이 DB에 정상적으로 추가된 것을 확인할 수 있다.

목록 처리

1. 목록 처리하는 작업은 다음과 같은 상황을 고려해야 한다.
 => 화면에서 필요한 목록 데이터에 대한 DTO 생성
 => DTO를 Pageable 타입으로 전환
 => Page<Entity>를 화면에서 사용하기 쉬운 DTO의 리스트 등으로 변환
 => 화면에 필요한 페이지 번호 처리

2. 페이지 요청처리 DTO(PageRequestDTO)
 => 작성하려고 하는 PageRequestDTO는 목록 페이지를 요청할 때 사용하는 데이터를 
재사용하기 쉽게 만드는 클래스이다. 목록 화면에서는 페이지 처리를 하는 경우가 많이 있기 때문에
'페이지 번호', '페이지 내 목록의 개수', '검색 조건'들이 많이 사용한다. 
PageRequestDTO는 이러한 파라미터를 DTO로 선언하고 나중에 재사용하는 용도로 사용한다.
화면에 전달되는 목록 관련된 데이터에 대한 DTO PageRequestDTO
화면에 필요하는 결과PageResultDTO라는 이름의 클래스로 생성한다.

 

PageRequestDTO

@Builder
@AllArgsConstructor
@Data
public class PageRequestDTO {
    private int page;
    private int size;
    private String type;
    private String keyword;

    public PageRequestDTO(){
        this.page = 1;
        this.size = 10;
    }

    public Pageable getPageable(Sort sort){
        return PageRequest.of(page -1 , size, sort);
    }
}

PageResultDTO

@Data
// Generic Class 생성. DTO, EN은 PageResultDTO의 인스턴스를 생성할 때 지정.
public class PageResultDTO<DTO,EN> {
    private List<DTO> dtoList; //글목록을 저장하는 List
    private int totalPage; // 총페이지수
    private int page; //현재페이지
    private int size; //페이지당 보여지는 글수
    private int start; // 1......10 에서 시작번호
    private int end; // 1........10 에서 끝번호
    private boolean prev; //이전블록 유무 여부
    private boolean next; //다음블록 유무 여부
    private List<Integer> pageList; // 1......10 번호 목록
    //생성자
    public PageResultDTO(Page<EN> result, Function<EN,DTO> fn){
        //목록생성. dtoList에 저장
        //map()함수에서 사용할 함수를 fn으로 지정
        //map함수 사용법
        //배열or컬렉션or페이지.stream().map(화살표함수).collect(Collectors.toList())
        dtoList=result.stream().map(fn).collect(Collectors.toList());
        //총페이지수
        totalPage=result.getTotalPages();
        //paging에 관련된 필드들의 값을 구해서 저장
        makePageList(result.getPageable());
    }
    //paging에 관련된 필드들의 값을 구해서 저장
    private void makePageList(Pageable pageable){
        this.page=pageable.getPageNumber()+1;//페이지번호
        this.size=pageable.getPageSize(); //페이지당 글수
        System.out.println("페이지당 글수 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ "+size);
        int tempEnd=(int)(Math.ceil(page/10.0))*10; // 1.......10에서 10. 총페이지수가 현재구간의 tempEnd보다 작으면 총페이지수가 end가 됨
        this.start=tempEnd-9; // 끝번호(10)에서 9를 빼면 시작번호(1)
        this.prev=start > 1; // 시작번호가 1보다 크면 최소 두번째 구간이므로 이전구간 존재
        this.end=totalPage>tempEnd?tempEnd:totalPage; //총페이지수가 현재구간의 tempEnd보다 작으면 총페이지수가 end가 됨
        this.next=totalPage>tempEnd; //총페이지수가 현재구간의 tempEnd보다 크면 다음 구간 존재
        this.pageList= IntStream.rangeClosed(start,end).boxed().collect(Collectors.toList());// 1....10까지의 수를 List에 저장
    }
}

페이지 결과 처리 DTO(PageResultDTO)
 => JPA를 이용하는 Repository에서는 페이지 처리 결과Page<Entity> 타입으로 반환하게 된다.
따라서 서비스 계층에서 이를 처리하기 위해서도 별도의 클래스를 만들어서 처리해야 한다.
처리하는 클래스는 크게 다음과 같은 내용이다.

  • Page<Entity>엔티티 객체들을 DTO 객체로 변환해서 자료구조로 담아 줘야 한다.
  • 화면 출력에 필요한 페이지 정보들을 구성해 줘야 한다.

목록 데이터 페이지 처리
=> 화면까지 전돨되는 데이터PageResultDTO이고, 이를 이용해서 화면에서는 페이지 처리를 진행하게 될 것이다.
PageResultDTO 타입으로 처리된 결과에는 시작 페이지, 끝 페이지 등 필요한 모든 정보를 담아서 화면에서는
필요한 내용들만 찾아서 구성이 가능하도록 작성한다. 필요한 구성은 다음과 같다.

  • 화면에서 시작 페이지 번호(start)
  • 화면에서 끝 페이지 번호(end)
  • 이전 / 다음 이동 링크 여부(prev, next)
  • 현재 페이지 번호(page)

페이지 처리를 하기 위해 가장 필요한 정보는 현재 사용자가 보고 있는 페이지(page)의 정보이다.
사용자가 5페이지를 본다면 화면의 페이지 번호는 1부터 시작하지만
사용자가 19페이지를 본다면 11부터 시작해야 하기 때문이다.
페이징 처리를 할땐 끝 번호를 먼저 계산해 두는 것이 수월하다.

  • 끝 번호 공식 : tempEnd = (int)(Math.ceil(페이지번호 / 10.0)) * 10; 
    => Math.ceil()은 소수점을 올림으로 처리하기 때문에 다음과 같은 상황이 가능하다.
    1 페이지의 경우 : Math.ceil(0,1) * 10 = 10
    10 페이지의 경우 : Math.ceil(1) * 10 = 10
    11페이지의 경우 : Math.ceil(1,1) * 10 = 20
  • 시작 번호 공식 : start = tempEnd - 9; 
    => 만일 화면에 10개씩 보여준다면 시작 번호(start)는 무조건 임시로 만든 끝 번호에서 9를 뺀 값이 된다.
  • 끝 번호 와 실제 마지막 페이지 비교 -
    => totalPage = result.getTotalPages(); // result는 Page<Guestbook>
    => end = totalPage > tempEnd ? tempEnd: totalPage;
  • 이전 페이지 공식 : prev = start > 1;
    => 이전의 경우는 시작번호(start)가 1보다 큰 경우라면 존재하게 된다.
  • 다음 페이지 공식 : next = totalPage > tempEnd;
    => 다음(next)은 위에 realEnd가 끝 번호(end)보다 큰 경우에만 존재하게 된다.

GeustbookService

    // Entity를 DTO로 변환
    default GuestbookDTO entityToDto(Guestbook entity){
        GuestbookDTO dto = GuestbookDTO.builder()
                .gno(entity.getGno())
                .title(entity.getTitle())
                .content(entity.getContent())
                .writer(entity.getWriter())
                .regDate(entity.getRegDate())
                .modDate(entity.getModDate())
                .build();
        return dto;
    }

 

GeustbookServiceImpl

 @Override
    public PageResultDTO<GuestbookDTO, Guestbook> getList(PageRequestDTO requestDTO) {
        Pageable pageable = (Pageable) requestDTO.getPageable(Sort.by("gno").descending());

        BooleanBuilder booleanBuilder = getSearch(requestDTO); // 검색 조건 처리

        Page<Guestbook> result = repository.findAll(booleanBuilder, pageable); // seelct문 실행 Querydsl 사용
        //PageResultDTO 생성자의 map() 함수에서 사용할 함수 생성
        //Function<Guestbook, GuestbookDTO)에서 Guestbook은 화살표함수의 파라미터의 타입, GuestbookDTO는 리턴값의 타입
        //fn은 화살표 함수를 가리키는 레퍼런스이다.
        Function<Guestbook, GuestbookDTO> fn = (entity -> entityToDto(entity));
        return new PageResultDTO<>(result,fn);
    }

GeustbookServiceTests

    @Test
    public void testList(){
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .page(1)
                .size(10)
                .build();
        PageResultDTO<GuestbookDTO, Guestbook> resultDTO = service.getList(pageRequestDTO);

            System.out.println("PREV: "+resultDTO.isPrev());
            System.out.println("NEXT: " +resultDTO.isNext() );
            System.out.println("TOTAL: " + resultDTO.getTotalPage());
            System.out.println("=========================================");
            
            for(GuestbookDTO guestbookDTO : resultDTO.getDtoList()){
                System.out.println(guestbookDTO);
            }
            System.out.println("=========================================");
            resultDTO.getPageList().forEach(i-> System.out.println(i));
    }
}

위와 같이 코드를 추가 후 테스트 결과 화면이다 !

GeustbookController

Controller
@RequestMapping("/guestbook")
@Log4j2
@RequiredArgsConstructor
public class GuestbookController {
    // Service객체
    private final GuestbookService service; // 자동 주입

    // 목록
    @GetMapping("/")
    public String index(){
        return "redirect:/guestbook/list";
    }
    @GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model){
        log.info("list........"+ pageRequestDTO);
        // 글목록을 출력
        model.addAttribute("result", service.getList(pageRequestDTO));
    }

list.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<th:block th:replace="~{/layout/basic :: setContent(~{this::content})}">
    <th:block th:fragment="content">
        <h1>GuestBook List Page</h1>


        <form action="/guestbook/list" method="get" id="searchForm">
                <div class="input-group">
                    <input type="hidden" name="page" th:value="1">
                    <div class="input-group-prepend">
                        <select class="custom-select" name="type">
                            <option th:selected="${pageRequestDTO.type == null}">-----</option>
                            <option th:value="t" th:selected="${pageRequestDTO.type =='t'}">제목</option>
                            <option th:value="c" th:selected="${pageRequestDTO.type =='c'}">내용</option>
                            <option th:value="w" th:selected="${pageRequestDTO.type =='w'}">작성자</option>
                            <option th:value="tc" th:selected="${pageRequestDTO.type =='tc'}">제목 + 내용</option>
                            <option th:value="tcw" th:selected="${pageRequestDTO.type =='tcw'}">제목 + 내용 + 작성자</option>
                        </select>
                    </div>
                    <input class="form-control" name="keyword" th:value="${pageRequestDTO.keyword}">
                    <div class="input-group-append" id="button-addon4">
                        <button class="btn btn-outline-secondary btn-search" type="button">Search</button>
                        <button class="btn btn-outline-secondary btn-clear" type="button">Clear</button>
                    </div>
                </div>
        </form >


            <span>
                <a th:href="@{/guestbook/register}" class="btn btn-outline-primary">글쓰기</a>
            </span>
        <table class="table table-striped">
            <thead>
                <tr>
                    <th scope="col">Gno</th>
                    <th scope="col">Tile</th>
                    <th scope="col">Writer</th>
                    <th scope="col">Regdate</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="dto : ${result.dtoList}">
                    <th scope="row">
                        <a th:href="@{/guestbook/read(gno=${dto.gno},page=${result.page},
                        type=${pageRequestDTO.type}, keyword = ${pageRequestDTO.keyword})}">
                            [[${dto.gno}]]
                        </a>
                    </th>
                    <td>
                        <a th:href="@{/guestbook/read(gno=${dto.gno},page=${result.page})}">
                            [[${dto.title}]]
                        </a>
                    </td>
                    <td>[[${dto.writer}]]</td>
                    <td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
                </tr>

            </tbody>
        </table>

        <!--------------------------------- paging 번호 출력 ----------------------------------->
        <ul class="pagination h-100 justify-content-center align-items-center" >
            <li class="page-item" th:if="${result.prev}">
                <a class="page-link" th:href="@{/guestbook/list(page = ${result.start -1},
                type=${pageRequestDTO.type}, keyword = ${pageRequestDTO.keyword}
                )}" tabindex="-1">이전</a>
            </li>

            <li th:class="'page-item ' + ${result.page == page?'active':''}" th:each="page:${result.pageList}">
                <a class="page-link" th:href="@{/guestbook/list(page = ${page},
                type=${pageRequestDTO.type}, keyword = ${pageRequestDTO.keyword}
                )}">
                    [[${page}]]
                </a>
            </li>

            <li class="page-item" th:if="${result.next}">
                <a class="page-link" th:href="@{/guestbook/list(page = ${result.end +1},
                type=${pageRequestDTO.type}, keyword = ${pageRequestDTO.keyword}
                )}">다음</a>
            </li>

        </ul>
        <!--------------------------------- paging 번호 출력 끝----------------------------------->


페이징을 이용한 목록 처리까지 완료되었다.