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