개요
엔티티를 설계하고, 애플리케이션을 구축해 나가다 보면 엔티티간의 여러 연관관계를 고려해주어야할 것입니다. 이렇게 여러 엔티티간의 연관관계를 가지게 되면, 하나의 엔티티가 삭제될 때에도 이 엔티티가 어디까지 영향을 미치는지 고려해주어야 하는데요. 이를 처리하기 위해서는 hard delete와 soft delete가 존재합니다. 이번 포스팅에서는 Hard Delete가 무엇인지 간단하게 언급하고, Soft Delete에 대해 적용해보도록 하겠습니다.
Hard Delete?
Hard Delete는 DELETE 쿼리를 데이터베이스에 날려 데이터를 실제로 삭제하는 방법을 말합니다.
Soft Delete?
Soft delete는 hard delete와 다르게, 실제로 데이터베이스에서 데이터를 삭제하는 것이 아닌, 테이블에 deleted와 같은 필드를 추가하고, delete 쿼리 대신 update 쿼리를 날려 deleted 필드의 값을 변경해주는 방법입니다.
이 때, 조회 쿼리의 결과로 삭제 처리된 값이 반환되면 안되기 때문에 @where 어노테이션을 통해 조건을 추가해주거나, 애플리케이션 단에서 삭제되지 않은 데이터만 필터링하는 작업이 필요합니다.
이제 코드를 통해 좀 더 자세히 알아보도록 하겠습니다.
아래의 엔티티는 Post와 Comment가 있으며, Post와 Comment는 Comment의 관점에서 N:1의 연관관계를 맺을 것 입니다.
Comment Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Entity
public class Comment extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String comment;
@ManyToOne
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
public static Comment createComment(String comment, Post post, User user) {
return Comment.builder()
.comment(comment)
.post(post)
.user(user)
.build();
}
public void updateComment(String comment) {
this.comment = comment;
}
}
위와 같이, 댓글 엔티티는 포스트 엔티티와의 연관관계 주인으로서, 다대일 관계를 맺어 참조하고 있는 것을 알 수 있습니다.
Post Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Entity
public class Post extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String title;
private String body;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
public static Post createPost(String title, String body, User user){
return Post.builder()
.title(title)
.body(body)
.user(user)
.build();
}
public void updatePost(String title, String body){
this.title = title;
this.body = body;
}
}
BaseEntity
@Getter
@ToString
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime lastModifiedAt;
private LocalDateTime deletedAt;
}
이러한 상황에서, 게시글이 존재하고, 게시물에 대한 댓글도 존재한다고 가정해봅시다.
구현해둔 비즈니스 로직은 생략하고, 포스트맨을 통해서 수동으로 테스트해보도록 하겠습니다.
게시글 등록

위 게시글에 대한 댓글 등록 및 조회

위와 같이, postId가 25번인 게시글이 등록되었고, 이 게시글에 대한 댓글이 2개 존재하는 상황이 만들어졌습니다.
게시글 삭제하기
이제 위에서 생성했던 게시글을 삭제해보도록 하겠습니다. 그러나, 삭제가 진행되지 않습니다.
java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`mutsasns`.`comment`, CONSTRAINT `FKs1slvnkuemjsq2kj4h3vhx7i1` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`))
이는, 댓글 엔티티가 포스트 엔티티를 참조하고 있기 때문에, 삭제나 업데이트를 할 수 없다는 것입니다. 이제, 우리가 SOFT DELETE로 이를 처리해주어야 할 차례가 왔습니다.
@SQLDelete
@SQLDelete는 엔티티 삭제가 발생했을 경우, delete 쿼리 대신 실행시켜줄 커스텀 SQL 구문을 뜻하는 어노테이션 입니다.
따라서, @SQLDelete를 적어주면 엔티티 삭제 요청시 delete 쿼리 대신 적어둔 update 쿼리가 날아갑니다. 따라서, 우리는 포스트를 삭제할 때에 포스트에 달린 댓글 또한 삭제가 된 것처럼 구현할 수 있다는 것 입니다.
아래와 같이, Post, Comment 엔티티 각각 @SQLDelete 어노테이션을 추가해줍니다.
Post
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@SQLDelete(sql = "UPDATE Post SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
@where(clause = "deleted_at is null")
@Entity
public class Post extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String title;
private String body;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
public static Post createPost(String title, String body, User user){
return Post.builder()
.title(title)
.body(body)
.user(user)
.build();
}
public void updatePost(String title, String body){
this.title = title;
this.body = body;
}
}
Comment
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@SQLDelete(sql = "UPDATE comment SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?")
@where(clause = "deleted_at is null")
@Entity
public class Comment extends BaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String comment;
@ManyToOne
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
public static Comment createComment(String comment, Post post, User user) {
return Comment.builder()
.comment(comment)
.post(post)
.user(user)
.build();
}
public void updateComment(String comment) {
this.comment = comment;
}
}
@Where
이제 우리는, 포스트와 댓글이 삭제된 것 처럼 구현했기 때문에, 댓글 조회 요청 시 deleteAt 필드에 값이 null인 것들만 가져와야할 것 입니다. 이 말은 즉슨, 삭제가 되지 않은 데이터들만 조회되도록 구현해야 한다는 것 입니다. 이는 위와 같이, @Where로 해결할 수 있습니다.
@Where은 기본적으로 적용할 where 구문을 뜻하는 어노테이션으로, 일반적으로 soft delete를 할 때 사용합니다.
변경 뒤 댓글 삭제 테스트
이제, deleted_at 필드에 현재 시간값을 컬럼값으로 설정해줌으로써, soft delete를 구현하였기 때문에, 정상적으로 삭제 처리되는 것을 알 수 있습니다.

나가면서
오늘은, @SQLDelete와 @Where를 통해 softdelete를 구현하는 방법에 대해 알아보았습니다.
그러나, 여기서 한가지 의문점이 생겼습니다. 이렇게 구현하게 되면, Post를 참조하는 엔티티에 모두 soft delete를 구현해주어야 할 것 같다는 생각이 듭니다.
Post를 참조하는 엔티티가 늘어나면 늘어날 수록, 혹은 규모가 너무 커서 여러 엔티티가 존재하는 경우에 일일히 설정하기 어렵다는 단점이 있다는 생각이 듭니다.
이를 개선하는 방안은 변경 감지로 soft delete를 구현하는 방법이 있겠다는 생각이 드는데요. 다음 포스팅에서 관련 내용을 다뤄보도록 하겠습니다.
'JPA' 카테고리의 다른 글
| [JPA] N+1 문제 (0) | 2022.12.22 |
|---|