Spring boot
버전 2 -4.게시글 수정하기 V2 - Persistence Context와 Dirty Checking 활용
mynote6676
2025. 6. 24. 17:48
| 도전 과제
게시글 수정하기 화면을 연결해보세요
{{> layout/header}}
<div class="container p-5">
<div class="card">
<div class="card-header"><b>글수정하기 화면입니다</b></div>
<div class="card-body">
<form action="/board/{{board.id}}/update-form" method="post">
<div class="mb-3">
<input type="text" class="form-control"
placeholder="Enter username" name="username" value="{{board.username}}" >
</div>
<div class="mb-3">
<input type="text" class="form-control"
placeholder="Enter title" name="title" value="{{board.title}}">
</div>
<div class="mb-3">
<textarea name="content" cols="" rows="5" class="form-control">
{{board.content}}
</textarea>
</div>
<button class="btn btn-primary form-control">글수정하기완료</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
BoardRequest 에 UpdateDTO 추가
package com.tenco.blog.board;
import lombok.Data;
/**
* 클라이언트에게 넘어온 데이터를
* Object로 변화해서 전달하는 DTO 역할을 담당한다
*/
public class BoardRequest {
// 정적 내부 클래스로 기능별로 DTO 관리
// 게시글 저장 요청 데이터
// BoardRequest.SaveDTO 변수명
@Data
public static class SaveDTO {
private String title;
private String content;
private String username;
// DTO에서 Entity로 변환하는 메서드를 만들기
// 계층간 데이터 변환을 명확하게 분리 하기 위함
public Board toEntity() {
return new Board(title, content, username);
}
} // end of SaveDTO
// 게시글 수정용 DTO 추가
@Data
public static class UpdateDTO {
private String title;
private String content;
private String username;
// 검증 메서드 (유효성 검사 기능을 추가)
public void validate() throws IllegalAccessException {
if(title == null || title.trim().isEmpty()) {
throw new IllegalAccessException("제목은 필수 입니다");
}
if(content == null || content.trim().isEmpty()) {
throw new IllegalAccessException("내용은 필수 입니다");
}
}
}
}
| 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;
/**
* Get 맵핑
* 주소 설계 : http://localhost:8080/board/{id}/update-form
* @return update-form.mustache
* @param : id (board pk)
*/
@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:/";
}
}
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;
// 게시글 수정하기 (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;
}
}
Dirty Checking의 핵심 개념
변경 감지 메커니즘
@Transactional
public void updateExample() {
// 1. 영속 상태로 조회 (스냅샷 생성)
Board board = em.find(Board.class, 1L);
// 영속성 컨텍스트가 엔티티의 현재 상태를 스냅샷으로 보관
// 2. 엔티티 값 변경
board.setTitle("새로운 제목"); // 필드 값 변경
// 3. 트랜잭션 커밋 시점에 자동 감지
// - 현재 엔티티 상태와 스냅샷 비교
// - 변경된 필드만 UPDATE 쿼리 생성
// - UPDATE board_tb SET title=? WHERE id=?
}
V1 vs V2 비교
V1 : 수동 UPDATE 쿼리
// v1: 직접 SQL 작성
@Transactional
public void updateById(int id, String title, String content) {
Query query = em.createNativeQuery(
"UPDATE board_tb SET title=?, content=? WHERE id=? "
);
query.setParameter(1, title);
query.setParameter(2, content);
query.setParameter(3, id);
query.executeUpdate();
}
V2: Dirty Checking
V2: 영속성 컨텍스트 활용ㅇ
@Transactional
public void updateById(Long id, BoardRequest.UpdateDTO reqDTO) {
Board board = em.find(Board.class, id); // 영속 상태로 조회
board.update(reqDTO);
// UPDATE 쿼리 자동 생성 및 실행!
}
728x90