[스프링부트] 익명 블로그 V6 - 연관 관계가 있는 엔티티 삭제 (데이터 삭제 방식의 종류)

익명 블로그 V6 - 4
이나겸's avatar
Dec 04, 2024
[스프링부트] 익명 블로그 V6 - 연관 관계가 있는 엔티티 삭제 (데이터 삭제 방식의 종류)

익명 블로그에서 연관 관계가 있는 엔티티 삭제 활용

⇒외래키 제약 조건 때문에 게시글 삭제 불가능한 상태에서 데이터 삭제 방식의 종류 활용

 

[Reply 클래스 - 엔티티]

  • Board 클래스와 Reply 클래스는 N : 1의 관계
@NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Table(name = "reply_tb") @Entity public class Reply { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String comment; @CreationTimestamp private Timestamp createdAt; @ManyToOne(fetch = FetchType.LAZY) // 유저1 : 댓글n private User user; // 댓글을 쓴 유저 @ManyToOne(fetch = FetchType.LAZY) // 보드1 : 댓글n private Board board; // 댓글이 달린 게시물 @Builder public Reply(Integer id, String comment, Timestamp createdAt, User user, Board board) { this.id = id; this.comment = comment; this.createdAt = createdAt; this.user = user; this.board = board; } }
 

[Board 클래스 - 엔티티]

  • Board 클래스와 Reply 클래스는 N : 1의 관계
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 외부에서 빈 생성자 생성하지 못하게 제한(protected) @Getter @Table(name = "board_tb") // 테이블 명 @Entity // Board 객체를 테이블로 생성하며, 데이터베이스 테이블과 매핑하여 데이터베이스 작업(insert, update, delete 등)을 수행 public class Board { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) // AutoIncrement private Integer id; private String title; private String content; // 연관 관계 설정 (User 쪽이 1이니까 ManyToOne) @ManyToOne(fetch = FetchType.LAZY) // LAZY와 EAGER일때 콘솔 출력되는 쿼리 다름 private User user; // mappedBy : fk의 변수명이 무엇인가? @OneToMany(mappedBy = "board", fetch = FetchType.EAGER) private List<Reply> replies = new ArrayList<>(); // 댓글들을 List로 만듦 @CreationTimestamp private Timestamp createdAt; // 풀 생성자 만들고 @Builder 달아서 빌더의 형태 잡음 @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; } // update(글 수정)를 setter로 만들어서 활용하기로 함 // setter는 명확한 의도가 있는것만 해놓는게 좋기 때문에 public void update(String title, String content) { this.title = title; this.content = content; } }
 

[BoardResponse 클래스 - ReplyDTO]

  • 세션에 저장된 User 정보와 댓글(Reply)을 작성한 User 정보 비교
  • Integer 비교는 ‘==’ 이 아니라 .equals
    • int와 달리 Integer은 객체의 참조 비교하기 때문
    • 값이 아닌 메모리 주소를 비교하므로, 동일한 값을 가진 다른 객체는 false반환 할 수 있음
// [ReplyDTO 로직의 일부] if (sessionUser != null) { // lazy loading this.isOwner = sessionUser.getId().equals(reply.getUser().getId()); }
public class BoardResponse { // 글 상세보기 @Data public static class DetailDTO { private int id; private String title; private String content; private String createdAt; private Integer userId; // 게시글을 작성한 userId private String username; private boolean isOwner = false; private List<ReplyDTO> replies; // 내부 DTO(Class)는 public 붙이지 않기 - 외부에서 모르고 꺼내쓸 수 있기때문 @Data class ReplyDTO { private int id; private String comment; private int userId; // 댓글 작성한 userId private String username; // 댓글 작성한 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) { // sessionUser는 권한 확인을 위해 this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.createdAt = Encoding.formatToStr(board); this.userId = board.getUser().getId(); this.username = board.getUser().getUsername(); if (sessionUser != null) { // lazy loading this.isOwner = sessionUser.getUsername().equals(board.getUser().getUsername()); } this.replies = board.getReplies().stream().map(r -> new ReplyDTO(r, sessionUser)).toList(); } } }
 

[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:/"; } }
 

[데이터 삭제 방법]

외래 키(FK)에 걸기

  • @OnDelete(action = OnDeleteAction.CASCADE)
    • FK에 @OnDelete 설정
    • 부모 엔티티(Board) 삭제 시 관련된 자식 엔티티(Reply)도 자동으로 삭제되도록 설정 ⇒ cascade 설정
@OnDelete(action = OnDeleteAction.CASCADE) @ManyToOne(fetch = FetchType.LAZY) private Board board;

게시글 삭제 시 댓글을 null로 업데이트

  • 부모 엔티티 삭제 시 자식 엔티티의 외래 키(FK)를 null로 설정하여 관계 해제
// 게시글 삭제 로직에서 댓글의 board를 null로 업데이트 public void deleteBoard(Board board) { List<Reply> replies = replyRepository.findByBoard(board); for (Reply reply : replies) { reply.setBoard(null); replyRepository.save(reply); } boardRepository.delete(board); }

게시글 삭제 시 댓글을 직접 삭제 (쿼리 튜닝)

  • 부모 엔티티 삭제 시 자식 엔티티를 직접 삭제하는 쿼리 실행
// 게시글 삭제 로직에서 댓글을 먼저 삭제 public void deleteBoard(Board board) { replyRepository.deleteByBoard(board); boardRepository.delete(board); }

제약 조건 해제 후 삭제

  • 외래 키 제약 조건을 해제하여 관계를 자유롭게 관리할 수 있도록 함
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @ManyToOne(fetch = FetchType.LAZY) private Board board;

논리적 삭제

  • 실무에서 주로 사용하는 방법 (가장 중요!!)
  • 데이터베이스에서 실제로 데이터를 삭제하지 않고, 특정 칼럼을 사용하여 삭제된 것처럼 처리
  • Board에 visible 컬럼 추가하고 삭제 시 false로 update
@ManyToOne(fetch = FetchType.LAZY) private Board board; @Column(nullable = false) private boolean visible = true; @Column(nullable = true) private Timestamp updatedAt; public void delete() { this.visible = false; this.updatedAt = new Timestamp(System.currentTimeMillis()); }
 
Share article

Nakyeom's Study