[SpringBoot] 게시판 프로젝트 - 2

Hyeonseok Jeong·2023년 9월 26일
0

저번 복습글에 이은 2편을 작성해본다.

사용한 기술 스택

  • Spring Boot 3
  • Spring Security
  • Spring JPA
  • Java
  • MySQL
  • Thymeleaf
  • LomBok
  • HTML
  • CSS
  • Javascript

구현한 기능

  • 스프링 시큐리티를 이용한 로그인 기능 (작성 완료)
  • 스프링 시큐리티를 이용한 회원가입 기능 (작성 완료)
  • 게시판 리스트
  • 게시판 디테일 (다음글, 이전글 링크 구현)
  • 게시글 생성 & 다중 파일 업로드 기능 구현
  • 파일 다운로드 기능
  • 게시글 수정
  • 검색 기능
  • 페이지 네이션 기능
  • 댓글 생성
  • 댓글 삭제

오늘의 구현

  • 게시판 리스트
  • 게시판 디테일 (다음글, 이전글 링크 구현)

리스트 & 디테일

프로젝트를 할때 인증 인가 부분을 먼자한 이유는 까다로운 이유도 있지만 게시판에서 리스트와 디테일을 보는건 누구나 볼 수 있지만 게시물을 생성하거나 디테일 페이지에서 댓글을 생성하는 부분에서는 사용자의 로그인이 필요하므로 로그인, 회원가입 부분을 먼저 진행하였다.

아무튼 이번편은 게시판 리스트 부분이다.
솔직히 해당 부분은 쉬우면서도 까다로운 부분이라고 생각한다.
이유는 JPA 를 사용하기 때문이 아닐까? 가령 그냥 시퀄(SQL)을 사용한다고 했을 때
select * from board 와 같은 부분은 JPA를 통해서도 쉽게 구현 할 수 있다.
Repository에 접근해서 Service단에서 BoardRepository.findByAll() [Optional<>] 과 같이 사용하면 되기 때문이다.

그런데 쿼리문이 복잡해지기 시작하면 사용이 어려워지는것 같다.
select * from (select rownum r, b.* from (select * from board order by reg_date desc)) where r between 1 and 5 와 같은 쿼리문을 페이지 전환을 위해 사용하게 되는데 JPA 로 사용하기 위해서는 Board Repository단에 Page<Board>반환 값을 가지고 매게변수로 Pageable pageable 를 받는 메소드를 추가하여 사용해야한다.

사실 사용하는 것 자체는 쉬울순 있다. 단지 따라하기만 하면 되니까
그런데 따라만 하면서 코드를 작성하고 개발을 할거면 그건 개발자가 아닌 코드 복사기가 아닐까
무엇이든 이해가 뒷받침 되어야만 후에 스스로 코드를 작성하고 개발을 할 수 있을거라 생각한다.
물론 처음에는 코드를 복사하며 공부하는 것도 좋지만 그게 일정시간동안 반복되면 진지하게 개발자라는 직종을 향해 다가가고 있나? 라는 고민을 해볼 필요가 있을것이다.

라고 말하며 다시한번 나 자신을 다그쳐 보며 계속 설명하자면 위의 메소드를 사용하기 위해 Service 단에서는 Page<Board>를 반환하는 서비스 로직을 작성하고 그 메소드 안에 매게변수에 입력할 Pageable을 만들어 주어야 한다.

너무 설명이 길어지는 것 같으니 자세한건 코드를 봐보도록 하자
먼저 리스트와 디테일 출력 부분의 Repository 단 이다.

package com.mysite.board.siteBoard;

import com.mysite.board.siteUser.SiteUser;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.repository.query.Param;

import javax.swing.text.html.Option;
import java.util.List;
import java.util.Optional;



public interface BoardRepository extends JpaRepository<Board, Integer> {
    @Query(value = "SELECT * FROM BOARD " +
            "WHERE REG_DATE > (SELECT REG_DATE FROM BOARD WHERE ID = :id) " +
            "LIMIT 1", nativeQuery = true)
    Optional<Board> findByNextBoard(Integer id);

    @Query(value = "select * from board " +
            "where reg_date < (select reg_date from board where id = :id) order by reg_date desc " +
            "LIMIT 1", nativeQuery = true)
    Optional<Board> findByPrevBoard(Integer id);

    Page<Board> findAll(Pageable pageable);

    Page<Board> findAll(Specification<Board> spec, Pageable pageable);

    Page<Board> findBySubjectContaining(String keyword, Pageable pageable);
    Page<Board> findByContentContaining(String keyword, Pageable pageable);

    Page<Board> findBySubjectOrContentContaining(String keyword1, String keyword2, Pageable pageable);

}

Repository 단을 살펴보면 위에서 설명한 내용들이 있으며 다른 건 @Query(...) 를 사용하여 쿼리문을 작성해 디테일 페이지에 다음글과 이전글이 보여질 수 있도록 데이터 조회를 하였고 Page<Board> findAll(Pageable pageable); 아래의 코드들은 검색을 위한 부분들이니 지금은 신경 쓰지 않아도 된다.

다음으로는 Repository를 사용하기 위한 Service단 이다.

package com.mysite.board.siteBoard;


import com.mysite.board.DataNotFoundException;
import com.mysite.board.siteCmt.Comment;
import com.mysite.board.siteFile.FileEntity;
import com.mysite.board.siteFile.FileService;
import com.mysite.board.siteUser.SiteUser;
import jakarta.persistence.criteria.*;
import lombok.RequiredArgsConstructor;
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 org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
@Service
public class BoardService {

    private final BoardRepository boardRepository;
    private final FileService fileService;


