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