Cohe

SpringBoot Project 게시판 만들기 2 본문

Spring, SpringBoot

SpringBoot Project 게시판 만들기 2

코헤0121 2024. 5. 24. 15:27
728x90

우선 프로젝트의 기본 구조를 설명하자면 mvc2 패턴으로 앞으로 작성해야 할 패키지는 controller, service, repository, dto, entity, html 코드이다.

mvc 모델은 다음과 같다.

  • Client: 사용자가 웹 브라우저를 통해 서버에 요청을 보낸다
  • Controller: 클라이언트의 요청을 받아 적절한 서비스 메서드를 호출한다. 서비스에서 반환된 결과를 바탕으로 HTML 페이지를 생성하여 클라이언트에게 응답한다.
  • Service: 비즈니스 로직을 처리하는 계층으로, 데이터 조작 및 변환을 수행한다. 필요한 경우 DTO를 사용하여 데이터를 전달한다
  • Repository: 데이터베이스와 상호 작용하는 계층으로, 엔티티 객체를 사용하여 데이터를 저장하고 조회한다.
  • DTO (Data Transfer Object): 계층 간 데이터 전달을 위한 객체로, 필요한 데이터만 포함한다.
  • Entity: 데이터베이스의 테이블과 매핑되는 객체로, 데이터베이스 스키마와 밀접한 관련이 있다.
  • HTML: 클라이언트에게 응답으로 전달되는 웹 페이지를 나타낸다.

더불어 프로젝트를 시작하기 전에 mapper를 설정해야 하기 때문에 config 설정을 해준다. 우선 config 설정을 위한 폴더를 만들고, RootConfig를 만들어준다.

RootConfig


package org.example.demo.config;

import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration //설정과 관련된 컴포넌트이다.
public class RootConfig {

    @Bean  //spring setting을 코드로 하고 있다
    public ModelMapper getMapper(){
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration().setFieldMatchingEnabled(true)
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
                .setMatchingStrategy(MatchingStrategies.STRICT);                // 딱딱하게 matching하는 것
        return modelMapper;
    }


}

springboot project 게시판 만들기 2

가장 먼저 할 일은 Board table에 맞춰 작성하는 것이다! 보통 패턴은 entity 작성 -> repository 작성 (test code 작성)-> dto 작성 -> service 작성(test code 작성) -> controller 작성 -> html 작성으로 넘어간다.

폴더를 아래 사진처럼 만들고

가장 먼저 생성한 날짜와 수정한 날짜의 경우 board와 reply에 동시에 들어갈 수 있으니, baseEntity로 생성해주자!


package org.example.demo.domain;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class})
@Getter
abstract class BaseEntity {
    //시간 설정 - 등록, 수정과 관련된 시간 설정
    @CreatedDate    //생성 시간 설정
    @Column(name = "regdate", updatable = false) //이름은 regdate, update는 불가능
    private LocalDateTime regDate;

    @LastModifiedDate       //마지막 수정 날짜
    @Column(name = "moddate")
    private LocalDateTime modDate;


}

Repository

엔티티를 만든 다음에 Repository를 작성하면 된다. 

package org.example.demo.repository;

import org.example.demo.domain.Board;
import org.example.demo.repository.search.BoardSearch;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

// 이 인터페이스는 JpaRepository를 확장하여 Board 엔티티에 대한 CRUD 작업을 제공합니다.
public interface BoardRepository extends JpaRepository<Board, Long>, BoardSearch { // 타입 = Board, Board bno의 타입인 id
    // JpaRepository는 Board 엔티티에 대한 DB 관련 작업을 수행하기 위해 사용됩니다.
    // 제네릭 타입 매개변수는 엔티티 타입이 Board이고 ID 타입이 Long임을 지정합니다.

    // JPA로 DB 관련 작업을 수행하기 위한 id 값
    // 하나면 OPTIONAL, 여러 개면 LIST -> 쿼리 메서드 방식
    // 이 메서드는 제목에 주어진 키워드가 포함된 Board 엔티티의 페이지를 반환합니다.
    // 결과는 bno (Board 번호)를 기준으로 내림차순으로 정렬됩니다.
    Page<Board> findByTitleContainingOrderByBnoDesc(String keyword, Pageable pageable);

    // @Query 어노테이션에서 사용하는 구문은 JPQL을 사용합니다.
    // JPQL은 SQL과 유사하게 JPA에서 사용하는 쿼리 언어입니다.
    // @Query를 이용하는 경우
    // 1. 조인과 같이 복잡한 쿼리를 실행하려고 할 때
    // 2. 원하는 속성만 추출해서 Object[]로 처리하거나 DTO로 처리 가능
    // 3. 속성 값 중 nativeQuery 속성 값을 true로 지정하면 SQL 구문으로 사용이 가능합니다.

