[스프링 부트] 34. 연관 관계가 있는 엔티티 삭제 구현 - 웹 게시판 v6

KangHo Lee's avatar
Dec 04, 2024
[스프링 부트] 34. 연관 관계가 있는 엔티티 삭제 구현 - 웹 게시판 v6
💡
Reply 엔티티와 Board 엔티티는 n : 1의 관계를 가지고 있습니다.

Reply 엔티티

@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Table(name = "reply_tb") @Entity public class Reply { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String comment; @ManyToOne(fetch = FetchType.LAZY) private User user; @ManyToOne(fetch = FetchType.LAZY) private Board board; @CreationTimestamp private Timestamp createdAt; @Builder public Reply(Integer id, String comment, User user, Board board, Timestamp createdAt) { this.id = id; this.comment = comment; this.user = user; this.board = board; this.createdAt = createdAt; } }

Board 엔티티

@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Table(name = "board_tb") @Entity public class Board { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String title; private String content; @ManyToOne(fetch = FetchType.LAZY) private User user; @OneToMany(mappedBy = "board", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) private List<Reply> replies; @CreationTimestamp private Timestamp createdAt; @Builder public Board(Integer id, String title, String content, User user, Timestamp createdAt) { this.id = id; this.title = title; this.content = content; this.user = user; this.createdAt = createdAt; } }

BoardResponseDTO

public class BoardResponse { @Data public static class DetailDTO { private int id; private String title; private String content; private String createdAt; private Integer userId; private String username; private boolean isOwner = false; private List<ReplyDTO> replies; @Data class ReplyDTO { private int id; private String comment; private int userId; private String username; private boolean isOwner = false; // 이너클래스라 변수 이름이 같아도 된다. public ReplyDTO(Reply reply, User sessionUser) { this.id = reply.getId(); this.comment = reply.getComment(); this.userId = reply.getUser().getId(); this.username = reply.getUser().getUsername(); if (sessionUser != null) { this.isOwner = sessionUser.getId().equals(reply.getUser().getId()); } } } public DetailDTO(Board board, User sessionUser) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.createdAt = MyDate.formatToStr(board.getCreatedAt()); this.userId = board.getUser().getId(); this.username = board.getUser().getUsername(); if (sessionUser != null) { this.isOwner = sessionUser.getUsername().equals(board.getUser().getUsername()); } this.replies = board.getReplies().stream().map(r -> new ReplyDTO(r, sessionUser)).toList(); } } }
ReplyDTO
if (sessionUser != null) { this.isOwner = sessionUser.getId().equals(reply.getUser().getId()); }
  • 세션에 저장된 User 정보와 Reply(댓글)를 작성한 User 정보를 비교합니다.
  • Integer 비교는 ‘==’ 이 아니라 .equals로 해야 합니다.
    • int와 달리 Integer는 객체의 참조를 비교합니다.
    • 값이 아닌 메모리 주소를 비교하므로, 동일한 값을 가진 다른 객체는 false를 반환할 수 있습니다.

BoardRepository

@RequiredArgsConstructor @Repository public class BoardRepository { private final EntityManager em; public void delete(int id) { em.createQuery("delete from Board b where id = :id") .setParameter("id", id) .executeUpdate(); } }

BoardService

@Transactional(readOnly = true) @RequiredArgsConstructor @Service public class BoardService { private final BoardRepository boardRepository; @Transactional public void 게시글삭제(int id, User sessionUser) { Board boardPS = boardRepository.findById(id) .orElseThrow(() -> new Exception404("게시글 아이디를 찾을 수 없습니다.")); if (!sessionUser.getId().equals(boardPS.getUser().getId())) { throw new Exception403("권한이 없습니다."); } boardRepository.delete(id); } }

BoardController

@RequiredArgsConstructor @Controller public class BoardController { private final BoardService boardService; private final HttpSession session; // 메인화면 이동 @GetMapping("/") public String list(Model model) { List<BoardResponse.DTO> boardList = boardService.게시글목록보기(); model.addAttribute("models", boardList); return "index"; } // 글 삭제 @PostMapping("/board/{id}/delete") public String delete(@PathVariable("id") int id) { User sessionUser = (User) session.getAttribute("sessionUser"); boardService.게시글삭제(id, sessionUser); return "redirect:/"; } }
💡
이렇게 작성할 경우 외래키 제약 조건 때문에 게시글 삭제가 되지 않습니다.

삭제 구현 방법

1. FK에 @OnDelete 설정 걸기

Reply.java
@OnDelete(action = OnDeleteAction.CASCADE) @ManyToOne(fetch = FetchType.LAZY) private Board board;
  • @OnDelete(action = OnDeleteAction.CASCADE)
    • 부모 엔티티(Board)가 삭제될 때 종속된 자식 엔티티(Reply)도 함께 삭제되도록 설정합니다.

2. Reply 삭제 메서드 추가

ReplyRepository.java
public void deleteAll(int boardId){ Query q = em.createQuery("delete from Reply r where r.board.id=:boardId"); q.setParameter("boardId", boardId); q.executeUpdate(); }

3. Reply의 외래키를 null로 설정

ReplyRepository.java
public void updateNull(int boardId){ Query q = em.createQuery("update Reply r set r.board.id=null where r.board.id=:boardId"); q.setParameter("boardId", boardId); q.executeUpdate(); }

4. 외래키를 설정하지만 제약 조건은 비활성화

Reply.java
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "board_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) // 외래 키 제약 조건 비활성화 private Board board;

5. Board를 논리 삭제

  • Board에 visible 컬럼을 추가하고 삭제 시 false로 update합니다.
 
Share article

devleekangho