카테고리 없음

과제 - 소통 게시판 - 3

parkrams 2023. 6. 28. 17:34
728x90

Spring Data JPA

프로그램 개발 목표

  • 게시물 등록 / 수정 / 삭제 / 조회
  • 게시물 페이징 처리 / 게시물 검색

Board Entity와 JpaRepository

  • domain package 생성
  • Board class 작성
package com.board.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long bno;           // 게시글 번호 오토인크리먼트

    private String title;       // 제목

    private String content;     // 내용

    private String writer;       //작성자

}
  • entity 객체를 위한 entitiy 클래스는 반드시 @Entity를 적용해야 하고 @Id가 필요

@MappedSuperClass를 이용한 공통 속성 처리

  • domain 패키지에 BaseEntity 클래스 생성
  • BaseEntity에서 가장 중요한 부분은 자동으로 Spring Data JPA의 AuditingEntityLis-tener를 지정하는 부분
  • AuditingEntityListener를 적용하면 엔티티가 데이터베이스에 추가되거나 변경될 때 자동으로 시간 값을 지정할 수 있다.
  • AuditingEntityListener를 활성화 시키기 위해서는 프로젝트의 설정에 @EnableJpaAuditing을 추가해 주어야 한다.
package com.board;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class BoardApplication {

    public static void main(String[] args) {
        SpringApplication.run(BoardApplication.class, args);
    }

}
package com.board.domain;

import lombok.*;

import javax.persistence.*;

@Getter
@Entity
@Builder
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Board extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long bno;           // 게시글 번호 오토인크리먼트

    @Column(length = 500, nullable = false) // 칼럼의 길이와 null 허용여부
    private String title;       // 제목

    @Column(length = 2000, nullable = false)
    private String content;     // 내용

    @Column(length = 50, nullable = false)
    private String writer;       //작성자

}

JpaRepository 인터페이스

테스트 코드를 통한 CRUD / 페이징 처리 확인

  • insert 기능 테스트
    • 데이버테이스에 insert 를 실행하는 기능은 JpaRepository 의 save()를 통해서 이루어 진다.
    • save() 는 엔티티 객체가 없을 때는 insert, 존재할 때는 update를 자동으로 실행
    // insert 기능
    @Test
    public void testInsert(){
        IntStream.rangeClosed(1, 100).forEach(i -> {
            Board board = Board.builder()
                    .title("title......" + i)
                    .content("content 으으" + i)
                    .writer("작성자는 ------" + (i%10))
                    .build();

            Board result  = boardRepository.save(board);
            log.info("BNO : " + result.getBno());
        });
    }
  • select 기능 테스트
    • 특정 번호의 게시물을 조회하는 기능은 findById를 이용해서 처리
    • findById의 리턴 타입은 Optional
  • orElseThrows는 Optional의 인자가 null일 경우 예외처리를 한다
  • 보통 JPA 사용 시에 가져온 값이 null일 경우 예외를 발생
    // select 기능
    @Test
    public void testSelect(){
        Long bno = 100L;

        Optional<Board> result = boardRepository.findById(bno);
        Board board = result.orElseThrow();

        log.info(board);
    }
  • update 기능 테스트
    • insert 와 동일하게 save()를 통해서 처리
    • update는 등록 시간이 필요하므로 가능하면 findBy-Id()로 가져온 객체를 이용해서 약간의 수정을 통해서 처리
  • Board의 제목 / 내용은 수정이 가능하므로 이에 맞도록 change() 라는 메소드를 다음과 같이 추가
  • Board Entity 에서 수정
  • public void change(String title, String content){ this.title = title; this.content = content; }
  // update
    @Test
    public void testUpdate(){
        Long bno = 100L;
        Optional<Board> result = boardRepository.findById(bno);
        Board board = result.orElseThrow();

        board.change("update..title 100", "update content 100");
        boardRepository.save(board);
    }
  • delete 기능
    • @Id에 해당하는 값으로 deleteById()를 통해서 실행
  • Pageable 과 Page 타입
    • 페이징 처리는 Pageable 이라는 타입의 객체를 구성해서 파라미터로 전달하면 된다
    • Pageable은 인터페이스로 설계되어 있고, 일반적으로 PageRequest.of()라는 기능을 이용해서 개발이 가능
    • PageRequest.of(페이지 번호, 사이즈) : 페이지 번호는 0 부터
    • PageRequest.of(페이지 번호, 사이즈 Sort): 정렬 조건 추가
    • PageRequest.of(페이즈 번호, 사이즈, Sort.Direction, 속성...) : 정렬 방향과 여러 속성 지정
        @Test
    public void testPaging(){
        //1 page order by bno desc
        Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());
        Page<Board> result = boardRepository.findAll(pageable);

        log.info("total count : " + result.getTotalElements());
        log.info("total pages : " + result.getTotalPages());
        log.info("page number : " +  result.getNumber());
        log.info("page size : " + result.getSize());

        List<Board> todoList = result.getContent();

        todoList.forEach(board -> log.info(board));

    }
  • 쿼리 메소드와 @Query
    • 쿼리 메소드는 보통 SQL에서 사용하는 키워드와 칼럼들을 같이 결합해서 구성하면 그 자체가 JPA에서 사용하는 쿼리가 되는 기능
      • @Query 어노테이션의 value로 작성하는 문자열을 JPQL이라고 하는데 SQL과 유사하게 JPA에서 사용하는 쿼리 언어라 생각
  • build.gradle 맨 위에