    // 이 메서드는 JPQL을 사용하여 제목에 주어진 키워드가 포함된 Board를 찾습니다.
    // 결과는 Pageable 파라미터에 따라 페이지로 나뉩니다.
    @Query("select b FROM Board b where b.title like concat('%',:keyword,'%')")
    Page<Board> findKeyword(String keyword, Pageable pageable);

    // 이 메서드는 네이티브 SQL을 사용하여 데이터베이스에서 현재 시간을 가져옵니다.
    // 현재 시간을 String으로 반환합니다.
    @Query(value = "select now()", nativeQuery = true)
    String getTime();
}

상세 설명:

  1. 인터페이스 정의:
    • BoardRepository 인터페이스는 JpaRepository를 확장하여 Board 엔티티에 대한 CRUD 작업을 상속받습니다.
    • 또한, BoardSearch를 확장하여 추가 검색 기능을 제공합니다.
  2. JpaRepository:
    • JpaRepository<Board, Long>는 리포지토리가 Board 엔티티를 관리하며, ID 타입이 Long임을 지정합니다.
    • JpaRepositorysave(), findById(), findAll(), deleteById() 등의 메서드를 제공합니다.
  3. 커스텀 쿼리 메서드:
    • findByTitleContainingOrderByBnoDesc:
      • 이 메서드는 제목에 주어진 키워드가 포함된 모든 Board 엔티티를 찾습니다.
      • 결과는 bno 기준으로 내림차순으로 정렬됩니다.
      • Page<Board>를 반환하여 페이징을 지원합니다.
    • JPQL 쿼리:
      • findKeyword:
        • 이 메서드는 JPQL(Java Persistence Query Language)을 사용하여 쿼리를 실행합니다.
        • 제목에 주어진 키워드가 포함된 모든 Board 엔티티를 선택합니다.
        • Pageable 파라미터를 통해 페이징을 지원합니다.
    • 네이티브 SQL 쿼리:
      • getTime:
        • 이 메서드는 네이티브 SQL 쿼리를 사용하여 데이터베이스에서 현재 시간을 가져옵니다.
        • @Query 어노테이션에서 nativeQuery 속성을 true로 설정하여 네이티브 SQL 구문을 사용할 수 있습니다.

@Query 사용 사례:

  1. 복잡한 쿼리: 조인과 같이 복잡한 쿼리를 수행해야 할 때 사용합니다.
  2. 부분 데이터 추출: 전체 엔티티 대신 특정 속성만 추출해야 할 때, 원하는 필드를 선택하고 DTO 또는 Object[]로 매핑할 수 있습니다.
  3. 네이티브 쿼리: 원시 SQL 쿼리를 실행해야 할 때, nativeQuerytrue로 설정하여 데이터베이스 특정 SQL을 사용할 수 있습니다.

TestCode 작성

이제 repository가 잘 작동되는지 테스트를 해야 한다. Test 폴더에 repository라는 폴더를 만들고, BoardRepositoryTests 클래스를 만든다.

 

코드는 다음과 같다.


package org.example.demo.repository;

import lombok.extern.log4j.Log4j2;
import org.example.demo.domain.Board;
import org.example.demo.dto.BoardListReplyCountDTO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import java.util.List;
import java.util.Optional;
import java.util.stream.IntStream;

@SpringBootTest
@Log4j2
public class BoardRepositoryTests {
    @Autowired
    private BoardRepository boardRepository;

    //insert
    @Test
    public void testInsert() {
        IntStream.rangeClosed(1,100).forEach(i -> {
            Board board = Board.builder()
                    .title("title.............."+i)
                    .content("content...................."+i)
                    .writer("user"+(i%10)) //사용자는 0~9번까지
                    .build();
            Board result = boardRepository.save(board);  //JPA는 자동으로 만들어주기 때문에 내가 만들지 않은 save 메소드도 나온다.
            log.info(result);
        });
    }

    @Test
    public void testSelct() {
        Long bno =100L;

        Optional<Board> result = boardRepository.findById(bno);  //optional Type으로 받아서 처리해야 함
        Board board = result.orElseThrow();
        log.info(board);
    }

    @Test
    public void testUpdate() {
        //Entity는 생성시 불변이면 좋으나 변경이 일어날 경우 최소한으로 설계한다.
        Long bno =100L;

        Optional<Board> result = boardRepository.findById(bno);  //optional Type으로 받아서 처리해야 함
        Board board = result.orElseThrow();
        board.change("update title","update contents100");  //modDate 시간은 바뀌나 regdate 시간은 바뀌지 않는다.
        boardRepository.save(board);
        log.info(board);
    }
    @Test
    public void testDelete() {
        Long bno =100L;
        Optional<Board> result = boardRepository.findById(bno);
        Board board = result.orElseThrow();
        boardRepository.delete(board);
    }

//    Pageable과 page<E> 타입을 이용한 페이징 처리
//    페이징 처리는 Pagealbe이라는 타입의 객체를 구성해서 파라미터로 전달
//    pageable은 인터페이스로 설계되어 있고, 일반적으로 PageRequest.of()를 이용해서 개발함
//    PageRequest.of(페이지번호, 사이즈) : 페이지번호는 0번부터
//    PageRequest.of(페이지번호, 사이즈, Sort) : sort객체를 통한 정렬조건 추가
//    PageRequest.of(페이지번호, 사이즈, Sort.Direction, 속성) : 정렬 방향과 여러 속성 추가 지정
//    Pageable로 값을 넘기면 반환타입은 Page<T>를 이용하게 됨

