최원종의 개발 블로그

V3-9 게시글 수정(Dirty Checking과 권한 관리) 본문

Spring boot 입문

V3-9 게시글 수정(Dirty Checking과 권한 관리)

chl6698 2026. 5. 13. 17:54

board/update-form.mustache파일

{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <div class="card">
        <div class="card-header"><b>글수정 화면입니다</b></div>
        <div class="card-body">
            <!--  익명 게시글 작성    -->
            <form action="/board/{{id}}/update" method="post">
                <div class="mb-3">
                    <input type="text" name="username"  class="form-control"
                           placeholder="enter username" value="{{board.user.username}}" readonly>
                </div>
                <div class="mb-3">
                    <input type="text" name="title" class="form-control"
                           placeholder="enter title" value="{{board.title}}">
                </div>
                <div class="mb-3">
                    <textarea type="text" name="content" rows="5" class="form-control" placeholder="enter title">
                        {{board.content}}
                    </textarea>
                </div>
                <button class="btn btn-primary">글수정 완료</button>
            </form>
        </div>
    </div>
</div>

{{> layout/footer}}

BoardRequest 핵심 메서드 분석

1. UpdateDTO.validate() (수정 로직의 첫 관문)
목적: 사용자가 수정한 데이터가 올바른 형식인지 검사함.

로직: 제목(title): 공백을 제외하고 반드시 존재해야 함.
      내용(content): 최소 3글자 이상이라는 비즈니스 규칙을 적용.

효과: 서버 리소스를 낭비하기 전, 잘못된 수정 요청을 입구에서 차단함.

2. SaveDTO.toEntity(User user) (객체 생성의 일관성)
목적: 전송 받은 데이터와 세션의 사용자 정보를 조합하여 새로운 게시글 엔티티를 생성함.

기술: 빌더 패턴(@Builder)을 사용하여 필드 주입 순서에 상관없이 명확하게 객체를 생성하며,
      연관 관계인 User 객체를 매개변수로 받아 데이터 무결성을 지킴.

BoardRequest 코드

package com.tenco.blog.board;

import com.tenco.blog.user.User;
import lombok.Builder;
import lombok.Data;

// 요청 데이터를 담는 DTO 클래스
// 컨트롤러.. 비즈니스 .. 데이터 계층 사이에서 데이터 전송 역할 객체
public class BoardRequest {

    @Data
    @Builder
    public static class SaveDTO {

        private String title;
        private String content;

        // 편의 기능 설계 가능
        // DTO 에서 Entity로 변환해주는 편의 메서드
        public Board toEntity(User user) {
            return Board.builder()
                    .title(title)
                    .user(user)
                    .content(content)
                    .build();
        }

        public void validate() {
            if(title == null || title.trim().isEmpty()) {
                throw new IllegalArgumentException("제목은 필수입니다");
            }
            if(content == null || content.length() < 3) {
                throw new IllegalArgumentException("내용은 3글자 이상 작성해야 합니다.");
            }
        }

    }

    // 내부 정적 클래스 게시글 수정 DTO 설계
    @Data
    public static class UpdateDTO {
        private String title;
        private String content;

        // 게시글 수정시 유효성 검사 편의 메서드
        public void validate() {
            if(title == null || title.trim().isEmpty()) {
                throw new IllegalArgumentException("제목은 필수입니다");
            }
            if(content== null || content.length() < 3) {
                throw new IllegalArgumentException("내용은 3글자 이상 작성해야 합니다.");
            }
        }

    }

}

게시글 수정 화면 요청(BoardController) 

 

코드 수정 및 추가된 부분

2단계 보안 적용: 단순한 로그인 체크(인증)를 넘어, 해당 글의 주인인지 확인하는 인가 처리가 추가됨.

에러 핸들링: 권한이 없을 경우 RuntimeException을 던져 비정상적인 접근을 강력하게 차단함.

데이터 바인딩: 수정할 기존 데이터를 Model에 담아 뷰(update-form)로 전달하여 사용자 편의성을 높임.

 

핵심 메서드 분석

1. 인증 처리 (Authentication):
세션에서 sessionUser를 꺼내 로그인 상태인지 확인함.
로그인하지 않았다면 로그인 페이지로 보냄.

2. 게시글 조회: URL 경로에 포함된 {id}를 사용해 수정할 게시글을 DB에서 가져옴.

3. 인가 처리 (Authorization): "세션의 사용자 ID"와 "게시글 작성자의 ID"를 대조함.
일치하지 않으면 수정 화면 자체를 보여주지 않고 예외를 발생시킴.

4. 모델 전달: 모든 검증을 통과하면 board 객체를 모델에 담아, 
수정 폼에서 기존 제목과 내용을 미리 보여줄 수 있게 함.

BoardController 코드

    // http://localhost:8080/board/1/update-form
    // 게시글 수정 화면 요청
    @GetMapping("/board/{id}/update-form")
    public String updateFormPage(@PathVariable(name = "id") Integer id, Model model, HttpSession session) {

        // 인증 처리
        User sessionUser = (User) session.getAttribute("sessionUser");
        if(sessionUser == null) {
            return "redirect:/login-form";
        }

        // 인가 처리
        Board board = boardPersistRepository.findById(id);
        if(sessionUser.getId() != board.getUser().getId()) {
            throw new RuntimeException("수정 권한이 없습니다");
        }

        model.addAttribute("board", board);
        return "board/update-form";
    }

게시글 수정 기능 요청

 

 핵심 메서드 분석

updateProc (POST)
인증 검사 (Authentication): 세션에서 사용자를 확인하여 비로그인 상태면 로그인 페이지로 보냄.

데이터 검증 (validate): updateDTO.validate()를 호출하여 빈 값이나 
짧은 글 등 부적절한 데이터 전송을 막음.

인가 처리 (Authorization): DB에서 해당 글을 조회한 후, 
sessionUser.getId() != board.getUser().getId() 조건을 통해 
본인의 글이 아니면 수정 권한 없음 예외를 던짐.

수정 실행 (updateById): 모든 검증을 통과하면 실제 영속성 계층에 수정을 요청함.

결과 응답: 성공 시 해당 게시글의 상세 페이지(/board/{id})로 리다이렉트하여 
수정된 내용을 바로 확인할 수 있게 함.

게시글 수정 기능 요청(BoardController) 코드

    // /board/{id}/update
    @PostMapping("/board/{id}/update")
    // 메세지 컨버터란 객체가 동작해서 자동으로 객체를 생성하고 값을 매핑해 준다.
    public String updateProc(@PathVariable(name = "id") Integer id,
                             BoardRequest.UpdateDTO updateDTO, HttpSession session) {

        // 인증 검사
        User sessionUser =  (User) session.getAttribute("sessionUser");
        if(sessionUser == null) {
            return "redirect:/login-form";
        }
        try  {
            // 유효성 검사
            updateDTO.validate();
            // 인가 검사
            Board board =  boardPersistRepository.findById(id);
            if (sessionUser.getId() != board.getUser().getId()) {
                throw new RuntimeException("수정할 권한이 없습니다");
            }
            boardPersistRepository.updateById(id, updateDTO);
        } catch (Exception e) {
            // /board/{id}/update-form
           return "redirect:/board/" + id + "/update-form";
        }
        return "redirect:/board/" + id;
    }

주요 기술 포인트 요약

메시지 컨버터: 폼 데이터를 자바 객체(UpdateDTO)로 자동 변환 및 매핑하는 기술
Fail-Fast 전략: validate()를 가장 먼저 수행하여 잘못된 요청을 빠르게 거절함
이중 보안 체크: GET(화면 요청)과 POST(실제 수정) 양쪽에서 인가 처리를 수행함
Redirect 전략: 성공/실패 시 적절한 URL로 이동시켜 중복 전송 및 UX 단절을 방지

 


전체 코드 - BoardController

더보기
package com.tenco.blog.board;


import com.tenco.blog.user.User;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Slf4j
@Controller // IoC
@RequiredArgsConstructor // DI
public class BoardController {
    // DI
    private final BoardPersistRepository boardPersistRepository;

    /**
     * 게시글 작성 화면 요청
     * @return 페이지 반환
     * 주소설계 : http://localhost:8080/board/save-form
     */
    @GetMapping("/board/save-form")
    public String saveForm(HttpSession httpSession) {
       // 로그인 여부 체크 - 즉 로그인 한 사용자만 이 페이지 안에 들어 올 수 있음.
       // 1. 인증 검사
       User sessionUser =  (User)httpSession.getAttribute("sessionUser");
       if(sessionUser == null) {
           return "redirect:/login-form";
       }

        return "board/save-form";
    }

    /**
     * 게시글 작성 기능 요청
     * @return 페이지 반환
     * 주소설계 : http://localhost:8080/board/save-form
     */
    @PostMapping("/board/save")
    // 사용자 요청 -> HTTP 요청 메시지(Post)
    public String saveProc(BoardRequest.SaveDTO saveDTO, HttpSession session) {

        log.info("=== 게시글 저장 요청 ===");
        // 이 요청 시 사용자가 로그인을 했다면 로그인 정보를 세션 메모리에서 가져오면 된다.
        // 1. 세션에서 로그인한 사용자 정보 가져오기
        User sessionUser = (User) session.getAttribute("sessionUser");
        // 2. 로그인 여부 확인
        if(sessionUser == null) {
            return "redirect:/login-form";
        }

        try {
            // 3. 로그인 된 사용자
            // 3.1 유효성 검사
            saveDTO.validate();
            Board board = saveDTO.toEntity(sessionUser);
            boardPersistRepository.save(board);
            return "redirect:/";
        } catch (Exception e) {
            System.out.println("에러 발생 : " + e.getMessage());
            return "board/save-form";
        }

    }


    /**
     * 게시글 목록 화면 요청
     * 주소설계 : http://localhost:8080/
     */
    @GetMapping({"/", "index"})
    public String list(Model model) {
        List<Board> boardList = boardPersistRepository.findAll();
        model.addAttribute("boardList", boardList);
        return "board/list";
    }


    // 게시글 상세보기 화면 요청
    // http://localhost:8080/board/1
    @GetMapping("/board/{id}")
    public String detailPage(@PathVariable(name = "id") Integer id, Model model) {

        Board board = boardPersistRepository.findById(id);
        // board는 연관관계가 User 엔티티와 ManyToOne 관계 설정이 되어 있다.
        // 직접 쿼리구문을 작성하지 않을 때 즉, 엔티티 매니저의 메서드로 객체를 조회시
        // 자동으로 JOIN 구문을 호출해 준다.
        // 단 Fatch 전략에 따라 EAGER, LAZY 전략에 따라 한번에 다 조인해서 가져오거나
        // 필요할 때 한번 더 요청하는것이 LAYZY 전략이다.
        // 코드상에서 User 에 정보를 요구 (현재 LAYZY 전략)
        // System.out.println(board.getUser().getUsername());


        model.addAttribute("board", board);
        return "board/detail";
    }


    // 삭제 기능 요청
    // 1. 로그인 여부 확인
    // 2. 삭제할 게시글이 본인이 작성한 게시글인지 확인 (권한 확인, 인가 처리)
    // 3. 인가 처리 후 삭제 진행
    @PostMapping("/board/{id}/delete")
    public String deleteProc(@PathVariable(name = "id") Integer id, HttpSession session) {
        log.info("===  게시글 삭제 요청 ===");
        // 인증 검사
        User sessionUser =  (User) session.getAttribute("sessionUser");
        if(sessionUser == null) {
            return "redirect:/login-form";
        }
        try {
            // 삭제할 게시글 조회 (권한 체크, 인가 처리)
            Board board = boardPersistRepository.findById(id);
            if(board.getUser().getId() == sessionUser.getId() ) {
                boardPersistRepository.deleteById(id);
            }
        } catch (Exception e) {
            return "redirect:/";
        }
        return "redirect:/";
    }


    // http://localhost:8080/board/1/update-form
    // 게시글 수정 화면 요청
    @GetMapping("/board/{id}/update-form")
    public String updateFormPage(@PathVariable(name = "id") Integer id, Model model, HttpSession session) {

        // 인증 처리
        User sessionUser = (User) session.getAttribute("sessionUser");
        if(sessionUser == null) {
            return "redirect:/login-form";
        }

        // 인가 처리
        Board board = boardPersistRepository.findById(id);
        if(sessionUser.getId() != board.getUser().getId()) {
            throw new RuntimeException("수정 권한이 없습니다");
        }

        model.addAttribute("board", board);
        return "board/update-form";
    }

    // /board/{id}/update
    @PostMapping("/board/{id}/update")
    // 메세지 컨버터란 객체가 동작해서 자동으로 객체를 생성하고 값을 매핑해 준다.
    public String updateProc(@PathVariable(name = "id") Integer id,
                             BoardRequest.UpdateDTO updateDTO, HttpSession session) {

        // 인증 검사
        User sessionUser =  (User) session.getAttribute("sessionUser");
        if(sessionUser == null) {
            return "redirect:/login-form";
        }
        try  {
            // 유효성 검사
            updateDTO.validate();
            // 인가 검사
            Board board =  boardPersistRepository.findById(id);
            if (sessionUser.getId() != board.getUser().getId()) {
                throw new RuntimeException("수정할 권한이 없습니다");
            }
            boardPersistRepository.updateById(id, updateDTO);
        } catch (Exception e) {
            // /board/{id}/update-form
           return "redirect:/board/" + id + "/update-form";
        }
        return "redirect:/board/" + id;
    }

}