내 서비스는 소셜 로그인, 자체 로그인 2가지가 존재한다.
소셜 로그인의 경우 회원 탈퇴를 진행할 때 비밀번호 입력이 필요하지 않다.
반면에 자체 로그인의 경우 비밀번호 입력이 필요하다.
그래서 로그인하면 유저 정보를 가져올 때 소셜 유저인지 판단해서 Pinia에 저장해 두었다.
const social = computed(() => store.social);
그리고 먼저 소셜 유저면 true를 리턴하고 소셜 유저가 아니면 비밀번호 입력칸 체크부터 진행했다.
// 비밀번호 입력 확인 -> 모달 띄우기
const checkInput = () => {
if (social.value) {
showModal.value = true;
return;
}
if (password.value.length === 0) {
alert("비밀번호를 입력해주세요.");
} else if (passwordConfirm.value.length === 0) {
alert("비밀번호 확인을 입력해주세요.");
} else if (password.value != passwordConfirm.value) {
alert("비밀번호 확인이 일치하지 않습니다.");
} else {
showModal.value = true;
}
return;
};
그리고 실제 회원 탈퇴 API를 호출하도록 구현했다.
// 탈퇴하기 진행
const withdrawAPI = async () => {
if (checkInput) {
const socialType = social.value ? "KAKAO" : "GRNERAL";
try {
const data = await apiPatch("api/members/deactivate", {
socialType: socialType,
password: password.value,
});
alert(data.message);
store.logout();
router.push("/login");
} catch (error) {}
}
};
현재 소셜 유저가 KAKAO밖에 없으므로 삼항 연산자를 이용해서 집어넣었다.
(이거 사실 나중에 확장성 생각하면 다 뜯어내야 하는 코드인데...)
그리고 api를 날리는데 비밀번호와 소셜타입을 같이 넘긴다.
컨트롤러에서는 DTO를 받아서 소셜 타입을 꺼내 소셜 탈퇴인지, 일반 탈퇴인지 구분 지었다.
두 개의 서비스 차이점이라고 한다면 비밀번호를 암호화해서 체킹 하냐의 차이가 있다.
// 유저 탈퇴
@PatchMapping("members/deactivate")
public ResponseEntity<ApiResponse<Void>> deactivateMember(@RequestBody @Valid DeactivateRequestDTO requestDTO) {
SocialType socialType = requestDTO.getSocialType();
switch (socialType) {
case KAKAO -> memberService.deactivateSocialMember();
case GENERAL -> memberService.deactivateGeneralMember(requestDTO);
default -> throw new IllegalArgumentException("9000 타입 에러 발생. 관리자에게 문의가 필요합니다.");
}
return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.createSuccessNoContent("회원 탈퇴에 성공했습니다."));
}
아래는 소셜 유저의 회원탈퇴
// 소셜 유저 탈퇴
@Override
@Transactional
public void deactivateSocialMember() {
Member currentMember = getCurrentMember();
currentMember.updateWithdrawalDate();
}
아래는 일반 유저의 회원 탈퇴 로직이다.
// 일반 유저 탈퇴
@Override
@Transactional
public void deactivateGeneralMember(DeactivateRequestDTO requestDTO) {
Member currentMember = getCurrentMember();
if (!encoder.matches(requestDTO.getPassword(), currentMember.getPassword())) {
throw new MemberException(MemberExceptionInfo.NOT_MATCH_PASSWORD, currentMember.getEmail() + " 유저 비밀번호 불일치 발생(회원 탈퇴)");
}
currentMember.updateWithdrawalDate();
}
updateWithdrawalDate는 탈퇴 날짜를 추가하는 메서드이다.
고민점
어제 탈퇴에 대해서 한 분이랑 의견을 나눴을 때 2가지 방안을 제안해 주셨다.
1. int로 field를 만들어서 값을 추가한다.
-> 해당 int 값은 재가입을 몇 번 했는지 판단하는 기준이 된다.
2. 30일이 지나면 계정 물리적 삭제, 이전에는 재가입 불가능.
-> 즉 30일 지나면 데이터베이스에서 지우는데, 30일 이전에도 재가입이 불가능하다.
우선 내가 생각한 서비스의 특성은 한번 탈퇴를 진행하면 절대 재가입이 불가능하는 게 원칙이었다.
그런데 데이터 물리적 삭제를 진행할 경우 나중에 재가입이 가능하지 않을까? 그래서 탈퇴가 진행된 유저의 테이블을 따로 만들기로 결정했다.
탈퇴 테이블 추가
@Entity
@Getter
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode
public class WithdrawalMember {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String email;
@Column(nullable = true)
@CreatedDate
private LocalDateTime withdrawalDate;
public WithdrawalMember(String email) {
this.email = email;
}
}
이메일, 30일이 지난 탈퇴 시간을 기록한다.
만약에 30일이 지나서 탈퇴됐는데 로그인 시도를 한다면?
// 이메일 중복 확인
@Override
@Transactional(readOnly = true)
public boolean existEmailCheck(String email) {
Optional<WithdrawalMember> byEmail = withdrawalMemberRepository.findByEmail(email);
if (byEmail.isPresent()){
throw new MemberException(MemberExceptionInfo.WITHDRAWAL_MEMBER, email + "로 회원가입을 시도했습니다.");
}
return memberRepository.existsByEmail(email);
}
이렇게 예외를 던지도록 설정했다.
로그인은 동일하게 불가능하다고 나올 것.
추가 로직
회원 탈퇴 시간은 삽입이 완료됐다. 그리고 탈퇴 시간이 존재하면 해당 유저의 로그인도 불가능하도록 막았다.
그러면 이제 30일이 지나면 해당 유저의 데이터를 삭제하는 로직이 필요하다.
스케줄러를 돌려야 한다. 근데 언제마다 판단을 진행하지?
일단 매일 6시간마다 탈퇴 회원이 있는지 판단하도록 한다.
그럼 유저의 실제 데이터를 물리적으로 삭제한다면 해당 유저와 관련된 데이터를 전부 지워야 한다.
근데 내가 cascade 옵션을 전혀 걸어두지 않았다는 것!!
그래서 Member와 관련된 모든 로직을 파보려고 한다.
1. RefreshToken(리프레시 토큰)
2. MemberRole(멤버 권한)
3. CouponHistory(쿠폰 획득 기록)
4. ApiRequestHistory(api 사용 기록)
이 모든 데이터를 삭제해야 한다.
// 매일 6시간 간격으로 탈퇴 유저 30일 지났는지 판단
@Scheduled(cron = "0 32 0 * * *", zone = "Asia/Seoul")
@Transactional
public void withdrawalMember(){
LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
List<Member> byWithdrawalMember = memberRepository.findByWithdrawalMember(thirtyDaysAgo);
for(Member m : byWithdrawalMember){
memberRepository.delete(m);
withdrawalMemberRepository.save(WithdrawalMember.builder().email(m.getEmail()).build());
}
}
이렇게 탈퇴한 지 30일이 넘은 유저를 리스트로 찾고 리스트를 돌면서 유저 물리 삭제를 진행해 준다.
그리고 블랙리스트에 추가.
현재 유저로 api 요청 더미데이터 25만 개를 넣어뒀더니 25만 개의 쿼리가 나갔다 ㅋㅋㅋㅋㅋ 아무래도 이건 아닌 것 같은데
일단 연관 데이터도 전부 삭제됐고, 블랙리스트도 정상적으로 추가된 것을 확인할 수 있다.
더 이상 회원가입도 불가능하다.
25만 개의 delete 쿼리... 어떻게 처리해야 할까? 스프링 배치를 써야 하나? 뭘까 모르겠다..(내일 하자)
'프로젝트 > RESTAPI 추천 서비스' 카테고리의 다른 글
기록 요청에 필요한 데이터만 뽑아내자(Feat, QureyDsl) (0) | 2024.05.31 |
---|---|
이메일 전송을 비동기로 처리하기 (Feat, @Async) (3) | 2024.05.30 |
pem key를 삭제해버렸다. 다시 재발급을 받아야 한다.(아) (0) | 2024.05.24 |
onMounted()에 걸어놓은 함수가 계속 실행되는 문제 (0) | 2024.05.24 |