    public Page<Board> boardList (String query, String kw, int page) {
        List<Sort.Order> sorts = new ArrayList<>();
        sorts.add(Sort.Order.desc("regDate"));
        Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
        if(query.equals("subject")) {
            return this.boardRepository.findBySubjectContaining(kw, pageable);
        } else if (query.equals("content")) {
            return this.boardRepository.findByContentContaining(kw, pageable);
        } else if (query.equals("user")) {
            Specification<Board> spec = search(kw);
            return this.boardRepository.findAll(spec, pageable);
        } else if (query.equals("subject+content")) {
            return this.boardRepository.findBySubjectOrContentContaining(kw, kw, pageable);
        }
        else {
            return this.boardRepository.findAll(pageable);
        }
    }

    public Board detail (Integer boardId) {
        Optional<Board> _board = this.boardRepository.findById(boardId);
        if(_board.isPresent()) {
            return _board.get();
        } else {
            throw new DataNotFoundException("board is empty");
        }
    }

    public Board NextPost (Integer boardId) {
        Optional<Board> _nextPost = this.boardRepository.findByNextBoard(boardId);
        return _nextPost.orElseGet(Board::new);
    }
    public Board prevPost (Integer boardId) {
        Optional<Board> _prevPost = this.boardRepository.findByPrevBoard(boardId);
        return _prevPost.orElseGet(Board::new);
    }

    public void hitUpdate (Integer boardId) {
        Optional<Board> _board = this.boardRepository.findById(boardId);
        if(_board.isPresent()) {
            Board board = _board.get();
            board.setHit(board.getHit() + 1);
            this.boardRepository.save(board);
        } else {
            throw new DataNotFoundException("board is empty");
        }
    }


}

(먼가 Board Service 단에 비해 부족해 보이는건 생성, 삭제 등은 오늘 주제가 아니기에 지워서 그럼)

Service단을 살펴보면 위에서 설명한대로 public Page<Board> boardList (String query, String kw, int page) { 부분에서 Pageable 생성을 위해 List<Sort.Order> sorts = new ArrayList<>(); 을 선언하고 sorts.add(Sort.Order.desc("regDate"));를 통해서 먼저 Sort를 정해준다 (글 최신순으로 정렬을 위해서 Sorts를 작성) 이후 PageRequests.of(page, 10 ,Sort.by(sorts)) 를 통해서 Pageable를 만들어 준다. Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));

  • page = 현재 페이지, 10 = 페이지당 출력되는 게시글 단위, Sort는 정렬

지금 상태는 위의 코드들을 말그대로 약간의 이해와 사용만을 하고 있는 상태 즉, Sort 와 PageRequest, Pagealbe 등 내부로직을 제대로 이해하지 못한 상태이다 물론 처음부터 모든걸 이해할 순 없다. 하지만 이렇게 블로그 글을 작성하면서 조금씩 나아가며 더 이해하고 활용할 수 있도록 해야겠다.

다음으론 if(query.equals("subject")) { 부분이 있는데 해당 부분은 검색을 위해 작성된 코드이므로 지금은 넘어간다 ( + 사실 별거 없고 JPA 정해진 문법?으로 findBy컬럼Containing() 을 통해서 where 컬럼 like '%kw%'와 같은 쿼리문이 작동하여 검색 기능이 되도록 해주는 코드들 이다.)

다음으로 Detail은 JPA 내장 메소드인 findById() 를 통해서 pathquery로 받아온 게시물 id를 매게변수로 전달하여 한개의 게시물을 받아와 출력하였고 해당 detail 페이지 안에서 해당 게시글 다음글과 이전글을 보여주기 위해 Repository에서 작성한 findByNext.. findByPrev.. 를 이용해 다음글과 이전글을 출력할 수 있도록 하였다.

마지막으로 이러한 Service 단을 사용하기 위한 Controller 단을 살펴보자면

    @GetMapping("/list")
    public String list (Model model,@RequestParam(value= "query", defaultValue = "subject") String query,@RequestParam(value = "kw", defaultValue = "") String kw , @RequestParam(value = "page", defaultValue = "0") int page) {
        Page<Board> boardList = this.boardService.boardList(query, kw, page);
        model.addAttribute("list", boardList);
        model.addAttribute("kw", kw);
        model.addAttribute("query", query);
        return "list";
    }

    @GetMapping("/detail/{id}")
    public String detail (Model model, @PathVariable("id") Integer id) {
        this.boardService.hitUpdate(id);
        Board post = this.boardService.detail(id);
        Board nextPost = this.boardService.NextPost(id);
        Board prevPost = this.boardService.prevPost(id);
        model.addAttribute("post", post);
        model.addAttribute("nextPost", nextPost);
        model.addAttribute("prevPost", prevPost);
        return "detail";
    }

위의 코드이다. 보면 BoardService를 사용하여 리스트 출력 및 디테일 출력하고 model단에 view로 보여주기 위해 출력된 데이터들을 추가해 주어 사용하게 된다.

사진

  • 게시판 리스트

  • 게시판 디테일

  • 이전글, 다음글 클릭

  • 검색 맛보기

마무리

이렇게 블로그를 적으며 내가 어떤 코드로 작성해서 프로젝트를 했는지 내가 얼마나 저 코드들에 대해서 이해했는지를 알아보고 있는데 아직 가야할길이 저 멀리 있다는게 보인다 하지만 처음 코딩을 시작했을때는 내가 걷는 길조차 안보였는데 지금은 어느정도 내가 걸어야할 길이 희미하게나마 보이니 길따라 가다보면 언젠간 내가 원하는 목표에 도달할 것이다.

profile
풀스텍 개발자

1개의 댓글

comment-user-thumbnail
2023년 9월 26일

벨로그 사진 업로드 안되는 이슈가 있네요~

답글 달기