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

[Feature] 봉사자 회원 탈퇴 API 구현 #219

Merged
merged 9 commits into from
Jun 2, 2024

Conversation

hojeong2747
Copy link
Member

💡 연관된 이슈

close #218

📝 작업 내용

  • 탈퇴한 계정 표시 위한 봉사자 계정 쿼리 추가
  • 봉사자 엔티티와 연관관계 있는 엔티티들 cascade 옵션 설정
  • 봉사자 탈퇴 API 구현

💬 리뷰 요구 사항

우선 탈퇴한 계정 표시를 위한 봉사자 계정을 하나 추가했습니다. 데이터를 삭제하지 않고 탈퇴한 사용자를 보여주기 위해서 매번 로직을 돌려서 탈퇴한 계정일 경우에 표시하는 것보다, 아예 탈퇴했다는 사실을 알려주기 위한 대표 계정을 만들어 표시하기도 한다고 합니다! DB에서 아예 삭제를 하지 않으면서 표시를 하기 위해 이 방법이 현재 가장 적합하다고 판단이 들었습니다!

다음은 삭제 관련인데요,
Volunteer 엔티티와 연관관계에 있는 엔티티들은 다음과 같습니다.

  1. Review
  2. Application
  3. VolunteerBadge
  4. Bookmark
  5. VolunteerNotification

1,2 번은 데이터를 삭제하지 않고 '탈퇴한 사용자'임을 표시해 주기로 하였기에 3,4,5번은 삭제가 필요합니다.

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "volunteer_id", nullable = false)
    private Volunteer volunteer; // 이동봉사자 id

이렇게 옵션을 설정해도 DB에서는
image
On Delete 값이 CASCADE로 설정이 안되더라구요!

그래서 다음과 같이 기존 외래키를 지우고, 다시 CASCADE 옵션으로 설정을 해주면
image
이렇게 바뀌고, 제대로 실행이 됩니다.


ALTER TABLE volunteer_badge
DROP FOREIGN KEY FK2lp66fjptwwkdxi5spm8jpb5;

ALTER TABLE bookmark
DROP FOREIGN KEY FKkm47dr0i09mor5ks9aaebx15u;

ALTER TABLE volunteer_notification
DROP FOREIGN KEY FK2njpjir587cvu7j8x7mgcrmop;

ALTER TABLE volunteer_badge
ADD CONSTRAINT FK_volunteer_badge_volunteer_id
FOREIGN KEY (volunteer_id)
REFERENCES volunteer (id)
ON DELETE CASCADE
ON UPDATE RESTRICT;

ALTER TABLE bookmark
ADD CONSTRAINT FK_bookmark_volunteer_id
FOREIGN KEY (volunteer_id)
REFERENCES volunteer (id)
ON DELETE CASCADE
ON UPDATE RESTRICT;

ALTER TABLE volunteer_notification
ADD CONSTRAINT FK_volunteer_notification_volunteer_id
FOREIGN KEY (volunteer_id)
REFERENCES volunteer (id)
ON DELETE CASCADE
ON UPDATE RESTRICT;

혹시 제가 놓친 게 있어서 옵션 설정이 안 먹는 건지, 아니면 맞게 설정했는데 이렇게 생성이 되는 거라면 위의 쿼리를 DB 생성된 후 실행해줘야 할 것 같습니다!

@hojeong2747 hojeong2747 added ✨ Feature 기능 개발 Priority: Medium 우선순위 중간 🐰 Hojeong 담당자 labels May 28, 2024
Copy link
Member

@kyeong-hyeok kyeong-hyeok left a comment

Choose a reason for hiding this comment

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

수고하셨습니다! 리뷰 확인 부탁드려요~ ~