    @Test
    public void testFindAll() {
        //1. PAGE order by bno desc
        Pageable pageable = PageRequest.of(0,10, Sort.by("bno").descending()); //domain이 알아서 생성해주기 때문에 따로 코드 진행하지 않아도 괜찮다
        Page<Board> result =boardRepository.findAll(pageable);
        log.info("total count : "+result.getTotalElements());   //전체 게시글의 수가 있음
        log.info("total pages : "+result.getTotalPages());      //전체 페이지 개수
        log.info("pages number : "+result.getNumber());         //페이지 번호
        log.info("pages size : "+result.getSize());             //페이지 사이즈
        log.info("pages has previous : "+result.hasPrevious());             //이전 페이지가 있냐
        log.info("pages has Next : "+result.hasNext());             //다음 페이지가 있냐

        List<Board> boardList=result.getContent();
        boardList.forEach(board -> {
            log.info(board);
        });
    }
    //쿼리 메서드 및 @Query 테스트
    @Test
    public void testQueryMethod() {
        Pageable pageable = PageRequest.of(0,10);
        String title = "title";
        Page<Board> result =  boardRepository.findByTitleContainingOrderByBnoDesc(
                title,
                pageable
        );
        result.getContent().forEach(board -> log.info(board));
    }

    @Test
    public void testQueryAnnotation() {
        Pageable pageable = PageRequest.of(0,10, Sort.by("bno").descending());

        String title = "title";
        Page<Board> result =  boardRepository.findByTitleContainingOrderByBnoDesc(title,pageable);
        result.getContent().forEach(board -> log.info(board));

    }

    @Test
    public void testGetTime(){
        log.info(boardRepository.getTime());
    }
    @Test
    public void testSearch(){

        //2번 페이지에 있는 order By bno desc
        Pageable pageable = PageRequest.of(1,10, Sort.by("bno").descending());

        boardRepository.searchOne(pageable);
    }
    @Test
    public void testSearchAll() {
        String[] types = {"t","c","w"};
        String keyword = "1";
        Pageable pageable = PageRequest.of(1,10,Sort.by("bno").descending());

        Page<Board> result = boardRepository.searchAll(types,keyword,pageable);
        result.getContent().forEach(board -> log.info(board));
        log.info("사이즈 : "+ result.getSize());
        log.info("페이지번호 : "+ result.getNumber());
        log.info("이전페이지 : "+ result.hasPrevious());
        log.info("다음페이지 : "+ result.hasNext());
    }

    @Test
    public void testSearchWithReplyCount(){
        String[] types = {"t","c","w"};
        String keyword = "1";
        Pageable pageable = PageRequest.of(0,10,Sort.by("bno").descending());
        Page<BoardListReplyCountDTO> result = boardRepository.searchWithReplyCount(types, keyword,pageable);

        result.getContent().forEach(boardListReplyCountDTO -> {
            log.info(boardListReplyCountDTO);
        });
    }





}
  1. testInsert: 1부터 100까지의 게시물을 생성하여 데이터베이스에 추가합니다.
  2. testSelect: 게시물 번호 100번을 조회하여 확인합니다.
  3. testUpdate: 게시물 번호 100번의 제목과 내용을 업데이트하고 확인합니다.
  4. testDelete: 게시물 번호 100번을 삭제하고 확인합니다.
  5. testFindAll: 페이징 처리를 하여 게시물을 조회하고 확인합니다.
  6. testQueryMethod: 제목에 특정 키워드가 포함된 게시물을 쿼리 메서드를 통해 조회하고 확인합니다.
  7. testQueryAnnotation: 제목에 특정 키워드가 포함된 게시물을 쿼리 어노테이션을 통해 조회하고 확인합니다.
  8. testGetTime: 데이터베이스의 현재 시간을 조회하여 확인합니다.
  9. testSearch: 특정 페이지에 있는 게시물을 조회하고 확인합니다.
  10. testSearchAll: 다양한 조건으로 게시물을 조회하고 확인합니다.
  11. testSearchWithReplyCount: 답글 수와 함께 다양한 조건으로 게시물을 조회하고 확인합니다.