Spring/GuestBook

GuestBook #2 Querydsl 사용을 위한 설정부터 까지 처리 메서드선언까지

java나유 2022. 10. 29. 17:15

6.Querydsl 사용을 위한 설정

1)build.gradle 파일 수정

 

//QueryDSL 
-------맨위작성-----------------------------------------
buildscript {
	ext {
		queryDslVersion = "5.0.0"
	}
}
-----------------------------------------------
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
--------------------------------------------------------------------
	implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
	implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
------------------dependencies 다음에---------------------------------------
def querydslDir = "$buildDir/generated/querydsl"

querydsl {
	jpa = true
	querydslSourcesDir = querydslDir
}
sourceSets {
	main.java.srcDir querydslDir
}
configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
	querydsl.extendsFrom compileClasspath
}
compileQuerydsl {
	options.annotationProcessorPath = configurations.querydsl
}

tasks.named('test') {
	useJUnitPlatform()
}

2) Gradle-Tasks-build-jar 더블 클릭

 

3)GuestBookRepository 인터페이스의 선언 부분 수정

GuestBookRepository

public interface GuestBookRepository extends JpaRepository<GuestBook, Long>,
 QuerydslPredicateExecutor<GuestBook> {
}

 

4)Test 클래스에서 title에 1이 포함된 데이터를 조회 && 출력하는 메서드 생성하고 확인

//데이터 querydsl을 이용한 조회
    @Test
    public void testQuery1() {
        //gno의 내림차순 정렬 후 0페이지 10를 가져오기 위한 Pageable 객체 생성
        Pageable pageable = PageRequest.of(0, 10, Sort.by("gno")
                .descending()); //뒤에서 부터 gno에 1이 들어가는 10개
        //Entity에 동적 쿼리를 수행할 수 있는 도메인 클래스 찾아오기 Querydsl 설정을 해야 사용 가능
        //컬럼들을 속성으로 포함시켜 조건을 설정하는 것이 가능
       QGuestBook qGuestBook = QGuestBook.guestBook; //I
        //검색어 생성
        String keyword = "1";
        //검색을 적용하기 위한 Builder 객체 생성
        BooleanBuilder builder = new BooleanBuilder(); //2
        //조건 표현식 생성
        BooleanExpression expression =
                qGuestBook.title.contains(keyword); //3
        //검색 객체에 표현식 추가
        builder.and(expression); //4
        Page<GuestBook> result = guestBookRepository.findAll(
                builder, pageable); //5
        result.stream().forEach(guestbook -> {
            System.out.println(guestbook);
        });
    }

 

5)Test 클래스에 title이나 content에 1이 포함된 데이터를 조회해서 출력하는 메서드를 생성하고 확인

@Test
    public void testQuery2() {
        //gno의 내림차순 정렬 후 0페이지 10를 가져오기 위한 Pageable 객체 생성
        Pageable pageable = PageRequest.of(0, 10, Sort.by("gno")
                .descending()); //뒤에서 부터 gno에 1이 들어가는 10개
        //Entity에 동적 쿼리를 수행할 수 있는 도메인 클래스 찾아오기 Querydsl 설정을 해야 사용 가능
        //컬럼들을 속성으로 포함시켜 조건을 설정하는 것이 가능
        QGuestBook qGuestBook = QGuestBook.guestBook;
        //검색어 생성
        String keyword = "1";
        //검색을 적용하기 위한 Builder 객체 생성
        BooleanBuilder builder = new BooleanBuilder();
        //조건 표현식 생성
        BooleanExpression ex1 =
                qGuestBook.title.contains(keyword);

        BooleanExpression ex2 =
                qGuestBook.content.contains(keyword);
        //2개의 표현식을 or로 연결
        BooleanExpression expression =
                ex1.or(ex2);
        //검색 객체에 표현식 추가
        builder.and(expression); //4
        Page<GuestBook> result = guestBookRepository.findAll(
                builder, pageable); //5
        result.stream().forEach(guestbook -> {
            System.out.println(guestbook);
        });
    }

7.Service Layer

1)domain 패키지에 GuestBookDTO 클래스를 생성

GuestBookDTO

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GuestBookDTO {
private Long gno;
private String title;
private String content;
private String writer;
private LocalDateTime regDate;
private LocalDateTime modDate;

}

 

