최원종의 개발 블로그

V7-4 댓글 삭제 기능 추가 본문

Spring boot 입문

V7-4 댓글 삭제 기능 추가

chl6698 2026. 5. 21. 11:26

https://github.com/User20202373/spring_blog

 

GitHub - User20202373/spring_blog

Contribute to User20202373/spring_blog development by creating an account on GitHub.

github.com


 


board/ detail.mustache 코드 추가

{{#isOwner}}
    <form action="/reply/{{id}}/delete" method="post">
        <input type="hidden" name="boardId" value="{{board.id}}" >
        <button type="submit" class="btn btn-danger btn-sm">🗑️ 삭제</button>
    </form>
{{/isOwner}}

ReplyService 코드 요약

Stream API 기반 리팩토링 : 기존의 명령형 for 루프 스타일을 선언형 Stream API(.map, .toList)로 
전환하여 코드 가독성 향상

화면 맞춤 DTO 변환 및 상태 전달 : Entity 리스트를 뷰 레이어에 최적화된 ListDTO로 변환하는 과정에서
sessionUserId를 주입해 본인 글 여부 판별 기반 마련

댓글 삭제(DELETE) 비즈니스 로직 추가 : findById와 orElseThrow 조합으로
대상 댓글의 존재 여부를 선제적으로 검증 후 삭제 프로세스 수행

철저한 인가(Authorization) 방어선 구축 : 댓글 작성자 ID와 로그인한 세션 유저 ID를 비교하여
일치하지 않을 시 Exception403(Forbidden) 예외 차단

 

ReplyService 코드 리팩토링(댓글삭제 추가)⬇️

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

import com.tenco.blog._core.errors.Exception403;
import com.tenco.blog._core.errors.Exception404;
import com.tenco.blog.board.Board;
import com.tenco.blog.board.BoardRepository;
import com.tenco.blog.user.User;
import com.tenco.blog.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

// Service 계층에서는 여러 Repository를 조합해서 비즈니스 규칙을 완성한다
// 즉, 서비스 계층이 필요한 이유 중 하나이다.
@Service // IoC
@RequiredArgsConstructor // DI 처리
public class ReplyService {

    private final ReplyRepository replyRepository;
    private final BoardRepository boardRepository;
    private final UserRepository userRepository;

    // 댓글 목록 조회
    public List<ReplyResponse.ListDTO> 댓글목록조회(Integer boardId, Integer sessionUserId) {

        List<Reply> replyList = replyRepository.findByBoardIdWithUser(boardId);

//        List<ReplyResponse.ListDTO> listDtoList = new ArrayList<>();
//        for(int i =0; i < replyList.size(); i++) {
//            Reply tempReply = replyList.get(i);
//            // 객체 생성을 위해 필요한 데이터
//            ReplyResponse.ListDTO listDTO = new ReplyResponse.ListDTO(tempReply, sessionUserId);
//            listDtoList.add(listDTO);
//        }
        // Stream API 활용
        return replyList.stream()
                .map(reply -> new ReplyResponse.ListDTO(reply, sessionUserId))
                .toList();
    }


    @Transactional
    public Reply 댓글작성(ReplyRequest.SaveDTO saveDTO, Integer id) {
        // 게시글 조회
        Board boardEntity = boardRepository.findById(saveDTO.getBoardId()).orElseThrow(
                () -> new Exception404("해당 게시글을 찾을 수 없습니다"));

        // id 로 사용자 조회
        User userEntity = userRepository.findById(id).orElseThrow(
                () -> new Exception404("사용자를 찾을 수 없습니다")
        );
        Reply reply = saveDTO.toEntity(userEntity, boardEntity);
        replyRepository.save(reply);
        return reply;
    }

    @Transactional
    public void 댓글삭제(Integer replyId, Integer sessionUserId) {
        Reply replyEntity = replyRepository.findById(replyId).orElseThrow(
                () -> new Exception404("해당 댓글을 찾을 수 없습니다"));

        // 인가 처리
        if(replyEntity.getUser().getId() != sessionUserId) {
            throw new Exception403("댓글 삭제 권한이 없습니다");
        }
        // 댓글 삭제
        replyRepository.delete(replyEntity);
    }
}

BoardService (기존에 작성된 댓글부터 전체 삭제 기능)

 /**
     * 게시글 삭제 요청
     * @param id (Board PK)
     * @param sessionUser
     */
    @Transactional
    public void 게시글삭제(Integer id, User sessionUser) {
        log.info("게시글 삭제 서비스");
        Board boardEntity = boardRepository.findById(id).orElseThrow(
                () -> new Exception404("게시글을 찾을 수 없습니다")
        );
        boardEntity.isOwner(sessionUser.getId());

        // 기존에 작성된 댓글 부터 전체 삭제
        // 게시글 삭제 요청시 해당 게시글에 관련된 댓글 삭제는 어떻게? 만들 수 있음?
        replyRepository.deleteByBoardId(boardEntity.getId());

        boardRepository.deleteById(id);
        log.info("게시글 삭제 완료 - ID : {}", id);
    }

ReplyRepository

 

코드 요약 - @Modifying (반드시 기억)

벌크 삭제(Bulk Delete) 전용 쿼리 구축 : 특정 게시글 ID(boardId)에 종속된 모든 댓글을
한방에 날리는 데이터 정제용 JPQL 설계

@Query 기반 쓰기 연산 선언 : 표준 CRUD를 넘어 직접 커스텀 JPQL을 작성하여
조건 기반 대량 데이터 삭제 환경 마련

@Modifying 어노테이션 필수 적용 : JPA에게 해당 쿼리가 SELECT가 아닌 
데이터 변경(INSERT, UPDATE, DELETE) 작업임을 명시

데이터 무결성 방어 : 게시글 삭제 시 외래키(FK) 제약 조건으로 인한 오류를 방지하는
연쇄 삭제(Cascade) 처리의 기반 마련

ReplyRepository 코드(@Modifying 적용한 쿼리문 추가)

package com.tenco.blog.reply;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

// @Repository - 부모의 클래스에 정의 되어 있음()
public interface ReplyRepository extends JpaRepository<Reply, Integer> {

    // 1. 등록 및 수정 save(Reply entity)
    // 2. 단건 조회 : findById(Integer id)
    // 3. 전체 조회 : findAll()
    // 4. 삭제 : deleteById(Integer id) - reply
    // 5. 데이터 개수: count()
    // 6. 존재 여부 확인: existsById(Integer id)

		// ... 생략 

    /**
     * 이전 수정,삭제 기능에서는 수정은 더티 체킹으로 처리를 하였고 삭제는 기본적으로
     * 제공하는 em.remove() 메서드를 사용해서 처리 했었다. 지금은 직접 JQPL 쿼리를
     * 선언해서 DELETE 처리하는 구문이라 다른 상황이다.
     * @Query(...) <- JPA 기본적으로 SELECT 쿼리로만 인식을 하기 때문에
     * INSERT, UPDATE, DELETE 는 JPA 에게 SELECT 쿼리가 아니야 라고 알려줘야 제대로 동작
     * 그 어노테이션이 @Modifying 이다 !! 반드시 기억 !!!!
     */
    @Modifying
    @Query("DELETE FROM Reply r WHERE r.board.id = :boardId")
    void deleteByBoardId(@Param("boardId") Integer boardId);

}