List<Application> applications = applicationRepository.findByVolunteer(volunteer);
for (Application application : applications) {
application.updateDeletedVolunteer(deletedVolunteer);
Copy link
Member

Choose a reason for hiding this comment

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

Application의 비즈니스 메서드가 누락된 것 같습니다! 확인 부탁드려요

Copy link
Member Author

Choose a reason for hiding this comment

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

맞네요.. 커밋될 때 누락된 게 맞았어요! 다시 인텔리제이 들어가니까 뜨네요. 다음부터는 시간 더 많을 때 pr 올리고 확인까지 제대로 하겠습니다아

List<Review> reviews = reviewRepository.findByVolunteer(volunteer);
for (Review review : reviews) {
review.updateDeletedVolunteer(deletedVolunteer);
reviewRepository.save(review);
Copy link
Member

Choose a reason for hiding this comment

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

@ Transactional로 트랜잭션을 관리하고 있고 review가 영속성 컨텍스트에 포함돼서 따로 save하지 않아도 변경 사항이 DB에 반영될 거예요!

Copy link
Member Author

Choose a reason for hiding this comment

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

오옷 맞아요! 다시금 영속성 컨텍스트와 트랜잭션 관리에 대해 인지하는 중입니다. 체크 감사해요 👍

List<Application> applications = applicationRepository.findByVolunteer(volunteer);
for (Application application : applications) {
application.updateDeletedVolunteer(deletedVolunteer);
applicationRepository.save(application);
Copy link
Member

Choose a reason for hiding this comment

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

application도 review와 같이 save하는 로직이 없어도 괜찮을 것 같습니다!

@@ -17,7 +17,7 @@ public class VolunteerBadge extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "volunteer_id", nullable = false)
private Volunteer volunteer; // 이동봉사자 id
Copy link
Member

Choose a reason for hiding this comment

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

현재 작성하신 코드를 보면, VolunteerBadge에서 volunteer 필드에 cascade 조건이 걸려있는데 이렇게 된다면 봉사자 배지를 저장, 삭제할 때 volunteer에도 영향이 가게 됩니다! 자칫하면 배지를 삭제할 때 봉사자가 의도치 않게 삭제될 수도 있을 것 같습니다.

봉사자가 탈퇴할 때 봉사자가 가지고 있는 배지들을 한 번에 삭제하기 위해 사용하기 위해 cascade를 작성하신 거라면, 이는 봉사자 엔티티 내에 @ OneToMany로 VolunteerBadge를 필드로 가지고 있을 때 사용하는 것이 좋다고 판단됩니다! (물론 미치는 영향들을 고려해야 하지만..) 그렇게 된다면 봉사자를 삭제할 때 봉사자가 가지고 있는 배지들을 모두 삭제하는 조건으로 CascadeType.REMOVE를 이용할 수 있을 것 같아요.

이건 제 생각이지만! 회원 탈퇴에서 탈퇴한 사용자를 따로 만드셔서 구현하셨고, 해당 회원의 배지와 북마크를 모두 삭제하는 것을 위해 cascade를 고려하신 것 같은데 벌크연산으로 한 번에 삭제하도록 구현하면 어떨까요? 해당 회원이 가지고 있던 배지들과, 북마크한 공고들을 각각 한 번에 삭제할 수 있다면 cascade를 사용하지 않아도 될 것 같습니다! 현재 단방향 연관관계를 맺고 있어 사용하기 어려움도 있어 보이기도 하고 미치는 영향까지 고려한다면 저는 벌크 연산은 어떨까 생각합니다 ~!

Copy link
Member Author

Choose a reason for hiding this comment

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

  • CascadeType.ALL의 문제점
    • VolunteerBadge 엔티티에서 Volunteer 엔티티와의 관계에 CascadeType.ALL을 사용하면 VolunteerBadge 엔티티의 변경이 Volunteer 엔티티에 영향을 미칠 수 있다.
      • VolunteerBadge 를 저장하면 Volunteer도 저장된다.
      • VolunteerBadge 를 삭제하면 Volunteer 도 삭제될 수 있다.
      • 이는 의도하지 않은 데이터 삭제를 초래할 수 있다. 예를 들어, 봉사자 배지를 삭제할 때 봉사자가 함께 삭제될 위험이 있다.
  • 권장되는 설계
    • 봉사자 탈퇴 시, 봉사자가 갖고 있는 배지들을 삭제하려는 의도라면 Volunteer 엔티티에서 VolunteerBadge 엔티티를 @onetomany 관계로 갖고 있고, 그 관계에 CascadeType.REMOVE를 사용하는 것이 더 적합하다.

      @Entity
      public class Volunteer {
          @OneToMany(mappedBy = "volunteer", cascade = CascadeType.REMOVE)
          private List<VolunteerBadge> badges = new ArrayList<>();
          
          // 기타 필드 및 메서드
      }
      • 이렇게 하면, 봉사자를 삭제할 때 해당 봉사자가 가지고 있는 배지들도 함께 삭제된다.
  • 결론
    • VolunteerBadge 에서 CascadeType.ALL을 사용하는 것은 위험할 수 있다.
    • 오히려 Volunteer 엔티티에서 VolunteerBadge 와의 관계에 CascadeType.REMOVE 를 사용하거나, 벌크 연산을 고려하는 것이 더 안전하고 효율적이다.

Copy link
Member Author

Choose a reason for hiding this comment

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

  • 양방향 연관관계
    • 두 엔티티가 서로를 참조하는 관계
    • 한 엔티티가 다른 엔티티의 컬렉션 또는 필드를 참조하고, 반대쪽 엔티티도 이를 참조한다.
    • CascadeType 설정이 두 엔티티 간의 참조 방향에 따라 다르게 적용된다.
      • Volunteer 엔티티가 VolunteerBadge 엔티티를 CascadeType.REMOVE로 설정하면, Volunteer 를 삭제할 때 관련된 VolunteerBadge 엔티티도 삭제된다.
    • 장점
      • 직관성 - 데이터 모델링이 더 직관적이다. 엔티티 간의 관계를 명확하게 표현할 수 있다.
      • 네비게이션 용이성 - 양쪽에서 서로를 참조할 수 있어, 코드에서 양쪽 엔티티로 쉽게 이동할 수 있다.
      • 데이터 일관성 - 엔티티 간의 데이터 일관성을 유지하는 데 도움이 된다. 두 엔티티가 서로를 참조하므로 관계가 더 명확해진다.
  • 단방향 연관관계
    • 한 엔티티만 다른 엔티티를 참조하는 관계
      • CascadeType 설정이 참조하는 쪽에서만 적용된다.
        • Volunteer 엔티티가 VolunteerBadge 엔티티를 CascadeType.REMOVE로 설정하면, Volunteer 를 삭제할 때 관련된 VolunteerBadge 엔티티도 삭제된다. (예시는 동일)
    • 장점
      • 단순성 - 설계와 구현이 더 단순하다. 엔티티 간의 종속성이 적어 관리하기 쉽다.
      • 성능 - 불필요한 데이터 로드를 피할 수 있어 성능이 향상될 수 있다. 예를 들어, 엔티티를 조회할 때 관련된 엔티티를 로드할 필요가 없으면 성능이 향상된다.
      • 응집도 - 엔티티 간의 응집도를 높여준다. 엔티티가 자기 자신만의 책임을 갖도록 설계할 수 있어 코드의 유지보수성이 좋아진다.
      • 순환 참조 방지 - 양방향 연관관계는 순환 참조의 위험이 있다. 이는 직렬화나 JSON 변환 시 문제가 될 수 있다. 단방향 연관관계를 사용하면 이런 문제를 해결할 수 있다.
    • 단방향 연관관계를 사용하는 이유
      • 필요한 경우에만 관계 설정 - 모든 관계를 양방향으로 설정하면, 불필요한 복잡성이 증가한다. 실제 비즈니스 로직에서 양방향으로 참조할 필요가 없다면 단방향 연관관계가 더 적합하다.
      • 성능 최적화 - 양방향 연관관계는 종종 불필요한 데이터 로드를 초래할 수 있다. 단방향 연관관계를 사용하면 필요한 데이터만 로드할 수 있어 성능이 최적화된다.
      • 유지보수 용이 - 단방향 연관관계는 유지보수가 쉽다. 엔티티 간의 의존성이 적어 변경이 필요할 때 수정 범위가 줄어든다.
  • 언제 양방향 연관관계를 사용해야 하는가?
    • 두 엔티티 간의 관계를 양쪽에서 자주 참조해야 하는 경우
    • 비즈니스 로직에서 양방향으로 데이터를 탐색해야 하는 경우
    • 데이터 일관성을 유지하기 위해 두 엔티티 간의 참조가 필요한 경우

Copy link
Member Author

Choose a reason for hiding this comment

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

  • DB에서 여러 행에 대해 한 번에 실행되는 SQL 연산

  • 단일 쿼리로, 여러 레코드를 삽입/업데이트/삭제할 수 있게 해주며, 성능을 크게 향상시킬 수 있다.

  • JPA, Hibernate에서도 벌크 연산을 지원하며, 이를 통해 많은 양의 데이터를 효율적으로 처리할 수 있다.

  • 장점

    • 성능 향상 - 다수의 레코드에 대해 반복적으로 쿼리를 실행하는 것보다, 한 번에 많은 레코드를 처리하는 벌크 연산은 DB와의 통신 횟수를 줄여 성능을 향상시킨다.
    • 트랜잭션 오버헤드 감소 - 여러 개의 단일 트랜잭션을 처리하는 대신, 한 번의 트랜잭션으로 처리하여 오버헤드를 줄일 수 있다.
    • 간결성 - 코드가 간결해지고 유지보수가 쉬워진다.
  • 예시

    @Modifying
    @Query("UPDATE Volunteer v SET v.status = 'INACTIVE' WHERE v.lastLogin < :date")
    int deactivateVolunteers(@Param("date") LocalDate date);
    @Modifying
    @Query("DELETE FROM VolunteerBadge vb WHERE vb.volunteer.id = :volunteerId")
    void deleteBadgesByVolunteerId(@Param("volunteerId") Long volunteerId);
  • 주의사항

    • 영속성 컨텍스트와의 불일치 - 벌크 연산은 영속성 컨텍스트를 무시하고 직접 DB에서 실행되기 때문에 영속성 컨텍스트와 DB의 상태가 불일치할 수 있다.
    • 연관된 엔티티 - 벌크 연산은 연관된 엔티티에 영향을 미치지 않는다. 따라서 연관된 엔티티를 함께 처리해야 할 경우 별도의 추가 작업이 필요할 수 있다.
    • 트랜잭션 관리 - 벌크 연산은 트랜잭션 내에서 실행되어야 한다.

Copy link
Member Author

@hojeong2747 hojeong2747 Jun 1, 2024

Choose a reason for hiding this comment

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

회사에서 양방향 연관관계를 안 쓰고 무언가 삭제 시에도 useYn 필드 값으로 N 처리만 하기 때문에 JPA 개념들을 잊고 있었네요.. 명확하게 알려주셔서 감사해요. 다시 공부해보고 문제점을 파악해서 고쳐봤습니다!

Copy link
Member

@kyeong-hyeok kyeong-hyeok left a comment

Choose a reason for hiding this comment

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

이전 리뷰 사항 반영하시느라 고생하셨습니다! 리뷰 한 번 확인 부탁드려요~~


@Modifying
@Query("DELETE FROM VolunteerBadge vb WHERE vb.volunteer.id = :volunteerId")
void deleteByVolunteerId(@Param("volunteerId") Long volunteerId);
Copy link
Member

Choose a reason for hiding this comment

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

https://koeyhk.tistory.com/35
제가 예전에 썼던 글인데 코멘트에 달아주신 것과 같이 영속성 컨텍스트를 무시하고 쿼리를 실행하기 때문에 고려해야 할 점이 있습니다! 한 번 확인해 주세요~

Copy link
Member Author

Choose a reason for hiding this comment

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

  • 벌크 연산 수행 후 영속성 컨텍스트를 초기화 해주어야 한다고 하며 이 옵션을 써야 한다고 하지만, 사실 주의사항이 있다.
Volunteer deletedVolunteer = volunteerRepository.findByEmail("[email protected]").orElseThrow(() -> new BadRequestException(VOLUNTEER_NOT_FOUND));
List<Review> reviews = reviewRepository.findByVolunteer(volunteer);
for (Review review : reviews) {
    review.updateDeletedVolunteer(deletedVolunteer);
}

List<Application> applications = applicationRepository.findByVolunteer(volunteer);
for (Application application : applications) {
    application.updateDeletedVolunteer(deletedVolunteer);
}

bookmarkRepository.deleteByVolunteerId(volunteer.getId());
volunteerBadgeRepository.deleteByVolunteerId(volunteer.getId());
volunteerRepository.delete(volunteer);
  • 이 코드에서 clearAutomatically = true 옵션을 쓴 벌크 연산을 진행하게 되면, 오류가 난다.

    • 해당 메서드 호출 직후 영속성 컨텍스트가 초기화되는데, 이는 모든 영속 상태의 엔티티들을 컨텍스트에서 제거해 더 이상 관리되지 않음을 의미한다.
    • 이는 트랜잭션 커밋을 의미하지 않는다. 단순히 현재 영속성 컨텍스트를 비워 다음 작업에서 DB로부터 다시 로딩하게 만든다.
    could not execute statement 
    [(conn=597688) Cannot delete or update a parent row: a foreign key constraint fails 
    (`connectdog_db`.`application`, CONSTRAINT `FK93b2udtda22eo2xm382uvrcgp` FOREIGN KEY (`volunteer_id`) REFERENCES `volunteer` (`id`))] 
    [delete from volunteer where id=?]; SQL [delete from volunteer where id=?]; constraint [null]
    
    • 벌크 연산 후 영속성 컨텍스트가 초기화되면서, 다시 로딩된 엔티티들이 외래키 제약 조건을 위반하게 된다. 특히 벌크 삭제가 실행된 후 영속성 컨텍스트가 초기화되면, 다시 로딩된 application 엔티티들이 삭제된 volunteer 엔티티와 연관되어 외래키 제약 조건을 위반한다.
  • 이 옵션을 제거하고 영속성 컨텍스트를 수동으로 관리하는 방법을 고려할 수 있다. clearAutomatically = true 옵션은 entityManager.clear(); 만 실행하는 것과 동일하다.

    entityManager.flush(); // DB에 변경 사항 반영
    entityManager.clear(); // 영속성 컨텍스트 초기화
    • 벌크 연산 후에 flush, clear를 사용하는 것은 권장 사항이지만, 상태 불일치 문제가 없다는 확신이 있다면 반드시 사용해야 하는 것은 아니다.
    • 권장사항에 따라 다음과 같이 구현할 수 있다.
      • 벌크 연산 전 : entityManager의 flush 메서드를 호출해 변경 사항을 DB에 반영한다.
      • 벌크 연산 후 : entityManager의 clear 메서드를 호출해 영속성 컨텍스트를 초기화하거나, clearAutomatically = true 옵션을 사용한다.
    • 해결 완료!


@Modifying
@Query("DELETE FROM Bookmark b WHERE b.volunteer.id = :volunteerId")
void deleteByVolunteerId(@Param("volunteerId") Long volunteerId);
Copy link
Member

Choose a reason for hiding this comment

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

이 부분도 위에서 말한 부분 고려해야 할 것 같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

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

알려주신 @ Modifying(clearAutomatically = true) 적용된 메서드 호출 시 트러블 슈팅 과정 남겨두었습니다~!

Copy link
Member

@kyeong-hyeok kyeong-hyeok left a comment

Choose a reason for hiding this comment

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

벌크연산 사용 시 생기는 문제점을 고려하고 학습해서 적용하신 거 👍! 수고하셨습니다~~

@hojeong2747 hojeong2747 merged commit 67a5780 into develop Jun 2, 2024
1 check passed
@hojeong2747 hojeong2747 deleted the feat/218-delete-withdraw-api branch June 2, 2024 01:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ Feature 기능 개발 🐰 Hojeong 담당자 Priority: Medium 우선순위 중간
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature] 봉사자 회원 탈퇴 API 구현
2 participants