회원 탈퇴 기능 추가

Jongwon·2023년 4월 9일
1

DMS

목록 보기
16/18

이번엔 회원 탈퇴 기능을 만들어보겠습니다.

회원 탈퇴 시에 회원과 연관되어 있는 사진, PetOwner 엔티티가 같이 삭제되어야 하고, 만약 애완견의 유일한 소유자라면 애완견, 애완견 사진도 같이 삭제되어야 합니다.

돌이켜 생각해보면 연관된 엔티티 모두를 삭제하는 OrphanRemoval = true를 잘 설정해서 사용하면 좀 더 간결한 코드가 될 것이라 생각하지만, 연관관계로 묶인 것이 많아 이후에 수정하도록 하겠습니다.


먼저 LoginRequestDTO를 IdPasswordDTO로 수정하였습니다. 회원 탈퇴 시 비밀번호를 한번 더 묻고 탈퇴하는 절차로 진행하는데, 로그인 시 사용했던 AuthService를 활용한다면 중복된 코드없이 회원탈퇴에도 사용할 수 있기 때문입니다.

IdPasswordDTO(LoginRequestDTO에서 변경)

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequestDTO {

    private String userId;
    private String password;

    public UsernamePasswordAuthenticationToken toAuthentication() {
        return new UsernamePasswordAuthenticationToken(userId, password);
    }
}

AuthService의 메서드도, Login에 한정하지 않는다는 의미로 authenticatePassword()로 변경하겠습니다.

AuthService

@Service
@RequiredArgsConstructor
public class AuthService {

    private final AuthenticationManagerBuilder authenticationManagerBuilder;

    public void authenticatePassword(IdPasswordDTO requestDTO) {
        UsernamePasswordAuthenticationToken authenticationToken = requestDTO.toAuthentication();
        authenticationManagerBuilder.getObject().authenticate(authenticationToken);
    }
}

다음으로 ImageRepository를 상속받는 구조를 변경하였습니다. ImageRepository에서 Jpa를 상속받으면, Image엔티티에 대해서만 삭제 처리가 되기 때문에 MemberImage, PetImage가 인식되지 않는 문제가 발생하였습니다. 따라서 두 Repository가 JpaRepository를 직접적으로 상속하도록 변경하였습니다.


Repository

MemberImageRepository

public interface MemberImageRepository extends JpaRepository<MemberImage, Long> {
    void deleteById(Long id);
}

PetImageRepository

@Repository
public interface PetImageRepository extends JpaRepository<PetImage, List> {

    @Query("select pi from PetImage pi where pi.petDog.id = :petId")
    List<PetImage> findAllPetImages(@Param("petId") Long petId);

    @Query("select pi from PetImage pi where id = :imageId")
    PetImage findByImageId(Long imageId);

    void deleteById(Long imageId);
}

다음으로 MemberRepository에 delete 메서드를 추가합니다.

MemberRepository

@Repository
public interface MemberRepository extends JpaRepository<Member, String> {

...
    @OnDelete(action = OnDeleteAction.CASCADE)
    void deleteByUserId(String userId);
}

앞서 언급했듯이 회원이 삭제되기 위해서는 가장 먼저 연관되어있는 PetOwn이 삭제되어야 합니다. PetOwn을 통해 회원이 소유한 애완견을 모두 조회하고, 이 애완견들 각각에 대해 소유주가 삭제되는 회원 단독으로 있는지를 확인합니다. 단독이라면 애완견도 같이 삭제해야 합니다.

PetOwnRepository

public interface PetOwnRepository extends JpaRepository<PetOwner, PetOwnerId> {

    //join fetch 이용하여 petDog fetch
    @Query("select po.petDog"
            + " from PetOwner po"
            + " where po.member.userId = :userId")
    List<PetDog> findAllByMember(@Param("userId") String userId);

    @Query("select distinct m"
            + " from PetOwner po"
            + " inner join PetDog p"
            + " inner join Member m"
            + " on p.petId = :petId")
    List<Member> findAllByPet(@Param("petId") Long petId);

    @Modifying
    @Transactional
    void deleteAllByMember(Member member);
}

현재 Redis DB를 구현하지 않아 RefreshToken도 직접 삭제하겠습니다.

RefreshTokenRepository

@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {

    Optional<RefreshToken> findByMember(Member member);

    @Modifying
    void deleteByMember(Member member);
}

Service

