Skip to content

Commit 01aa2cc

Browse files
authored
Merge pull request #219 from DivaryOfficial/fix#218-cascadeType
fix#218: Member 삭제 시 Avatar cascade 설정 추가
2 parents 978a565 + 4389d01 commit 01aa2cc

File tree

3 files changed

+383
-0
lines changed

3 files changed

+383
-0
lines changed

src/main/java/com/divary/domain/member/entity/Member.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.divary.domain.member.entity;
22

33
import com.divary.common.entity.BaseEntity;
4+
import com.divary.domain.avatar.entity.Avatar;
45
import com.divary.domain.member.enums.Levels;
56
import com.divary.domain.member.enums.Role;
67
import com.divary.common.enums.SocialType;
@@ -55,6 +56,9 @@ public class Member extends BaseEntity {
5556
@Version
5657
private Long version; //버전을통해 레이스 컨디션 해결
5758

59+
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
60+
private Avatar avatar;
61+
5862

5963
// 탈퇴 요청 처리
6064
public void requestDeletion() {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.divary.integration;
2+
3+
import com.divary.domain.avatar.entity.Avatar;
4+
import com.divary.domain.avatar.repository.AvatarRepository;
5+
import com.divary.domain.device_session.entity.DeviceSession;
6+
import com.divary.domain.device_session.repository.DeviceSessionRepository;
7+
import com.divary.domain.member.entity.Member;
8+
import com.divary.domain.member.enums.Levels;
9+
import com.divary.domain.member.enums.Role;
10+
import com.divary.domain.member.enums.Status;
11+
import com.divary.domain.member.repository.MemberRepository;
12+
import com.divary.common.enums.SocialType;
13+
import org.junit.jupiter.api.DisplayName;
14+
import org.junit.jupiter.api.Test;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.boot.test.context.SpringBootTest;
17+
import org.springframework.transaction.annotation.Transactional;
18+
19+
import static org.junit.jupiter.api.Assertions.*;
20+
21+
@SpringBootTest
22+
@Transactional
23+
public class MemberDeletionSimpleTest {
24+
25+
@Autowired
26+
private MemberRepository memberRepository;
27+
28+
@Autowired
29+
private AvatarRepository avatarRepository;
30+
31+
@Autowired
32+
private DeviceSessionRepository deviceSessionRepository;
33+
34+
@Test
35+
@DisplayName("단순한 방법: @Transactional만으로 Member 삭제 테스트")
36+
void testSimpleMemberDeletion() {
37+
// Given: Member 생성
38+
Member member = Member.builder()
39+
40+
.socialId("simple-social-id")
41+
.socialType(SocialType.APPLE)
42+
.role(Role.USER)
43+
.level(Levels.OPEN_WATER_DIVER)
44+
.status(Status.ACTIVE)
45+
.build();
46+
member = memberRepository.save(member);
47+
48+
// Avatar 생성
49+
Avatar avatar = Avatar.builder()
50+
.user(member)
51+
.name("심플 버디")
52+
.build();
53+
avatar = avatarRepository.save(avatar);
54+
55+
// DeviceSession 생성
56+
DeviceSession session = DeviceSession.builder()
57+
.user(member)
58+
.refreshToken("simple-token")
59+
.socialType(SocialType.APPLE)
60+
.deviceId("simple-device")
61+
.build();
62+
session = deviceSessionRepository.save(session);
63+
64+
Long memberId = member.getId();
65+
Long avatarId = avatar.getId();
66+
Long sessionId = session.getId();
67+
68+
// 저장 확인
69+
assertTrue(memberRepository.existsById(memberId));
70+
assertTrue(avatarRepository.existsById(avatarId));
71+
assertTrue(deviceSessionRepository.existsById(sessionId));
72+
73+
// When: Member를 간단히 삭제 (flush, clear 없이)
74+
memberRepository.delete(member);
75+
76+
// Then: 모든 연관 데이터가 삭제되는지 확인
77+
assertFalse(memberRepository.existsById(memberId));
78+
assertFalse(avatarRepository.existsById(avatarId));
79+
assertFalse(deviceSessionRepository.existsById(sessionId));
80+
}
81+
}
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package com.divary.integration;
2+
3+
import com.divary.domain.avatar.entity.Avatar;
4+
import com.divary.domain.avatar.repository.AvatarRepository;
5+
import com.divary.domain.device_session.entity.DeviceSession;
6+
import com.divary.domain.device_session.repository.DeviceSessionRepository;
7+
import com.divary.domain.logbase.LogBaseInfo;
8+
import com.divary.domain.logbase.LogBaseInfoRepository;
9+
import com.divary.domain.logbase.logbook.enums.IconType;
10+
import com.divary.domain.logbase.logbook.enums.SaveStatus;
11+
import com.divary.domain.member.entity.Member;
12+
import com.divary.domain.member.enums.Levels;
13+
import com.divary.domain.member.enums.Role;
14+
import com.divary.domain.member.enums.Status;
15+
import com.divary.domain.member.repository.MemberRepository;
16+
import com.divary.domain.notification.entity.Notification;
17+
import com.divary.domain.notification.enums.NotificationType;
18+
import com.divary.domain.notification.repository.NotificationRepository;
19+
import com.divary.common.enums.SocialType;
20+
import jakarta.persistence.EntityManager;
21+
import jakarta.persistence.PersistenceContext;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.DisplayName;
24+
import org.junit.jupiter.api.Test;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.boot.test.context.SpringBootTest;
27+
import org.springframework.transaction.annotation.Transactional;
28+
29+
import java.time.LocalDate;
30+
import java.time.LocalDateTime;
31+
32+
import static org.junit.jupiter.api.Assertions.*;
33+
34+
@SpringBootTest
35+
@Transactional
36+
public class MemberDeletionTest {
37+
38+
@Autowired
39+
private MemberRepository memberRepository;
40+
41+
@Autowired
42+
private AvatarRepository avatarRepository;
43+
44+
@Autowired
45+
private LogBaseInfoRepository logBaseInfoRepository;
46+
47+
@Autowired
48+
private DeviceSessionRepository deviceSessionRepository;
49+
50+
@Autowired
51+
private NotificationRepository notificationRepository;
52+
53+
@PersistenceContext
54+
private EntityManager entityManager;
55+
56+
private Member testMember;
57+
58+
@BeforeEach
59+
void setUp() {
60+
// 테스트 회원 생성
61+
testMember = Member.builder()
62+
63+
.socialId("test-social-id")
64+
.socialType(SocialType.APPLE)
65+
.role(Role.USER)
66+
.level(Levels.OPEN_WATER_DIVER)
67+
.status(Status.ACTIVE)
68+
.build();
69+
testMember = memberRepository.save(testMember);
70+
}
71+
72+
@Test
73+
@DisplayName("Member 삭제 시 Avatar도 함께 삭제되어야 함 (CascadeType.ALL)")
74+
void testMemberDeletionWithAvatar() {
75+
// Given: Avatar 생성
76+
Avatar avatar = Avatar.builder()
77+
.user(testMember)
78+
.name("테스트 버디")
79+
.build();
80+
avatar = avatarRepository.save(avatar);
81+
avatarRepository.flush();
82+
83+
Long memberId = testMember.getId();
84+
Long avatarId = avatar.getId();
85+
86+
// 저장 확인
87+
assertTrue(memberRepository.existsById(memberId));
88+
assertTrue(avatarRepository.existsById(avatarId));
89+
90+
// When: Member 삭제
91+
entityManager.flush();
92+
entityManager.clear();
93+
Member memberToDelete = memberRepository.findById(memberId).orElseThrow();
94+
entityManager.remove(memberToDelete);
95+
entityManager.flush();
96+
97+
// Then: Avatar도 함께 삭제되어야 함
98+
assertFalse(memberRepository.existsById(memberId));
99+
assertFalse(avatarRepository.existsById(avatarId));
100+
}
101+
102+
@Test
103+
@DisplayName("Member 삭제 시 DeviceSession도 함께 삭제되어야 함 (@OnDelete CASCADE)")
104+
void testMemberDeletionWithDeviceSession() {
105+
// Given: DeviceSession 생성
106+
DeviceSession session = DeviceSession.builder()
107+
.user(testMember)
108+
.refreshToken("test-refresh-token")
109+
.socialType(SocialType.APPLE)
110+
.deviceId("test-device-id")
111+
.build();
112+
session = deviceSessionRepository.save(session);
113+
deviceSessionRepository.flush();
114+
115+
Long memberId = testMember.getId();
116+
Long sessionId = session.getId();
117+
118+
// 저장 확인
119+
assertTrue(memberRepository.existsById(memberId));
120+
assertTrue(deviceSessionRepository.existsById(sessionId));
121+
122+
// When: Member 삭제
123+
entityManager.flush();
124+
entityManager.clear();
125+
Member memberToDelete = memberRepository.findById(memberId).orElseThrow();
126+
entityManager.remove(memberToDelete);
127+
entityManager.flush();
128+
129+
// Then: DeviceSession도 함께 삭제되어야 함
130+
assertFalse(memberRepository.existsById(memberId));
131+
assertFalse(deviceSessionRepository.existsById(sessionId));
132+
}
133+
134+
@Test
135+
@DisplayName("Member 삭제 시 Notification도 함께 삭제되어야 함 (@OnDelete CASCADE)")
136+
void testMemberDeletionWithNotification() {
137+
// Given: Notification 생성
138+
Notification notification = Notification.builder()
139+
.receiver(testMember)
140+
.type(NotificationType.SYSTEM)
141+
.message("테스트 알림")
142+
.isRead(false)
143+
.build();
144+
notification = notificationRepository.save(notification);
145+
notificationRepository.flush();
146+
147+
Long memberId = testMember.getId();
148+
Long notificationId = notification.getId();
149+
150+
// 저장 확인
151+
assertTrue(memberRepository.existsById(memberId));
152+
assertTrue(notificationRepository.existsById(notificationId));
153+
154+
// When: Member 삭제
155+
entityManager.flush();
156+
entityManager.clear();
157+
Member memberToDelete = memberRepository.findById(memberId).orElseThrow();
158+
entityManager.remove(memberToDelete);
159+
entityManager.flush();
160+
161+
// Then: Notification도 함께 삭제되어야 함
162+
assertFalse(memberRepository.existsById(memberId));
163+
assertFalse(notificationRepository.existsById(notificationId));
164+
}
165+
166+
@Test
167+
@DisplayName("Member 삭제 시 LogBaseInfo도 함께 삭제되어야 함 (@OnDelete CASCADE)")
168+
void testMemberDeletionWithLogBaseInfo() {
169+
// Given: LogBaseInfo 생성
170+
LogBaseInfo logBaseInfo = LogBaseInfo.builder()
171+
.member(testMember)
172+
.name("테스트 로그")
173+
.iconType(IconType.CLOWNFISH)
174+
.date(LocalDate.now())
175+
.saveStatus(SaveStatus.COMPLETE)
176+
.build();
177+
logBaseInfo = logBaseInfoRepository.save(logBaseInfo);
178+
logBaseInfoRepository.flush();
179+
180+
Long memberId = testMember.getId();
181+
Long logBaseInfoId = logBaseInfo.getId();
182+
183+
// 저장 확인
184+
assertTrue(memberRepository.existsById(memberId));
185+
assertTrue(logBaseInfoRepository.existsById(logBaseInfoId));
186+
187+
// When: Member 삭제
188+
entityManager.flush();
189+
entityManager.clear();
190+
Member memberToDelete = memberRepository.findById(memberId).orElseThrow();
191+
entityManager.remove(memberToDelete);
192+
entityManager.flush();
193+
194+
// Then: LogBaseInfo도 함께 삭제되어야 함
195+
assertFalse(memberRepository.existsById(memberId));
196+
assertFalse(logBaseInfoRepository.existsById(logBaseInfoId));
197+
}
198+
199+
@Test
200+
@DisplayName("Member 삭제 시 모든 연관 데이터가 함께 삭제되어야 함 (통합 테스트)")
201+
void testMemberDeletionWithAllRelatedData() {
202+
// Given: 모든 연관 엔티티 생성
203+
Avatar avatar = Avatar.builder()
204+
.user(testMember)
205+
.name("테스트 버디")
206+
.build();
207+
avatar = avatarRepository.save(avatar);
208+
209+
DeviceSession session = DeviceSession.builder()
210+
.user(testMember)
211+
.refreshToken("test-refresh-token")
212+
.socialType(SocialType.APPLE)
213+
.deviceId("test-device-id")
214+
.build();
215+
session = deviceSessionRepository.save(session);
216+
217+
Notification notification = Notification.builder()
218+
.receiver(testMember)
219+
.type(NotificationType.SYSTEM)
220+
.message("테스트 알림")
221+
.isRead(false)
222+
.build();
223+
notification = notificationRepository.save(notification);
224+
225+
LogBaseInfo logBaseInfo = LogBaseInfo.builder()
226+
.member(testMember)
227+
.name("테스트 로그")
228+
.iconType(IconType.CLOWNFISH)
229+
.date(LocalDate.now())
230+
.saveStatus(SaveStatus.COMPLETE)
231+
.build();
232+
logBaseInfo = logBaseInfoRepository.save(logBaseInfo);
233+
234+
// flush to ensure all data is saved
235+
avatarRepository.flush();
236+
deviceSessionRepository.flush();
237+
notificationRepository.flush();
238+
logBaseInfoRepository.flush();
239+
240+
Long memberId = testMember.getId();
241+
Long avatarId = avatar.getId();
242+
Long sessionId = session.getId();
243+
Long notificationId = notification.getId();
244+
Long logBaseInfoId = logBaseInfo.getId();
245+
246+
// 모든 데이터 저장 확인
247+
assertTrue(memberRepository.existsById(memberId));
248+
assertTrue(avatarRepository.existsById(avatarId));
249+
assertTrue(deviceSessionRepository.existsById(sessionId));
250+
assertTrue(notificationRepository.existsById(notificationId));
251+
assertTrue(logBaseInfoRepository.existsById(logBaseInfoId));
252+
253+
// When: Member 삭제
254+
entityManager.flush();
255+
entityManager.clear();
256+
Member memberToDelete = memberRepository.findById(memberId).orElseThrow();
257+
entityManager.remove(memberToDelete);
258+
entityManager.flush();
259+
260+
// Then: 모든 연관 데이터가 함께 삭제되어야 함
261+
assertFalse(memberRepository.existsById(memberId), "Member가 삭제되어야 함");
262+
assertFalse(avatarRepository.existsById(avatarId), "Avatar가 삭제되어야 함");
263+
assertFalse(deviceSessionRepository.existsById(sessionId), "DeviceSession이 삭제되어야 함");
264+
assertFalse(notificationRepository.existsById(notificationId), "Notification이 삭제되어야 함");
265+
assertFalse(logBaseInfoRepository.existsById(logBaseInfoId), "LogBaseInfo가 삭제되어야 함");
266+
}
267+
268+
@Test
269+
@DisplayName("탈퇴 요청 후 Member 삭제 시나리오")
270+
void testDeactivatedMemberDeletion() {
271+
// Given: 탈퇴 요청한 회원
272+
testMember.requestDeletion();
273+
testMember = memberRepository.save(testMember);
274+
275+
// Avatar 생성
276+
Avatar avatar = Avatar.builder()
277+
.user(testMember)
278+
.name("테스트 버디")
279+
.build();
280+
avatar = avatarRepository.save(avatar);
281+
avatarRepository.flush();
282+
283+
assertEquals(Status.DEACTIVATED, testMember.getStatus());
284+
assertNotNull(testMember.getDeactivatedAt());
285+
286+
Long memberId = testMember.getId();
287+
288+
// When: 탈퇴 기간이 지나서 삭제
289+
entityManager.flush();
290+
entityManager.clear();
291+
Member memberToDelete = memberRepository.findById(memberId).orElseThrow();
292+
entityManager.remove(memberToDelete);
293+
entityManager.flush();
294+
295+
// Then: Member와 모든 연관 데이터 삭제됨
296+
assertFalse(memberRepository.existsById(memberId));
297+
}
298+
}

0 commit comments

Comments
 (0)