buildscript {
    ext{
        queryDslVersion = "5.0.0"
    }
}
  • 의존성
  • // Querydsl implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" annotationProcessor( "javax.persistence:javax.persistence-api", "javax.annotation:javax.annotation-api", "com.querydsl:querydsl-apt:${queryDslVersion}:jpa" )

- 마지막 부분

sourceSets{
main{
java{
srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
}
}
}


#### 기존의 Repository와 Querydsl 연동하기
- Querydsl을 기존 코드에 연동하기 위해 다음과 같은 과정으로 작성
    - Querydsl을 이용할 인터페이스 선언
    - '인터페이스 이름 + Impl' 이라는 이름으로 클래스를 선언 - 이때 QuerydslRepositorySupport라는 부모 클래스를 지정하고 인터페이스를 구현
    - 기존의 Repository에는 부모 인터페이스로 Querydsl을 위한 인터페이스를 지정


- repository 패키지에 search 하위 패키지를 추가 BoardSearch라는 인터페이스를 선언
    - BoardSearch에는 단순히 페이지 처리 기능만 선언

package com.board.repository.search;

import com.board.domain.Board;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

public interface BoardSearch {

Page<Board> search1(Pageable pageable);

}

package com.board.repository.search;

import com.board.domain.Board;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;

public class BoardSearchImpl extends QuerydslRepositorySupport implements BoardSearch {

public BoardSearchImpl(){
    super(Board.class);
}

@Override
public Page<Board> search1(Pageable pageable){
    return null;
}

}


- 기존의 BoardRepository의 선언부에 BoardSearch 인터페이스를 추가로 지정

package com.board.repository;

import com.board.domain.Board;
import com.board.repository.search.BoardSearch;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface BoardRepository extends JpaRepository<Board, Long>, BoardSearch {

@Query(value = "select now()", nativeQuery = true)
String getTime();

}


#### Q도메인을 이용한 쿼리 작성 및 테스트 
- Querydsl의 목적은 '타입' 기반으로 '코드'를 이용해서 JPQL 쿼리를 생성하고 실행하는 것
- 이때 코드를 만드는 대신 클래스가 Q도메인 클래스
- 작성된 BoardSearchImpl에서 Q도메인을 이용하는 코드를 작성 


- JPQLQuery는 @Query로 작성했던 JPQL을 코드를 통해서 생성할 수 있게 한다. 이를 where나 group by 혹은 조인 처리 등이 가능
- JPQLQuery의 실행은 fetch() 라는 기능을 이용하고, fetchCount()를 이용하면 count 쿼리를 실행할 수 있다.
- search1() 코드에 이를 작성