다음으로 애완견 삭제 시 애완견 + 해당 애완견의 사진을 삭제하는 로직을 생성하겠습니다.

PetService

@Service
@RequiredArgsConstructor
@Log4j2
public class PetService {

    private final PetRepository petRepository;
    private final PetOwnRepository petOwnRepository;
    private final FileService fileService;
    private final PetImageRepository imageRepository;
    private final BreedService breedService;
    private final MemberRepository memberRepository;

...
    @Transactional
    public void deletePet(PetDog petDog) {
        List<PetImage> images = imageRepository.findAllPetImages(petDog.getPetId());
        petDog.setProfileImage(null);
        images.stream().forEach(petImage -> imageRepository.deleteById(petImage.getId()));
        petRepository.deleteById(petDog.getPetId());

    }
}

Pet이 가진 Image를 모두 조회하여 각각을 삭제합니다.

프로필 사진을 비우고 처리하는 방식 역시, orphanRemoval를 사용하면 간단하게 지워지겠지만 이후에 적용하도록 하겠습니다.


MemberService에서도 회원과 관련된 모든 정보를 삭제합니다.

MemberService

@Service
@Log4j2
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {

    private final MemberRepository memberRepository;
    private final MemberImageRepository imageRepository;
    private final TokenService tokenService;
    private final FileService fileService;
    private final AuthService authService;
    private final PetOwnRepository petOwnRepository;
    private final PetService petService;
    private final RefreshTokenRepository tokenRepository;

    @Value("${spring.servlet.multipart.location}")
    private String uploadPath;

...

//private로 변경하여 엔티티를 반환하는 메서드는 외부에서 참조하지 못하도록 함
    private Member findMemberByUserId(String userId) {
        return memberRepository.findByUserIdEagerLoadImage(userId)
                .orElseThrow(() -> new RuntimeException("해당 ID를 가진 사용자가 존재하지 않습니다."));
    }

...

//파라미터를 LoginRequestDTO -> IdPasswordDTO로 변경. 메서드명도 변경
    @Transactional
    public TokenDTO login(IdPasswordDTO requestDTO) {
        authService.authenticatePassword(requestDTO);

        Member member = memberRepository.findByUserId(requestDTO.getUserId()).get();
        return tokenService.createToken(member);
    }

...

//삭제 로직 추가
    @Transactional(readOnly = false)
    public HttpStatus deleteUser(String userId, String password) {
        Member member = memberRepository.findByUserIdEagerLoadImage(userId).orElseThrow(() -> new RuntimeException("존재하지 않는 사용자입니다."));

        IdPasswordDTO idPasswordDTO = IdPasswordDTO.builder()
                .userId(userId)
                .password(password)
                .build();

        authService.authenticatePassword(idPasswordDTO);

        List<PetDog> petList = petOwnRepository.findAllByMember(userId);
        petOwnRepository.deleteAllByMember(member);

        petList.stream().forEach(petDog -> {
            if(petOwnRepository.findAllByPet(petDog.getPetId()).isEmpty()) {
                petService.deletePet(petDog);
            }
        });

        if(member.getMemberImage() != null) {
            imageRepository.deleteById(member.getMemberImage().getId());
        }

        tokenRepository.deleteByMember(member);
        memberRepository.deleteByUserId(userId);

        return HttpStatus.OK;
    }
}

추가적으로 변경된 코드에 대한 수정도 진행하였습니다.


Controller

마지막으로 MemberController에 삭제 API를 추가합니다.

MemberController

@Tag(name = "사용자 관련", description = "사용자 관련 API")
@RestController
@Log4j2
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

...

    @Operation(summary = "회원 탈퇴")
    @DeleteMapping("/delete")
    public ResponseEntity memberDelete(@RequestBody String password) {
        String userId = SecurityUtil.getCurrentUsername();

        HttpStatus httpStatus = memberService.deleteUser(userId, password);

        return new ResponseEntity<>(httpStatus);
    }
}


참고자료

https://velog.io/@hyojhand/JPA-deleteById-vs-delete
https://velog.io/@sun1203/Spring-Boot-JPA-%ED%8A%B9%EC%A0%95-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%82%AD%EC%A0%9C%EC%8B%9C-%EC%97%B0%EA%B4%80%EB%90%9C-%EC%97%94%ED%8B%B0%ED%8B%B0%EB%8F%84-%ED%95%A8%EA%BB%98-%EC%82%AD%EC%A0%9C

profile
Backend Engineer

0개의 댓글