Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor/#415 글 삭제, 수정시 게시글 이미지의 soft delete를 위해 update문이 중복해서 나지 않도록 수정함 #526

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package edonymyeon.backend.image.commentimage.repository;

import edonymyeon.backend.image.commentimage.domain.CommentImageInfo;
import java.util.List;
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;

public interface CommentImageInfoRepository extends JpaRepository<CommentImageInfo, Long> {

@Modifying //todo: 옵션.. 이대로 괜찮은가?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이전 기수 선배님께서 같은 고민을 하고 남기신 글이 있어 공유드려요!
Modifying을 이용한 작업을 하면 1차 캐시와 데이터베이스 사이의 정보 불일치가 발생하기 때문에,
해당 메서드를 호출한 다음에는 1차 캐시를 비워주는 작업을 반드시 해야 한다고 합니다 :)

Copy link
Collaborator Author

@jyeost jyeost Nov 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 저도 고민을 했었는데여!! 👍🏻

같은 트랜잭션 내에서 다시 해당 엔티티를 조회할 일이 있다면 "JQPL로 변경된 값을 조회할 때는 1차 캐시를 비우거나 다이렉트 DB 조회를 해야합니다."

이 분도 이렇게 말씀하셧네용
현재 게시글의 수정과 삭제 이후에 같은 트랜잭션에서 게시글의 상세정보, 이미지나 댓글의 상세정보를 사용하고 있지 않아여!
return이 Void거나 id 정도만 사용되고 있습니당!! 따라서 1차 캐시를 비워주는 작업을 추가하지 않았습니당~ 😘

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

합리적이네요!

@Query("update CommentImageInfo c set c.deleted = true where c.id in :ids")
void deleteAllById(@Param("ids") List<Long> ids);

@Query(value = "select * from comment_image_info", nativeQuery = true)
List<CommentImageInfo> findAllImages();
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ public PostImageInfo(final String storeName, final Post post) {
}

public static PostImageInfo of(final ImageInfo imageInfo, final Post post) {
final PostImageInfo postImageInfo = new PostImageInfo(imageInfo.getStoreName(), post);
//post.addPostImageInfo(postImageInfo);
return postImageInfo;
}

