Spring boot
버전-2 -5. 게시글 삭제하기 V2 - Persistence Context와 영속성 관리
mynote6676
2025. 6. 24. 19:50
영속성(Persistence)이란?
- 영속성은 데이터가 영구적으로 보관되는 성질을 의미합니다. 프로그램이 종료되어도 데이터가 사라지지 않고 계속 존재하는 특성입니다.
--------------------------------------------------------------------------------------------------------------------------------
- BoardController
package com.tenco.blog.board;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@RequiredArgsConstructor
@Controller // IoC 대상 - 싱글톤 패턴으로 관리 됨
public class BoardController {
// 생성자 의존 주의 - DI 처리
private final BoardPersistRepository br;
// 주소 설계 : /board/{{board.id}}/delete
@PostMapping("/board/{id}/delete")
public String delete(@PathVariable(name = "id") Long id) {
br.deleteById(id);
return "redirect:/";
}
/**
* Get 맵핑
* 주소 설계 : http://localhost:8080/board/{id}/update-form
*
* @param : id (board pk)
* @return update-form.mustache
*/
@GetMapping("/board/{id}/update-form")
public String updateForm(@PathVariable(name = "id") Long id, HttpServletRequest request) {
// select * from board_tb where id = 4;
Board board = br.findById(id);
// 머스태치 파일에 조회된 데이터를 바인딩 처리
request.setAttribute("board", board);
return "board/update-form";
}
@PostMapping("/board/{id}/update-form")
public String update(@PathVariable(name = "id") Long id,
BoardRequest.UpdateDTO reqDTO) {
// 트랜잭션
// 수정 -- select - 값을 확인해서 - 데이터를 수정 --> update
// JPA 영속성 컨텍스트 활용
br.update(id, reqDTO);
// 수정 전략을 더티 체킹을 활용
// 장점
// 1. UPDATE 쿼리 자동 생성
// 2. 변경된 필드만 업데이트 (성능 최적화)
// 3. 영속성 컨텍스트에 일관성 유지
// 4. 1차 캐시 자동 갱신 됨
// 성공 시 리스트 화면으로 리다이텍트 처리
return "redirect:/";
}
// 게시글 상세 보기
// 주소설계 GET : http://localhost:8080/board/3
@GetMapping("/board/{id}")
public String detail(@PathVariable(name = "id") Long id, HttpServletRequest request) {
Board board = br.findById(id);
request.setAttribute("board", board);
// prefix: classpath:/templates/
// return : board/detail
// suffix: .mustache
// 1차 캐시 효과 - DB에 접근하지 않고 바로 영속성 컨텍스트에서 꺼낸다.
// br.findById(id);
return "board/detail";
}
// 주소 설계 : http://localhost:8080/ , http://localhost:8080/index
@GetMapping({"/", "/index"})
public String boardList(HttpServletRequest request) {
List<Board> boardList = br.findAll();
request.setAttribute("boardList", boardList);
return "index";
}
// 게시글 작성 화면 요청 처리
@GetMapping("/board/save-form")
public String saveForm() {
return "board/save-form";
}
// 게시글 작성 액션(수행) 처리
@PostMapping("/board/save")
public String save(BoardRequest.SaveDTO reqDTO) {
// HTTP 요청 본문 : title=값&content=값&username=값
// form 태그의 MIME 타입 ( application/x-www-form-urlencoded)
// reqDTO <-- 사용자가 던지 데이터 상태가 있음
// DTO -- Board -- DB
//Board board = new Board(reqDTO.getTitle(), reqDTO.getContent(), reqDTO.getUsername());
Board board = reqDTO.toEntity();
br.save(board);
return "redirect:/";
}
}
BoardPersistRepository
package com.tenco.blog.board;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@RequiredArgsConstructor // 필수 멤버변수를 확인해서 생성자에 등록해줌
@Repository // Ioc 대상이 된다 - 싱글톤 패턴으로 관리 됨
public class BoardPersistRepository {
// JPA 핵심 인터페이스
// @Autowired final 를 사용시 사용 불가
private final EntityManager em;
// 게시글 삭제하기 (영속성 컨텍스트를 활용)
@Transactional
public void deleteById(Long id) {
// 1. 먼저 삭제할 엔티티를 영속 상태로 조회
Board board = em.find(Board.class, id);
// 영속 상태의 엔티티를 삭제 상태로 변경
em.remove(board);
// 트랜잭션이 커밋 되는 순간 삭제처리
// 삭제 과정
// 1. Board 엔티티가 영속 상태에서 remove() 호출시 삭제 상태로 변경
// 2. 1차 캐쉬에서 해당 엔티티를 제거
// 3. 트랜잭션 커밋 시점에 DELETE SQL 자동 실행
// 4. 연관관계 처리 자동 수행 (cascade 설정 시)
}
// 게시글 수정하기 (DB 접근 계층)
@Transactional
public void update(Long boardId, BoardRequest.UpdateDTO updateDTO) {
Board board = findById(boardId);
// board -> 영속성 컨텍트 1차 캐쉬에 key=value 값이 저장 되어 있다.
board.setTitle(updateDTO.getTitle());
board.setContent(updateDTO.getContent());
board.setUsername(updateDTO.getUsername());
// 트랜잭션 끝나면 영속성 컨텍스트에서 변경 감지를 한다.
// 변경 감지(Dirty Checking)
// 1. 영속성 컨텍스트가 엔티티 최초 상태를 스냅샷으로 보관
// 2. 필드 값 변경 시 현재 상태와 스냅샷 비교
// 3. 트랜잭션 커밋 시점에 변경된 필드만 UPDATE 쿼리르 자동 생성
// 4. Update board_tb set title=?, content=?, username=? where id =?
}
// 게시글 한건 조회 쿼리 만들기
// em.find(), JPQL, 네이티브 쿼리
public Board findById(Long id) {
// 1차 캐시 활용
return em.find(Board.class, id);
}
// JQPL를 사용환 조회 방법 (비교용 - 실제로는 find() 권장)
public Board findByIdWithJPQL(Long id) {
// 네임드 파라미터 권장 사용
String jpql = " SELECT b FROM Board b WHERE b.id = :id ";
// Query query = em.createQuery(jpql, Board.class);
// query.setParameter("id", id);
// Board board = (Board) query.getSingleResult();
try {
return em.createQuery(jpql, Board.class)
.setParameter("id", id)
.getSingleResult(); // 주의점 : 결과 없으면 NoResultException 발생
} catch (Exception e) {
return null;
}
// JPQL 단점 :
// 1. 1차 캐시 우회하여 항상 DB 접근
// 2. 코드가 복잡하게 나올 수 있음
// 3. getSingleResult() 호출 <-- 예외 처리 해주어야 함
}
// JPQL을 사용한 게시글 목록 조회
public List<Board> findAll() {
// JPQL : 엔티티 객체를 대상으로 하는 객체지향 쿼리
// Board는 엔티티 클르명, b는 별칭
String jpql = " SELECT b FROM Board b ORDER BY b.id DESC ";
// v1
//em.createNativeQuery()
// Query query = em.createQuery(jpql, Board.class);
// List<Board> boardList = query.getResultList();
// return boardList;
return em.createQuery(jpql, Board.class).getResultList();
}
// 게시글 저장 기능 - 영속성 컨텍스트 활용
@Transactional
public Board save(Board board) {
// v1 -> 네이트브 쿼리를 활용 했다.
// 1. 매개변수로 받은 board는 현재 비영속 상태이다.
// - 아직 영속성 컨텍스트에 관리 되지 않는 상태
// - 데이터베이스와 아직은 연관 없는 순수 Java 객체 상태
// 2. em.persist(board); - 이 엔티티를 영속성 컨텍스트에 저장하는 개념이다.
// - 영속성 컨텍스트가 board 객체를 관리하게 된다.
em.persist(board);
// 3. 트랜잭션 커밋 시점에 Insert 쿼리 실행
// - 이때 영속성 컨텍스트의 변경 사항이 자동으로 DB에 반영 됨
// - board 객체의 id 필드에 자동으로 생성된 값이 설정 됨
// insert ---> DB ---> (pk 값을 알 수 있다)
// select ---> DB ---> (할당된 pk 값 조회)
// 4. 영속 상태로 된 board 객체를 반환
// - 이 시점에는 자동으로 board id 멤버 변수에 db pk 값이 할당된 상태이다
return board;
}
}
BoardPersistRepositoryTest
@Test
public void deleteById_test() {
// given
Long id = 1L;
// when
// 삭제할 게시글이 실제로 존재하는지 확인
Board targetBoard = br.findById(id);
Assertions.assertThat(targetBoard).isNotNull();
// 영속성 컨텍스트에서 삭제 실행
br.deleteById(id);
// then
List<Board> afterDeleteBoardList = br.findAll();
Assertions.assertThat(afterDeleteBoardList.size()).isEqualTo(3);
}
viewResolver를 확인하고 SSR에 대한 개념을 다시 떠올려 보자.
뷰 리졸버는 컨트롤러가 반환한 논리적 뷰 이름 (예: "home")과 Model 데이터를 받아 실제 Mustache 템플릿( 예: home.mustache)로 매핑합니다.
viewResolver: Controller가 리턴한 "뷰 이름"을 → 실제 템플릿 파일 경로로 바꿔주는 역할을 하는 녀석
SSR = Server Side Rendering (서버 사이드 렌더링)
화면을 서버에서 미리 만들어서 클라이언트에게 HTML을 통째로 보내주는 방식
728x90