2)service 패키지에 Service 인터페이스를 생성하고 DTO와 Entity사이의 변환을 위한 메서드 작성

GuestBookService

public interface GuestBookService {
    //DTO를 Entity로 변환해주는 메서드
    default GuestBook dtoToEntity(GuestBookDTO dto){
    GuestBook entity = GuestBook.builder()
            .gno(dto.getGno())
            .title(dto.getTitle())
            .content(dto.getContent())
            .writer(dto.getWriter())
            .build();
    return  entity;
    }
}

 

GuestBookService'

//Entity를 DTO로 변환해주는 메서드
    default GuestBookDTO entiBookDto(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;
    }

 

3)클라이언트의 요청을 처리할 메서드를 구현 한 Service 클래스를 생성

GuestBookServiceImpl

@RequiredArgsConstructor
@Service
@Log4j2
public class GuestBookServiceImpl implements  GuestBookService{
    private final GuestBookRepository guestBookRepository;
}

⇒실제 업무에서는 Repository가 여러개 인 경우가 많음

⇒항상 이곳에서는 implements를 하고, Repository를 주입 해야 함

⇒Service 많이 안 읽어보고 Repository랑 entity을 위주로 코드를 많이 읽어봄

 

 

4)데이터 삽입을 위한 메서드 선언

GuestBookService

public interface GuestBookService {
    //데이터 삽입을 위한 메서드
    public Long register(GuestBookDTO dto);

 

5) ServiceImpl 클래스에 데이터 삽입을 위한 메서드 구현

GuestBookServiceImpl

@RequiredArgsConstructor
@Service
@Log4j2
public class GuestBookServiceImpl implements GuestBookService{
    private final GuestBookRepository guestBookRepository;

    //데이터 삽입을 위한 메서드
    @Override
    public Long register(GuestBookDTO dto){
        log.info("데이터 삽입");
        log.info(dto);
        //Repository에서 사용하기 위해 DTO를 Entity로 변환
        GuestBook entity =dtoToEntity(dto);
        //데이터 삽입        
        GuestBook result =guestBookRepository.save(entity);
        //삽입한 후 리턴받은 데이터의 gno 리턴
        return result.getGno();

    }
}

 

6)test디렉토리에 Service 테스트를 위한 클래스를 생성하고 삽입 메서트 테스트

ServiceTest

@SpringBootTest
public class ServiceTest {
    @Autowired
    private GuestBookService guestBookService;

    @Test
    public void insertTest(){
        GuestBookDTO dto = GuestBookDTO.builder()
                .title("삽입 테스트")
                .content("서비스에서 삽입")
                .writer("yn")
                .build();
        Long gno=guestBookService.register(dto);
        System.out.println("삽입된 번호:"+gno);
    }

 

7)목록 가져오기 구현

⇒데이터 목록을 요청할 때 전체 데이터가 아니라 페이지 단위로 요청하는 경우가 있는데 이 경우는 페이지 번호와 데이터 개수를 매개변수로 대입해주어야 한다.

페이지번호와 데이터개수를 받아서 Pageable인스턴스를 생성해주는 DTO를 생성

PageRequestDTO

@Builder
@AllArgsConstructor
@Data
public class PageRequestDTO {

    //페이지 번호
    private int page;
    //페이지 당 출력할 데이터 개수
    private int size;

    //기본값을 설정하기 위한 생성자
    public PageRequestDTO() {
        this.page = 1;
        this.size = 10;
    }

    //페이지 번호와 데이터 개수를 가지고 Pageable 객체를 만들어주는 메서드
    public Pageable getPageable(Sort sort) {
        return PageRequest.of(page - 1, size, sort);
    }
}

 

⇒목록 가져오기 요청을 했을 때 응답 받기 위한 DTO클래스를 생성

-다른 곳에서 재사용을 하기 위해 Generic을 사용 -domain.PageResponseDTO

PageResponseDTO

import lombok.Data;
import org.springframework.data.domain.Page;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

//목록보기 응답을 위한 클래스 (Generic)
@Data
public class PageResponseDTO<DTO, EN> {
    //응답 목록을 저장할 List
    private List<DTO> dtoList;

    //Page 객체와 변환 함수를 넘겨받아서 dtoList를 만들어주는tjem
    //Page객체를 순회하면서 fn함수로 변환한 후 List로 만들기
    public PageResponseDTO(Page<EN> result, Function<EN,DTO> fn ){
        dtoList = result.stream().map(fn).collect(Collectors.toList());

    }
}

 