public void delete() {
this.deleted = true;
return new PostImageInfo(imageInfo.getStoreName(), post);
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package edonymyeon.backend.image.postimage.domain;

import static edonymyeon.backend.global.exception.ExceptionInformation.IMAGE_STORE_NAME_INVALID;
import static edonymyeon.backend.global.exception.ExceptionInformation.POST_IMAGE_COUNT_INVALID;

import edonymyeon.backend.global.exception.EdonymyeonException;
import edonymyeon.backend.image.domain.ImageInfo;
import edonymyeon.backend.post.domain.Post;
import jakarta.persistence.Embeddable;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.BatchSize;

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

import static edonymyeon.backend.global.exception.ExceptionInformation.IMAGE_STORE_NAME_INVALID;
import static edonymyeon.backend.global.exception.ExceptionInformation.POST_IMAGE_COUNT_INVALID;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Embeddable
Expand Down Expand Up @@ -53,33 +54,14 @@ private boolean isInvalidImageCount(final Integer imageCount) {
return imageCount > MAX_IMAGE_COUNT;
}

public void addAll(final List<PostImageInfo> imagesToAdd) {
validateImageCount(this.postImageInfos.size() + imagesToAdd.size());
this.postImageInfos.addAll(imagesToAdd);
}

public void add(final PostImageInfo postImageInfo) {
if (this.postImageInfos.contains(postImageInfo)) {
return;
}
validateImageAdditionCount();
this.postImageInfos.add(postImageInfo);
}

private void validateImageAdditionCount() {
if (isInvalidImageCount(this.postImageInfos.size() + 1)) {
throw new EdonymyeonException(POST_IMAGE_COUNT_INVALID);
}
}

public void update(final List<String> remainedStoreNames, final List<PostImageInfo> newPostImageInfos) {
public List<Long> getImageIdsToDeleteBy(final List<String> remainedStoreNames, final List<PostImageInfo> newPostImageInfos) {
final List<PostImageInfo> imagesToDelete = findImagesToDelete(remainedStoreNames);
int updatedImageCount = this.postImageInfos.size() - imagesToDelete.size() + newPostImageInfos.size();
validateImageCount(updatedImageCount);

imagesToDelete.forEach(PostImageInfo::delete);
postImageInfos.removeAll(imagesToDelete);
postImageInfos.addAll(newPostImageInfos);
return imagesToDelete.stream()
.map(ImageInfo::getId)
.toList();
}

private List<PostImageInfo> findImagesToDelete(final List<String> remainedStoreNames) {
Expand All @@ -93,18 +75,6 @@ private List<PostImageInfo> findImagesToDelete(final List<String> remainedStoreN
return unmatchedPostImageInfos;
}

// todo : 여기 부분 맞게 했나요? 헷갈립니다.
public void delete(final List<PostImageInfo> deletedPostImageInfos) {
// 어쨌든 deleted = false 인 놈들만 가지고 있어야 하니 지워져야 할 녀석들을 리스트에서 뺀다.
this.postImageInfos.removeAll(deletedPostImageInfos);
// 지워져야 하는 녀석들을 soft delete
deletedPostImageInfos.forEach(PostImageInfo::delete);
}

public void deleteAll() {
this.postImageInfos.forEach(PostImageInfo::delete);
}

public boolean isEmpty() {
return this.postImageInfos.isEmpty();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package edonymyeon.backend.image.postimage.repository;

import edonymyeon.backend.image.postimage.domain.PostImageInfo;
import java.util.List;
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;

public interface PostImageInfoRepository extends JpaRepository<PostImageInfo, Long> {

List<PostImageInfo> findAllByPostId(final Long postId);

@Modifying
@Query("delete from PostImageInfo pi where pi.post.id=:postId")
@Query("update PostImageInfo pi set pi.deleted = true where pi.post.id=:postId")
void deleteAllByPostId(@Param("postId") final Long postId);

@Query(value = "select * from post_image_info", nativeQuery = true)
List<PostImageInfo> findAllImages();
Comment on lines -18 to -19
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 못지운 부분을 지워주셨군요!! 감사드립니당
CommentInfoRepository, ProfileImageInfoRepository에 있는 부분도 부탁드려도 될까요(굽신굽신)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정했슴돠 !!

@Modifying
@Query("update PostImageInfo pi set pi.deleted = true where pi.id in (:imageIds)")
void deleteAllByIds(@Param("imageIds") final List<Long> imageIds);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package edonymyeon.backend.image.profileimage.repository;

import edonymyeon.backend.image.profileimage.domain.ProfileImageInfo;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface ProfileImageInfoRepository extends JpaRepository<ProfileImageInfo, Long> {

@Query(value = "select * from profile_image_info", nativeQuery = true)
List<ProfileImageInfo> findAllImages();
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package edonymyeon.backend.post.application;

import static edonymyeon.backend.global.exception.ExceptionInformation.MEMBER_ID_NOT_FOUND;
import static edonymyeon.backend.global.exception.ExceptionInformation.POST_ID_NOT_FOUND;
import static edonymyeon.backend.global.exception.ExceptionInformation.POST_MEMBER_NOT_SAME;

import edonymyeon.backend.global.exception.EdonymyeonException;
import edonymyeon.backend.image.application.ImageService;
import edonymyeon.backend.image.domain.ImageType;
Expand All @@ -18,14 +14,17 @@
import edonymyeon.backend.post.application.event.PostDeletionEvent;
import edonymyeon.backend.post.domain.Post;
import edonymyeon.backend.post.repository.PostRepository;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.Objects;

import static edonymyeon.backend.global.exception.ExceptionInformation.*;

@RequiredArgsConstructor
@Service
public class PostService {
Expand Down Expand Up @@ -90,13 +89,12 @@ public void deletePost(final MemberId memberId, final Long postId) {
final Post post = findPostById(postId);
checkWriter(member, post);

// soft delete 시킬 때, 실제 이미지는 보관된다.
// todo: 이미지 삭제.. 한번에..
// todo: 소비내역 삭제할 때, 이벤트 대신 인터페이스로 변경
applicationEventPublisher.publishEvent(new PostDeletionEvent(post.getId()));
thumbsService.deleteAllThumbsInPost(postId);
commentService.deleteAllCommentsInPost(postId);
post.delete();
postImageInfoRepository.deleteAllByPostId(postId);
}

private Post findPostById(final Long postId) {
Expand Down Expand Up @@ -126,13 +124,18 @@ public PostIdResponse updatePost(
final List<String> remainedImageNames = imageService.convertToStoreName(request.originalImages(), ImageType.POST);

if(isImagesEmpty(imageFilesToAdd)) {
post.updateImages(remainedImageNames);
final List<Long> imageIdsToDelete = post.getImageIdsToDeleteBy(remainedImageNames);
postImageInfoRepository.deleteAllByIds(imageIdsToDelete);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 분기처리 안해도 될 거 같은데 어떻게 생각하나여? 케로상
지우고 테스트 해봤는데, delete할 게 없으면 어차피 update 쿼리가 안나가서 분기로 안빼는게 코드가 깔끔해지지 않을까 생각합니드아..

Copy link
Collaborator Author

@jyeost jyeost Nov 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 빼보려고 지워보긴 했는데,,,
제가 지우고 테스트 했을때는 imageFilesToAdd가 비어있을 때 emptyList가 아니라서 그런지 터지더라고여???
분기가 있기는 해야할 것 같던데,,,, 호이쒸가 함 고쳐서 푸시 해주시졍!!!

return new PostIdResponse(postId);
}

final PostImageInfos imagesToAdd = PostImageInfos.of(post, imageService.saveAll(imageFilesToAdd, ImageType.POST));
post.updateImages(remainedImageNames, imagesToAdd); //이때 기존 이미지중 삭제되는 것들은 softDelete

final List<Long> imageIdsToDelete = post.getImageIdsToDeleteBy(remainedImageNames, imagesToAdd);
postImageInfoRepository.deleteAllByIds(imageIdsToDelete); //이때 기존 이미지중 삭제되는 것들은 softDelete

postImageInfoRepository.saveAll(imagesToAdd.getPostImageInfos()); // //새로 추가된 이미지들을 DB에 저장

return new PostIdResponse(postId);
}
}
19 changes: 5 additions & 14 deletions backend/src/main/java/edonymyeon/backend/post/domain/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ public class Post extends TemporalRecord {
@JoinColumn(nullable = false)
private Member member;

// TODO: cascade
private PostImageInfos postImageInfos;

@ColumnDefault("0")
Expand Down Expand Up @@ -125,10 +124,6 @@ private void validateMember(final Member member) {
}
}

public void addPostImageInfo(final PostImageInfo postImageInfo) {
this.postImageInfos.add(postImageInfo);
}

public void validateImageCount(final Integer imageCount) {
this.postImageInfos.validateImageCount(imageCount);
}
Expand Down Expand Up @@ -156,18 +151,16 @@ private void updatePrice(final Long price) {

/**
* 게시글 수정시 사용, 새로 추가되는 이미지가 없고 기존 이미지에 대한 수정만 일어나는 경우
* -> imageNamesToMaintain을 제외하고 삭제한다.
*/
public void updateImages(final List<String> remainedImageNames) {
postImageInfos.update(remainedImageNames, Collections.emptyList());
public List<Long> getImageIdsToDeleteBy(final List<String> remainedImageNames) {
return this.postImageInfos.getImageIdsToDeleteBy(remainedImageNames, Collections.emptyList());
}

/**
* 게시글 수정시 사용, 새로 추가되는 이미지도 있는 경우
* -> imageNamesToMaintain을 제외하고 삭제 후, imagesToAdd를 추가한다.
*/
public void updateImages(final List<String> remainedImageNames, final PostImageInfos imagesToAdd) {
this.postImageInfos.update(remainedImageNames, imagesToAdd.getPostImageInfos());
public List<Long> getImageIdsToDeleteBy(final List<String> remainedImageNames, final PostImageInfos imagesToAdd) {
return this.postImageInfos.getImageIdsToDeleteBy(remainedImageNames, imagesToAdd.getPostImageInfos());
}

public boolean isSameMember(final Member member) {
Expand All @@ -179,7 +172,7 @@ public boolean isSameMember(final Long memberId) {
}

public Member getMember() {
return member;
return this.member;
}

public Long getWriterId() {
Expand Down Expand Up @@ -212,8 +205,6 @@ public void updateView(final Member member) {
}

public void delete() {
//lazyLoading 문제로 repository를 통해 직접 postImageInfos를 제거해주는 것이 필요하다.
this.postImageInfos.deleteAll();
this.deleted = true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
package edonymyeon.backend.post.docs;

import static edonymyeon.backend.auth.ui.SessionConst.USER;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import edonymyeon.backend.CacheConfig;
import edonymyeon.backend.image.postimage.domain.PostImageInfo;
import edonymyeon.backend.image.domain.ImageInfo;
import edonymyeon.backend.image.postimage.domain.PostImageInfos;
import edonymyeon.backend.image.postimage.repository.PostImageInfoRepository;
import edonymyeon.backend.member.application.dto.ActiveMemberId;
Expand All @@ -39,10 +18,6 @@
import edonymyeon.backend.support.TestMemberBuilder;
import edonymyeon.backend.thumbs.application.PostThumbsServiceImpl;
import jakarta.servlet.http.Part;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.http.entity.ContentType;
import org.junit.jupiter.api.Test;
Expand All @@ -54,7 +29,6 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
Expand All @@ -64,6 +38,24 @@
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import static edonymyeon.backend.auth.ui.SessionConst.USER;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SuppressWarnings("NonAsciiCharacters")
@RequiredArgsConstructor
@AutoConfigureMockMvc
Expand Down Expand Up @@ -132,11 +124,11 @@ class PostControllerDocsTest implements ImageFileCleaner {
.id(1L)
.buildWithoutSaving();

final Post 게시글 = new Post(1L, "제목", "내용", 1000L, 글쓴이);
final PostImageInfo 유지하는_이미지_정보 = new PostImageInfo("stay.jpg", 게시글);
final PostImageInfo 삭제될_이미지_정보 = new PostImageInfo("delete.jpg", 게시글);
게시글.addPostImageInfo(유지하는_이미지_정보);
게시글.addPostImageInfo(삭제될_이미지_정보);
final ImageInfo 유지하는_이미지_정보 = new ImageInfo("stay.jpg");
final ImageInfo 삭제될_이미지_정보 = new ImageInfo("delete.jpg");

final PostImageInfos postImageInfos = PostImageInfos.of(null, List.of(유지하는_이미지_정보, 삭제될_이미지_정보));
final Post 게시글 = new Post(1L, "제목", "내용", 1000L, 글쓴이, postImageInfos, 0,0, false);

회원_레포지토리를_모킹한다(글쓴이);
게시글_레포지토리를_모킹한다(게시글);
Expand Down
Loading