-
Notifications
You must be signed in to change notification settings - Fork 6
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
[BE] Refactor/#540 지연로딩의 경우, soft delete을 반영할 수 없는 문제 해결 #553
Changes from 6 commits
8ef8e54
9db5da3
ca7948b
1af7a11
28e0e4e
83efb4b
c030c84
5f90719
279566f
d3c8c7e
05c62bc
946c65a
43fc9fc
26d98b8
b75fdb6
9df7456
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
package com.mapbefine.mapbefine.admin.application; | ||
|
||
import static com.mapbefine.mapbefine.pin.exception.PinErrorCode.PIN_NOT_FOUND; | ||
import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.TOPIC_NOT_FOUND; | ||
|
||
import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; | ||
|
@@ -11,19 +12,23 @@ | |
import com.mapbefine.mapbefine.pin.domain.Pin; | ||
import com.mapbefine.mapbefine.pin.domain.PinImageRepository; | ||
import com.mapbefine.mapbefine.pin.domain.PinRepository; | ||
import com.mapbefine.mapbefine.pin.exception.PinException.PinNotFoundException; | ||
import com.mapbefine.mapbefine.topic.domain.Topic; | ||
import com.mapbefine.mapbefine.topic.domain.TopicRepository; | ||
import com.mapbefine.mapbefine.topic.exception.TopicException; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
import jakarta.persistence.EntityManager; | ||
import jakarta.persistence.PersistenceContext; | ||
import java.util.List; | ||
import java.util.NoSuchElementException; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Service | ||
@Transactional | ||
public class AdminCommandService { | ||
|
||
@PersistenceContext | ||
private EntityManager entityManager; | ||
private final MemberRepository memberRepository; | ||
private final TopicRepository topicRepository; | ||
private final PinRepository pinRepository; | ||
|
@@ -61,12 +66,13 @@ private void deleteAllRelatedMember(Member member) { | |
List<Long> pinIds = extractPinIdsByMember(member); | ||
Long memberId = member.getId(); | ||
|
||
pinImageRepository.deleteAllByPinIds(pinIds); | ||
topicRepository.deleteAllByMemberId(memberId); | ||
pinRepository.deleteAllByMemberId(memberId); | ||
permissionRepository.deleteAllByMemberId(memberId); | ||
atlasRepository.deleteAllByMemberId(memberId); | ||
bookmarkRepository.deleteAllByMemberId(memberId); | ||
entityManager.flush(); | ||
pinImageRepository.deleteAllByPinIds(pinIds); | ||
pinRepository.deleteAllByMemberId(memberId); | ||
topicRepository.deleteAllByMemberId(memberId); | ||
} | ||
|
||
private Member findMemberById(Long id) { | ||
|
@@ -91,12 +97,18 @@ public void deleteTopicImage(Long topicId) { | |
} | ||
|
||
private Topic findTopicById(Long topicId) { | ||
return topicRepository.findByIdAndIsDeletedFalse(topicId) | ||
return topicRepository.findById(topicId) | ||
.orElseThrow(() -> new TopicException.TopicNotFoundException(TOPIC_NOT_FOUND, List.of(topicId))); | ||
} | ||
|
||
public void deletePin(Long pinId) { | ||
pinRepository.deleteById(pinId); | ||
Pin pin = pinRepository.findById(pinId) | ||
.orElseThrow(() -> new PinNotFoundException(PIN_NOT_FOUND, pinId)); | ||
|
||
/// TODO: 2023/10/05 soft delete를 @Query로 구현하면서, pinCount 반영을 통합할 수 없음 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. update pin p, topic t
set p.is_deleted = true, t.pin_count = t.pin_count - 1
where p.id = "삭제하려는 pin id" AND t.id = p.topic_id; 금단의 영역인 것 같긴하지만, 방금 되는 것을 확인하긴 했습니다... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그냥 하나의 방법이니 보고 피식 웃고 넘어가주세용~~ ㅋㅋㅋ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금 방법대로 하면 안 되는건가요 ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사실 soft delete 의 대가인 것 같아요 ㅋㅋㅋ delete 해줘야 하는 부분에서 연관된 다른 부분들도 신경써야 하는거요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 근데 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아니면 JPA 이벤트리스너로 해결하는 방법도 있을까 싶은데 이 PR 내용과는 거리가 멀기 때문에 별개로 시도해볼게요! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋아연 |
||
pin.decrementTopicPinCount(); | ||
pinRepository.deleteById(pin.getId()); | ||
/// TODO: 2023/10/05 pinImage는? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pin image 도 연계적으로 지워줘야 하겠군요.... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. soft delete 가 참 산이 많네요 |
||
} | ||
|
||
public void deletePinImage(Long pinImageId) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,7 +11,6 @@ | |
import com.mapbefine.mapbefine.topic.domain.Topic; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
|
@@ -31,6 +30,7 @@ public void validateMember(Long memberId) { | |
} | ||
} | ||
|
||
// TODO 테스트가 필요하긴 한데, MemberQueryService와 너무 중복되는 내용이다. 엔티티 테스트로 중복을 없앨 수 있으면 좋겠다. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오우 조금 더 자세하게 말씀해주실 수 있을까요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 언젠가 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 맞아요 근데 AuthService 의존성 문제때문에 귀찮아서... + 서비스 단에서 테스트하다보니 발생하는 중복이 많은 것 같아서요. (이건 저희 테스트 코드 전반적인 문제인 것 같아요) 더 작은 단위에서 테스트하면 좋을텐데 엔티티 테스트 해보려니 TestEntityManager 잔뜩 써야 할 것 같아서 일단 안했습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 허허허허 |
||
public AuthMember findAuthMemberByMemberId(Long memberId) { | ||
return memberRepository.findById(memberId) | ||
.map(this::convertToAuthMember) | ||
|
@@ -63,15 +63,4 @@ private List<Long> getCreatedTopics(Member member) { | |
.toList(); | ||
} | ||
|
||
public boolean isAdmin(Long memberId) { | ||
if (Objects.isNull(memberId)) { | ||
return false; | ||
} | ||
|
||
Optional<Member> member = memberRepository.findById(memberId); | ||
|
||
return member.map(Member::isAdmin) | ||
.orElse(false); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,12 @@ | ||
package com.mapbefine.mapbefine.bookmark.domain; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.data.jpa.repository.Modifying; | ||
|
||
import java.util.Optional; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
public interface BookmarkRepository extends JpaRepository<Bookmark, Long> { | ||
|
||
boolean existsByMemberIdAndTopicId(Long memberId, Long topicId); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이건 쥬니 글에서 보았듯 @query 어노테이션이 동반되지 않으면 짜피 clearAutomatically 가 동작하지 않으니까 빼신게 맞나요 ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코멘트 위치가 잘못된듯요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 또잉또잉 클릭 잘못했어연 바로 아래줄일듯 허허허 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 맞아요! |
||
@Modifying(clearAutomatically = true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 부분 JPQL로 해결해야하는데... 도이꺼 PR 머지되면 브랜치 따서 해결할게요. 잊지말자 !!!!!!!!!!!! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기다가 제가 단 거 였는데 ㅋㅋㅋㅋ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Service에서 EntityManager 불러오는 대신 JPQL로 바꾸고 |
||
void deleteAllByMemberId(Long memberId); | ||
|
||
Optional<Bookmark> findByMemberIdAndTopicId(Long memberId, Long topicId); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -104,12 +104,16 @@ private static String createNicknameSuffix() { | |
.substring(0, DEFAULT_NICKNAME_SUFFIX_LENGTH); | ||
} | ||
|
||
public void update( | ||
public void updateNickName( | ||
String nickName | ||
) { | ||
memberInfo = memberInfo.createUpdatedMemberInfo(nickName); | ||
} | ||
|
||
public void updateStatus(Status status) { | ||
memberInfo = memberInfo.createUpdatedMemberInfo(status); | ||
} | ||
|
||
public void addTopic(Topic topic) { | ||
Comment on lines
+113
to
117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 형태를 통일해주셨군요 굳 |
||
createdTopics.add(topic); | ||
} | ||
|
@@ -147,14 +151,4 @@ public List<Topic> getTopicsWithPermissions() { | |
public boolean isNormalStatus() { | ||
return memberInfo.getStatus() == Status.NORMAL; | ||
} | ||
|
||
public void updateStatus(Status status) { | ||
memberInfo = MemberInfo.of( | ||
memberInfo.getNickName(), | ||
memberInfo.getEmail(), | ||
memberInfo.getImageUrl(), | ||
memberInfo.getRole(), | ||
status | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,10 +24,12 @@ | |
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import org.hibernate.annotations.ColumnDefault; | ||
import org.hibernate.annotations.Where; | ||
|
||
@Entity | ||
@NoArgsConstructor(access = PROTECTED) | ||
@Getter | ||
@Where(clause = "is_deleted = false") | ||
public class Pin extends BaseTimeEntity { | ||
|
||
@Id | ||
|
@@ -102,6 +104,10 @@ public void updatePinInfo(String name, String description) { | |
pinInfo = PinInfo.of(name, description); | ||
} | ||
|
||
public void decrementTopicPinCount() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엥 맞아요 왜 저렇게 썼을까요 ? 영어 천재시네요 . 고칠게용 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 천재아닌데요! |
||
topic.decrementPinCount(); | ||
} | ||
|
||
public void copyToTopic(Member creator, Topic topic) { | ||
Pin copiedPin = Pin.createPinAssociatedWithLocationAndTopicAndMember( | ||
pinInfo.getName(), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spring Data JPA의 Repository 인터페이스에는 flush 메서드가 기본적으로 포함되어있어서
해당 메서드를 사용하는 경우, 내부적으로 EntityManager의 flush 메서드가 호출된다고 하네용!
entityManager를 주입받을 필요없이 repository에서 flush하는 건 어떨까요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추가적인 주입이 필요없다는 면에서 굉장히 좋은 아이디어네요!
하지만 현재 코드는
Permission
,Atlas
,Bookmark
에 대한hard delete
쿼리를 한꺼번에 날리는flush
이기 때문에,flush
를 호출하는 의무를 어떠한Repository
가 가져가야 하는지를 정하는 것도 굉장히 애매한 부분인 것 같기도 하네요.다들 어떻게 생각하시나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사실 저도
entityManager
가 존재한다는게 거슬리긴 해요.준팍이 말씀해주신
이 부분은 별도의 설정하지 않아도 된다는 의미일까요 ?
아마, 테스트 코드 구성을 잘못해서 발생한 문제를 해결하기 위해 위와같은 구조를 갖게 된 걸로 알고있어요. (내가 짬 ㅋ)
테스트 코드에서
//given
에서 데이터를 세팅해주는 작업과 같은 트랜잭션으로 묶이기 때문에flush
를 강제로 호출해주지 않으면 수정 관련 쿼리가 쓰기 지연 저장소에 저장되었다가, 트랜잭션이 커밋되기 직전에 데이터베이스로 쿼리를 날라가게 되요.어떻게 해결 할 수 있다는 의미인지 궁금합니다 !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
맞아요! 다른 경우처럼 Repository 메서드를 호출하는 것만으로 flush를 처리하면 좋겠지만,
지금은 Transactional로 묶여있는 메서드라 서비스 메서드 호출이 끝날때 한번에 flush하지 않나요..?!
그래서 위 경우 Topic, Pin, PinImage의 soft delete 쿼리(update)를 날리기 전에
Permission, Atlas, Bookmark 에 대한 hard delete 쿼리를 먼저 날려주는 처리가 필요했어요.
하지만 저도 EntityManager에 대한 의존성을 없애는 게 좋다고 생각하는데,
그러려면
@Query
를 쓰더라도@Modifying
으로 flush automatically 해주는 게 좋을까요?아니면 더 좋은 방법을 아신다면 공유 부탁드립니다 !! 🥲
중간에 flush를 해줘야 하는 이유
72번 줄이 없을 경우, 예외는 발생하지 않았지만 hard delete 메서드 호출이 DB에 반영되지 않는 것을 확인할 수 있었어요.
(테스트, 로컬 POSTMAN 확인 모두)
제가 추측하는 플로우는 아래와 같습니다.
중간에 flush를 해주지 않고 쓰기 지연으로 한 번에 flush할 경우 아래와 같은 순서로 동작하는 것 아닐까요?
-> (1)에서 delete 해도, 2에서 조회할 때 함께 불러와지는 permission, atlas, bookmark까지 다시 영속화된다.
그래서 (1)과 (2) 사이에 직접 flush를 해야 한다고 생각했어요.
제가 아직 헷갈려서 정리하다보니 길어졌네요.. ㅎㅎ 그래도 참고차 남깁니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아니 제가 화면 새로고침을 안해서 쥬니 댓글 지금봤네용 하핫
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cpot5620 @yoondgu
아 저는 flush를 별도로 설정하지 말라는 의미는 아니었구요!
위에 보이는 메서드를 사용하자는 의미였습니다 ㅎㅎ
이 메서드를 사용하면 entityManager의 flush를 사용한것과 동일하게 작동한다고 하네요 ㅎㅎ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GPT 센세의 첨언...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
세 레포지터리 중 어떤 것을 써도 동일하게 동작할텐데
매튜 말대로 뭐 써야될지 조금 고민되긴 하겠네요 ㅋㅋ
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아 아까 트러블슈팅하다가 clear까지 해보려고 entityManager를 받아왔다가 이 생각을 못했네요 !!
근데 대신 delete 메서드에 JPQL 직접 쓰고 Modifying 으로 해결하는 건 어떠세요?
쥬니가 쓴 기술 블로그 참고
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋습니다 ㅎㅎ