[스프링 부트] 17. 스프링 웹 게시판 v2.0 (JPA)

KangHo Lee's avatar
Nov 21, 2024
[스프링 부트] 17. 스프링 웹 게시판 v2.0 (JPA)
💡
JPA에 포합된 JPQL과 Entity Manager, 영속성 컨텍스트를 활용해서 리팩터링

1. Board 객체

@AllArgsConstructor @Table(name = "board_tb") @NoArgsConstructor @Entity @Getter public class Board { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키 자동 생성 private Integer id; // null도 받기 위해 int -> Integer private String title; private String content; @CreationTimestamp // insert 시간 자동 입력 private Timestamp createdAt; // BoardRepository 의 update 메서드 대체 public void update(String title, String content) { this.title = title; this.content = content; } }

2. BoardRequest

package com.example.blog.board; import lombok.Data; public class BoardRequest { @Data public static class SaveDTO { private String title; private String content; // insert 용 메서드 public Board toEntity() { Board board = new Board(null, title, content, null); return board; } } @Data public static class UpdateDTO { private String title; private String content; } }

3. BoardResponse

package com.example.blog.board; import com.example.blog._core.util.DateToForm; import lombok.Data; import lombok.NoArgsConstructor; public class BoardResponse { @Data public static class DetailDTO { private int id; private String title; private String content; private String createdAt; public DetailDTO(Board board) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.createdAt = DateToForm.dateToFrom(board); } } @Data public static class UpdateFormDTO { private int id; private String title; private String content; private String createdAt; public UpdateFormDTO(Board board) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.createdAt = DateToForm.dateToFrom(board); } } @Data public static class ReadDTO { private int id; private String title; public ReadDTO(Board board) { this.id = board.getId(); this.title = board.getTitle(); } } }

4. BoardRepository

package com.example.blog.board; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @RequiredArgsConstructor @Repository public class BoardRepository { private final EntityManager em; // update 메서드 삭제 // public void update(int id, String title, String content) public void delete(int id) { em.createQuery("delete from Board b where id = :id") .setParameter("id", id) // :id가 여기에 mapping .executeUpdate(); } public void save(Board board) { em.persist(board); } public List<Board> findAll() { return em.createQuery("select b from Board b order by b.id desc", Board.class) .getResultList(); } public Optional<Board> findById(int id) { Board board = em.find(Board.class, id); return Optional.ofNullable(board); } }
  1. update
      • 더티 체킹을 이용하기 위해 Board에 update 메서드를 작성합니다.
      💡
      더티 체킹은 JPA에서 엔티티의 상태 변화를 자동으로 감지하여, 데이터베이스에 필요한 변경 사항을 반영하는 메커니즘입니다.
  1. delete
      • JPQL을 활용하여 delete 쿼리를 생성합니다.
      💡
      JPQL는 자바의 JPA에서 사용되는 쿼리 언어로, 관계형 데이터베이스와 객체 지향적으로 상호 작용할 수 있게 해줍니다.
  1. save
      • JPQL은 INSERT문은 지원하지 않기 때문에 Entity Manager를 활용하여 Board객체(엔티티)를 영속성 컨텍스트에 저장합니다.
      • persist 메서드는 엔티티가 아직 데이터베이스에 존재하지 않는 경우 새로운 레코드로 삽입하기 위해 INSERT SQL 문을 생성합니다.
      • 트랜잭션이 커밋될 때, JPA는 영속성 컨텍스트에 있는 모든 엔티티의 변경 사항을 데이터베이스에 반영합니다.
  1. findAll (모든 게시글 조회)
      • JPQL을 사용하여 모든 Board 엔티티를 조회해서 List로 반환합니다.
  1. findById (상세 게시글 보기)
      • em.find(Board.class, id)를 사용하여 엔티티를 조회합니다.
      • NullPointerException 처리를 위해 조회 결과를 Optional로 감싸서 반환합니다.
      💡
      persist 메서드를 사용하지 않았더라도, find 메서드를 사용하여 데이터베이스에서 엔티티를 조회하면, 해당 엔티티는 자동으로 영속성 컨텍스트에 포함됩니다.

5. BoardService

package com.example.blog.board; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @RequiredArgsConstructor @Service public class BoardService { private final BoardRepository boardRepository; public List<BoardResponse.ReadDTO> 게시글목록보기() { return boardRepository.findAll().stream() .map(BoardResponse.ReadDTO::new) .toList(); } public BoardResponse.UpdateFormDTO 게시글수정화면보기(int id) { Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("해당 id의 게시글이 없습니다 : " + id)); return new BoardResponse.UpdateFormDTO(board); } public BoardResponse.DetailDTO 게시글상세보기(int id) { Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("해당 id의 게시글이 없습니다 : " + id)); return new BoardResponse.DetailDTO(board); } @Transactional public void 게시글쓰기(BoardRequest.SaveDTO saveDTO) { boardRepository.save(saveDTO.toEntity()); } @Transactional public void 게시글삭제(int id) { boardRepository.delete(id); } @Transactional public void 게시글수정하기(int id, BoardRequest.UpdateDTO updateDTO) { // 1. 조회 Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("해당 id의 게시글이 없습니다 : " + id)); // 2. 업데이트 board.update(updateDTO.getTitle(), updateDTO.getContent()); } }
  1. 게시글목록보기
      • map(BoardResponse.ReadDTO::new)
        • map(board -> new BoardResponse.ReadDTO(board))와 같은 의미입니다.
  1. 게시글수정화면보기, 게시글상세보기
      • Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("해당 id의 게시글이 없습니다 : " + id));
        • NullPointerException 발생 시 RuntimeException을 발생시킵니다.
  1. 게시글쓰기
    1. @Data public static class SaveDTO { private String title; private String content; public Board toEntity() { Board board = new Board(null, title, content, null); return board; }
      • 요청에서 받는 SaveDTO를 Board 엔티티로 변화시킵니다.
      • persist 메서드는 JPA에서 엔티티를 영속성 컨텍스트에 추가하고, 엔티티가 데이터베이스에 존재하지 않는 경우 새로운 레코드로 삽입하는 역할을 합니다.
  1. 게시글수정하기
      • 조회를 통해 해당 Board 엔티티를 찾고 영속성 컨텍스트에 추가합니다.
      • board.update를 통해 Board 엔티티의 내용을 갱신합니다.
      • 영속 상태의 엔티티는 이후 변경된 속성이 자동으로 감지됩니다. (더티 체킹)
        • 트랜잭션이 커밋될 때, 영속성 컨텍스트 내의 엔티티 상태가 데이터베이스와 비교됩니다.
        • board 엔티티의 변경 사항이 감지되면, JPA는 자동으로 UPDATE 쿼리를 생성하고 실행하여 변경 사항을 데이터베이스에 반영합니다.

6. BoardController

package com.example.blog.board; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { private final BoardService boardService; // 글 업데이트 @PostMapping("/board/{id}/update-form") public String updateBoard(@PathVariable("id") int id, BoardRequest.UpdateDTO updateDTO) { boardService.게시글수정하기(id, updateDTO); return "redirect:/board/" + id; // 상세 글 보기로 이동 } // 업데이트 폼으로 이동 @GetMapping("/board/{id}/update-form") public String updateForm(@PathVariable("id") int id, Model model) { BoardResponse.UpdateFormDTO updateFormDTO = boardService.게시글수정화면보기(id); model.addAttribute("model", updateFormDTO); return "update-form"; } // 글 삭제 @PostMapping("/board/{id}/delete") public String delete(@PathVariable int id) { boardService.게시글삭제(id); return "redirect:/"; } // 글 작성 폼으로 이동 @GetMapping("/board/save-form") public String saveForm() { return "save-form"; } // 글 작성 @PostMapping("/board/save-form") public String saveV2(BoardRequest.SaveDTO saveDTO) { boardService.게시글쓰기(saveDTO); return "redirect:/"; } // 메인화면이자 전체 글 조회 @GetMapping("/") public String list(Model model) { List<BoardResponse.ReadDTO> boardList = boardService.게시글목록보기(); model.addAttribute("models", boardList); return "list"; } @GetMapping("/board/{id}") public String detail(@PathVariable("id") int id, Model model) { BoardResponse.DetailDTO boardDetail = boardService.게시글상세보기(id); model.addAttribute("model", boardDetail); return "detail"; } }
  • 컨트롤러는 변화가 없습니다.
Share article

devleekangho