#### 검색을 위한 메소드 선언과 테스트 

- BoardSearchImpl 에서 searchAll의 반복문과 제어문을 이용한 처리가 가능
- 검색 조건을 의미하는 types '제목(t), 내용(c), 작성자(w)'

@Override
public Page searchAll(String[] types, String keyword, Pageable pageable){

    QBoard board = QBoard.board;
    JPQLQuery<Board> query = from(board); //select.. from board

    if( (types != null && types.length > 0 ) && keyword != null ){ // 검색 조건과 키워드가 있다면

        BooleanBuilder booleanBuilder = new BooleanBuilder();

        for (String type : types){

            switch (type){
                case  "t":
                    booleanBuilder.or(board.title.contains(keyword));
                    break;
                case "c":
                    booleanBuilder.or(board.content.contains(keyword));
                    break;
                case "w":
                    booleanBuilder.or(board.writer.contains(keyword));
                    break;
            }
        } // end for
        query.where(booleanBuilder);
    } // end if

    //bno > 0
    query.where(board.bno.gt(0L));

    //paging
    this.getQuerydsl().applyPagination(pageable, query);
    List<Board> list = query.fetch();
    long count = query.fetchCount();

    return null;
}
 @Test
    public void testSearchAll(){
        String[] types = {"t", "c", "w"};

        String keyword = "1";

        Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());

        Page<Board> result = boardRepository.searchAll(types, keyword, pageable);

        //total pages
        log.info(result.getTotalPages() + "왜 이게 이거여?");

        // page size
        log.info(result.getSize());

        //pageNumber
        log.info(result.getNumber());

        //prev next
        log.info(result.hasPrevious() + " : " + result.hasNext());

        result.getContent().forEach(board -> log.info(board));
    }
2023-06-29 09:34:42.514  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : 2왜 이게 이거여?
2023-06-29 09:34:42.516  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : 10
2023-06-29 09:34:42.517  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : 0
2023-06-29 09:34:42.520  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : false : true
2023-06-29 09:34:42.537  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=100, title=title......100, content=content 으으100, writer=작성자는 ------0)
2023-06-29 09:34:42.537  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=91, title=title......91, content=content 으으91, writer=작성자는 ------1)
2023-06-29 09:34:42.538  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=81, title=title......81, content=content 으으81, writer=작성자는 ------1)
2023-06-29 09:34:42.538  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=71, title=title......71, content=content 으으71, writer=작성자는 ------1)
2023-06-29 09:34:42.538  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=61, title=title......61, content=content 으으61, writer=작성자는 ------1)
2023-06-29 09:34:42.539  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=51, title=title......51, content=content 으으51, writer=작성자는 ------1)
2023-06-29 09:34:42.539  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=41, title=title......41, content=content 으으41, writer=작성자는 ------1)
2023-06-29 09:34:42.539  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=31, title=title......31, content=content 으으31, writer=작성자는 ------1)
2023-06-29 09:34:42.539  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=21, title=title......21, content=content 으으21, writer=작성자는 ------1)
2023-06-29 09:34:42.539  INFO 10684 --- [    Test worker] c.board.repository.BoardRepositoryTest   : Board(bno=19, title=title......19, content=content 으으19, writer=작성자는 ------9)
2023-06-29 09:34:42.575  INFO 10684 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
  • 왜 2 일까 ... 페이지 수가

-------------------------- 23 - 6 - 29 ---------------------

게시물과 관리 완성하기

서비스 계층과 DTO의 구현

  • BoardRepository의 모든 메소드는 서비스 계층을 통해서 DTO로 변환되어 처리되도록 구성
  • 엔티티 객체는 영속 컨텍스트에서 관리 되므로 가능하면 많은 계층에서 사용 되지 않는 것이 좋다
  • ModelMapper 이용