⇒목록 보기를 위한 메서드를 Service 인터페이스에 선언

GuestBookService

//목록보기를 위한 메서드
    public PageResponseDTO<GuestBookDTO, GuestBook>
    getList(PageRequestDTO requestDTO);

 

⇒목록 보기를 위한 메서드를 ServiceImpl 클래스에 구현

@Override
    public PageResponseDTO<GuestBookDTO, GuestBook> getList(PageRequestDTO requestDTO) {
        //페이지 단위 요청을 위한 Pageable 객체를 생성
        Pageable pageable = requestDTO.getPageable(
                Sort.by("gno").descending());
        //데이터베이스에서 조회
        Page<GuestBook> result = 
                guestBookRepository.findAll(pageable);
        //Entity를 DTO로 변환하기 위한 객체 생성
        Function<GuestBook, GuestBookDTO> fn =
                (entity -> entityToDTO(entity));
        //데이터 목록 생성
        return new PageResponseDTO<>(result, fn);
        
    }

 

⇒Test클래스에 테스트를 위한 메서드를 생성하고 확인

//목록보기 테스트
    @Test
    public void listTest(){
        //페이지 번호 와 데이터 개수 설정
        PageRequestDTO pageRequestDTO =
                PageRequestDTO.builder()
                        .page(1)
                        .size(10)
                        .build();
        //메서드 호출
        PageResponseDTO<GuestBookDTO, GuestBook> resultDTO =
                guestBookService.getList(pageRequestDTO);
        //확인
        for(GuestBookDTO dto : resultDTO.getDtoList()){
            System.out.println(dto);
        }
    }

 

⇒페이지 번호 목록을 하단에 출력하고자 하는 경우는 PageResponseDTo 클래스에 추가

@Data
public class PageResponseDTO<DTO, EN> {
    //응답 목록을 저장할 List
    private List<DTO> dtoList;

    //전체 페이지 개수
    private int totalPage;

    //현재 페이지 번호
    private int page;
    //한 페이지에 출력되는 데이터 개수
    private int size;

    //페이지 번호 목록의 시작 번호 와 종료 번호
    private int start, end;
    //이전 과 다음 존재 여부
    private boolean prev, next;
    
    //페이지 번호 목록
    private List<Integer> pageList;
    
    //페이지 번호 목록을 만들어주는 메서드
    private void makePageList(Pageable pageable){
        //현재 페이지 번호 와 페이지 당 데이터 개수 가져오기
        this.page = pageable.getPageNumber() + 1;
        this.size = pageable.getPageSize();
        
        //임시 종료 페이지 번호
        //페이지 번호를 10개 출력할 것이라서 10으로 나누고 곱함
        //페이지 번호 개수를 다르게 하고자 하면 숫자를 변경
        int tempEnd = (int)(Math.ceil(page/10.0)) * 10;
        //시작 페이지 번호
        start = tempEnd - 9;
        //이전 존재 여부
        prev = start > 1;
        //종료 페이지 번호
        end = totalPage > tempEnd ? tempEnd : totalPage;
        next = totalPage > tempEnd;
        //페이지 번호 목록 만들기
        pageList = IntStream.rangeClosed(start, end)
                .boxed().collect(Collectors.toList());
        
        
    }

    //Page 객체 와 변환 함수를 넘겨받아서
    //dtoList를 만들어주는 메서드
    public PageResponseDTO(Page<EN> result,
                           Function<EN, DTO> fn){
        //Page 객체를 순회하면서 fn 함수로 변환한 후
        //List로 만들기
        dtoList = result.stream().map(fn)
                .collect(Collectors.toList());
        //페이지 번호 목록 만들기
        totalPage = result.getTotalPages();
        makePageList(result.getPageable());
    }
}

 

⇒이전에 만든 테스트 메서드를 수정해서 확인

