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);
}
}
- update
- 더티 체킹을 이용하기 위해 Board에 update 메서드를 작성합니다.
더티 체킹은 JPA에서 엔티티의 상태 변화를 자동으로 감지하여, 데이터베이스에 필요한 변경 사항을 반영하는 메커니즘입니다.
- delete
- JPQL을 활용하여 delete 쿼리를 생성합니다.
JPQL는 자바의 JPA에서 사용되는 쿼리 언어로, 관계형 데이터베이스와 객체 지향적으로 상호 작용할 수 있게 해줍니다.
- save
- JPQL은 INSERT문은 지원하지 않기 때문에 Entity Manager를 활용하여 Board객체(엔티티)를 영속성 컨텍스트에 저장합니다.
- persist 메서드는 엔티티가 아직 데이터베이스에 존재하지 않는 경우 새로운 레코드로 삽입하기 위해 INSERT SQL 문을 생성합니다.
- 트랜잭션이 커밋될 때, JPA는 영속성 컨텍스트에 있는 모든 엔티티의 변경 사항을 데이터베이스에 반영합니다.
- findAll (모든 게시글 조회)
- JPQL을 사용하여 모든 Board 엔티티를 조회해서 List로 반환합니다.
- 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());
}
}
- 게시글목록보기
- map(BoardResponse.ReadDTO::new)
- map(board -> new BoardResponse.ReadDTO(board))와 같은 의미입니다.
- 게시글수정화면보기, 게시글상세보기
- Board board = boardRepository.findById(id) .orElseThrow(() -> new RuntimeException("해당 id의 게시글이 없습니다 : " + id));
- NullPointerException 발생 시 RuntimeException을 발생시킵니다.
- 게시글쓰기
- 요청에서 받는 SaveDTO를 Board 엔티티로 변화시킵니다.
- persist 메서드는 JPA에서 엔티티를 영속성 컨텍스트에 추가하고, 엔티티가 데이터베이스에 존재하지 않는 경우 새로운 레코드로 삽입하는 역할을 합니다.
@Data
public static class SaveDTO {
private String title;
private String content;
public Board toEntity() {
Board board = new Board(null, title, content, null);
return board;
}
- 게시글수정하기
- 조회를 통해 해당 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