ModelMapper 설정

  • DTO와 엔티티간의 변환 처리를 간단히 처리 하기 위해 ModelMapper를 이용할 것이므로 build.gradle 파일에 ModelMapper 라이브러리가 존재하는지 확인
    //ModelMapper 를 사용하기 위한 의존성
    implementation 'org.modelmapper:modelmapper:3.1.0'
  • config 패키지 생성 - > RootConfig 클래스 생성

CRUD 작업처리

등록 작업 처리

  • BoardService 인터페이스에는 register()를 선언
Long register(BoardDto boardDto);
  • BoardServiceImpl 생성
@Service
@Log4j2
@RequiredArgsConstructor
@Transactional
public class BoardServiceImpl implements BoardService {

    private final ModelMapper modelMapper;

    private final BoardRepository boardRepository;

    @Override
    public Long register(BoardDto boardDto){
        Board board = modelMapper.map(boardDto, Board.class);

        Long bno = boardRepository.save(board).getBno();

        return bno;
    }
}
  • 의존성 주입 외에 @Transactional 어노테이션을 적용
  • @Transactional을 적요하면 스프링은 해당 객체를 감싸는 별도의 클래스를 생성해 내는데 간혹 여러번의 데이터베이스 연결이 있을 수도 있으므로 트랜잭션 처리는 기본적으로 적용 해두는 것이 좋다.
  • 등록 처리 구현
  • 테스트 코드에서 insert 문이 동작하는지 확인하고 최종적으로 DB 확인
    @Test
    public void register() {
        log.info(boardService.getClass().getName());

        BoardDto boardDto = BoardDto.builder().title("Sample Title....").content("SampleContent...").writer("user00").build();

        Long bno = boardService.register(boardDto);

        log.info("bno : " + bno);
    }

조회 작업 처리

  • 조회는 특정한 게시물의 번호를 이용하므로 readOne 메서드 추가
  • BoardServiceImpl
  • @Override public BoardDto readOne(Long bno){ Optional<Board> result = boardRepository.findById(bno); Board board = result.orElseThrow(); BoardDto boardDto = modelMapper.map(board, BoardDto.class); return boardDto; }

수정 작업 처리

  • 수정 작업은 기존 엔티티 객체에서 필요한 부분만 변경하도록 작성
  • void modify(BoardDto boardDto);
  • BoardServiceImpl 에서 Board chage를 이용해서 필요한 부분만 수정
  • 수정 확인 테스트
// update test
    @Test
    public void testModify(){
        // 변경에 필요한 데이터만
        BoardDto boardDto = BoardDto.builder()
                .bno(101L)
                .title("update..........1111")
                .content("update content 11111111.")
                .build();
        boardService.modify(boardDto);
    }

목록 / 검색 처리

PageRequestDto

  • PageRequestDto는 페이징 관련 정보 (page/size) 외에 검색의 종류 (type)와 키워드 (key-word)를 추가 해서 지정
  • 검색의 종류는 문자열 하나로 처리해서 나중에 각 문자를 분리하도록 구성
  • 현재 검색 조건들은 BoardRepository에서 String[] 로 처리하기 때문에 type라는 문자열을 배열로 반환해 주는 기능 필요, 페이징 처리를 위해 사용하는 Pageable 타입을 반환하는 기능도 있으면 편리 하므로 메소드로 구현

PageResponseDto

  • PageResponseDto는 화면에 DTO 목록과 시작 페이지 / 끝 페이지 등에 대한 처리를 담당
package com.board.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

import java.util.List;

@Getter
@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;

    @Builder(builderMethodName = "withAll")
    public PageResponseDto(PageRequestDto pageRequestDto, List<E> dtoList, int total){
        if (total <= 0){
            return;
        }

        this.page = pageRequestDto.getPage();
        this.size = pageRequestDto.getSize();

        this.total = total;
        this.dtoList = dtoList;

        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 = end > last ? last: end;

        this.prev = this.start > 1;

        this.next = total > this.end * this.size;
    }


}

BoardSerivce / BoardServiceImpl 수정

  • BoardService 에 list() 메서드 생성 // 목록 / 검색 기능