//목록보기 테스트
    @Test
    public void listTest(){
        //페이지 번호 와 데이터 개수 설정
        PageRequestDTO pageRequestDTO =
                PageRequestDTO.builder()
                        .page(1)
                        .size(10)
                        .build();
        //메서드 호출
        PageResponseDTO<GuestBookDTO, GuestBook> resultDTO =
                guestBookService.getList(pageRequestDTO);
        //확인
        for(GuestBookDTO dto : resultDTO.getDtoList()){
            System.out.println(dto);
        }

        //이전 페이지 번호 와 다음 페이지 존재 여부
        System.out.println("이전 여부:" + resultDTO.isPrev());
        System.out.println("다음 여부:" + resultDTO.isNext());
        //전체 페이지 개수
        System.out.println("전체 페이지 개수:"
                + resultDTO.getTotalPage());
        //페이지 번호 목록
        resultDTO.getPageList()
                .forEach(i -> System.out.println(i));
    }

8.Controller Layer

1)목록 보기 구현

⇒Controller클래스 요청 처리 메서드를 수정

@Controller
@Log4j2
@RequiredArgsConstructor
public class GuestBookController {
    //서비스 객체 주입
    private final GuestBookService guestBookService;
    
    @GetMapping({"/"})
    public String main(){
        log.info("/");
        return "redirect:/guestbook/list";
    }

    //void를 리턴하면 요청 URL이 View의 이름이 됩니다.
    @GetMapping({ "/guestbook/list"})
    public void list(PageRequestDTO dto, Model model){
        log.info("list............");
        //서비스 메서드 호출
        //result 의 dtoList 에 DTO 의 List 가 있고
        //result 의 pageList 에 페이지 번호의 List 가 존재 
        model.addAttribute("result", 
                guestBookService.getList(dto));
    }

}

 

⇒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>방명록</h1>

    <table class="table table-striped">
      <thead>
        <tr>
          <th scope="col">글번호</th>
          <th scope="col">제목</th>
          <th scope="col">작성자</th>
          <th scope="col">작성일</th>
        </tr>
      </thead>
      <tbody>
      <tr th:each="dto : ${result.dtoList}">
        <th scope="row">[[${dto.gno}]]</th>
        <td>[[${dto.title}]]</td>
        <td>[[${dto.writer}]]</td>
        <td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
      </tr>
      </tbody>
    </table>

    <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})}"
           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})}">
            [[${page}]]
          </a>
        </li>

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


      </li>
    </ul>

  </th:block>
</th:block>

 

2)등록 구현

⇒등록 링크를 눌러서 데이터 입력 화면으로 이동으로 데이터 입력 화면에서 데이터를 입력한 후 실제 등록 링크를 눌러서 이동하는 것은 forwarding으로 처리하지만 삽입 작업을 수행하고 결과 페이지로 이동 할 때는 redirect로 이동

⇒Controller클래스에 등록을 위한 메서드 생성

//등록 요청을 GET 방식으로 처리하는 메서드 - 등록 페이지로 이동
    @GetMapping("/guestbook/register")
    public void register(){
        log.info("register GET...");
    }
    
    //등록 요청을 POST 방식으로 처리하는 메서드 - 등록 수행
    @PostMapping("/guestbook/register")
    public String register(GuestBookDTO dto,
                           RedirectAttributes redirectAttributes){
        log.info("register POST...");
        //등록 요청 처리
        Long gno = guestBookService.register(dto);
        //데이터 저장
        redirectAttributes.addFlashAttribute("msg",
                gno + " 등록");
        //목록 보기로 리다이렉트
        return "redirect:/guestbook/list";
    }

 

⇒list.html파일에 등록 링크를 추가하고 msg 출력할 영역을 생성

<span>
      <a th:href="@{/guestbook/register}">
        <button type="button" class="btn btn-outline-primary">
          방명록 작성
        </button>
      </a>
    </span>
    <div th:if = "${msg != null}" th:text="${msg}"></div>

 

=>templates/guestbook 디렉토리에 register.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 class="mt-4">방명록 등록</h1>
    <form th:action="@{/guestbook/register}"
          th:method="post">
      <div class="form-group">
        <label>제목</label>
        <input type="text" class="form-control"
               name="title" placeholder="제목 입력"/>
      </div>
      <div class="form-group">
        <label>내용</label>
        <textarea class="form-control" rows="5"
                  name="content"></textarea>
      </div>
      <div class="form-group">
        <label>작성자</label>
        <input type="text" class="form-control"
               name="writer" placeholder="작성자 입력"/>
      </div>
      <button type="submit" class="btn btn-primary">
        등록
      </button>
    </form>
  </th:block>
</th:block>
728x90