PageResponseDto<BoardDto> list(PageRequestDto pageRequestDto);
  • BoardServiceImpl은 ㅇ선 boardRepository를 호출하는 기능 부터 작성
    @Override
    public PageResponseDto<BoardDto> list(PageRequestDto pageRequestDto){

        String [] types = pageRequestDto.getTypes();
        String keyword = pageRequestDto.getKeyword();
        Pageable pageable = pageRequestDto.getPageable("bnb");

        Page<Board> result = boardRepository.searchAll(types, keyword, pageable);

        return null;
  • Page<Board>는 List<BoardDto>로 변환될 필요가 있다. 이 처리는 뒤에서 한번에 DTO 추출 하는 기능을 사용하겠지만
  • 지금은 직접 변환하는 코드 작성

컨트롤러와 화면 처리

  • Board에 대한 컨트롤러는 프로젝트에 controller 패키지를 추가하고 BoardController를 이용해서 처리하도록 구성
  • 가장 우선 구현은 목록이므로 list() 메서드 추가 하고 PageRequestDto를 이용해 페이징 처리와 검색에 이용

화면 구성을 위한 준비

  • 화면 구성은 Thymeleaf를 이용해서 레이아웃을 적용하도록 준비
  • build.gradle 파일에 Thymeleaf의 레이아웃관련 라이브러리의 존재 여부 확인
    // thymeleaf
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.1.0'

템플릿 디자인 적용

목록화면 개발

페이지 목록의 출력

  • <table> 태그가 끝나는 부분과 이어지게 <div>를 구성해서 페이지 번호들을 화면에 출력
  • PageResponseDto는 시작 번호 start와 끝번호 end 만 가지고 있으므로 특정 범위의 숫자를 만들기 위해 Thymeleaf의 numbers를 이용
<!--                특정 범위의 숫자를 만들기 위해 numbers 이용 -->
                <div class="float-end">
                    <ul class="pagination flex-wrap">

                        <li class="page-item" th:if="${responseDto.prev}">
                            <a class="page-link" th:data-num="${responseDto.start -1}">Previous</a>
                        </li>


                        <th:block th:each="i: ${#numbers.sequence(responseDto.start, responseDto.end)}">
                            <li th:class="${responseDto.page == i}?'page-item active':'page-item'" >
                                <a class="page-link"  th:data-num="${i}">[[${i}]]</a>
                            </li>
                        </th:block>

                        <li class="page-item" th:if="${responseDto.next}">
                            <a class="page-link"  th:data-num="${responseDto.end + 1}">Next</a>
                        </li>
                    </ul>
                </div>
  • #numbers.sequence() 로 특정한 범위의 연속된 숫자를 만드는 부분과 <a>태ㅡ에 'data-num' 이라는 속성으로 페이지 번호를 처리하는 부분
  • 브라우저 아래에 페이지 출력
  • list?page=4 로 번호 출력

검색 화면 추가

  • list.html 페이지에는 검색이 이루어질 수 있도록 <table> 위에 별도의 card 영역을 만들어서 검색 조건을 선택할 수 있도록 구성
  • 검색 조건은 페이지 이동과 함께 처리될 수 있도록 form 태그로 감싸 처리
  • http://localhost:9090/board/list?type=t&keyword=2 코드 작성 후 검색

이벤트 처리

  • 페이지 번호를 클릭하거나 검색 / 필터링 조건을 눌렀을 대 이벤트를 처리 추가
  • 처리 방식
    • 페이지 번호를 클릭하면 검색 창에 있는 <form> 태그에 <input type='hidden'>으로 page를 추가한 후 submit
    • clear 버튼을 누르면 검색 조건 없이 '/board/list' 호출

등록 처리 화면 개발

  • 실제 Post 방식으로 처리할 때 눈 여겨 보야 하는 부분은 @Valid 에서 문제가 발생 했을 때 모든 에러를 errors라는 이름으로 RedirectAtrributes에 추가 해서 전송
register.html 처리
  • templates/board 폴더에 register.html 을 추가

조회 처리와 화면 개발

게시물의 수정 / 삭제 처리

728x90