개발 기록/도서관 관리 시스템

[Library Management System] 25.09.26 (55일) | (리팩토링) 사용자 본인 인증 정보, 본인 이메일/비밀번호 변경, 회원 탈퇴

dev.jelee 2025. 9. 26. 12:42

[ 작업한 내용 ]

< 사용자: 본인 인증 정보 >

# UserInfoResDTO

- 사용자 본인 인증 정보 조회 응답 DTO 정의

- 사용자 아이디, 이메일, 가입 날짜, 마지막 로그인 날짜 (username, email, joinDate, lastLoginDate)

- User 엔티티를 파라미터로 받는 생성자 함수 정의

@Getter
public class UserInfoResDTO {
  private String username;
  private String email;
  private LocalDateTime joinDate;
  private LocalDateTime lastLoginDate;

  public UserInfoResDTO(User user) {
    this.username = user.getUsername();
    this.email = user.getEmail();
    this.joinDate = user.getJoinDate();
    this.lastLoginDate = user.getLastLoginDate();
  }
}

# UserController

- GET /api/v1/user/me 로 요청합니다.

- 파라미터로 @AuthenticationPrincipal 형태로 사용자 정보를 가져와 User 엔티티 형태로 저장합니다.

- Servicer 계층으로 user.getId()를 전달합니다.

- 결과는 UserInfoResDTO 형태로 저장합니다.

- 클라이언트 측으로 성공 메시지와 결과 데이터를 ApiResponse.success()로 감싸서 반환합니다.

@GetMapping()
public ResponseEntity<?> getMyInfo(@AuthenticationPrincipal User user) {

  // 서비스 로직
  UserInfoResDTO responseDTO = userService.getMyInfo(user.getId());

  // 성공메시지
  String message = messageProvider.getMessage(AuthSuccessCode.AUTH_USER_VERIFIED.getMessage());

  // 반환
  return ResponseEntity
            .status(AuthSuccessCode.AUTH_USER_VERIFIED.getHttpStatus())
            .body(ApiResponse.success(
              AuthSuccessCode.AUTH_USER_VERIFIED, 
              message, 
              responseDTO));
}

# UserService

- userRepository.findById(userId)로 사용자 정보를 조회한 후 User 형태로 저장합니다. 사용자 정보가 유효하지 않다면 UserErrorCode.USER_NOT_FOUND 예외를 던집니다.

- Controller 계층으로 user 데이터를 기반으로 UserInfoResDTO 객체를 생성해 반환합니다.

@Transactional(readOnly = true)
public UserInfoResDTO getMyInfo(Long userId) {

  // User 조회 및 객체 생성, 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));

  // 반환
  return new UserInfoResDTO(user);
}

< 사용자: 본인 이메일 주소 변경 >

# UpdateEmailReqDTO

- 사용자 이메일 주소 변경 요청 DTO 정의

- 이메일 (email)

@Getter
public class UpdateEmailReqDTO {
  @Email
  private String email;
}

# UpdateEmailResDTO

- 사용자 이메일 주소 변경 응답 DTO 정의

- 사용자 고유번호, 사용자 아이디, 이메일, 수정 날짜 (id, username, email, updatedAt)

- User 엔티티를 파라미터로 받는 생성자 함수 정의

@Getter
public class UpdateEmailResDTO {
  private Long id;
  private String username;
  private String email;
  private LocalDateTime updatedAt;

  public UpdateEmailResDTO(User user) {
    this.id = user.getId();
    this.username = user.getUsername();
    this.email = user.getEmail();
    this.updatedAt = user.getUpdatedAt();
  }
}

# UserController

- PATCH /api/v1/user/me/email 로 요청합니다.

- 파라미터로 UpdateEmailReqDTO(email 필드)을 전달받고, @AuthenticationPrincipal 형태로 사용자 정보를 가져와 User 엔티티 형태로 저장합니다.

- Servicer 계층으로 user.getId()와 requestDTO(email)를 전달합니다.

- 결과는 UpdateEmailResDTO 형태로 저장합니다.

- 클라이언트 측으로 성공 메시지와 결과 데이터를 ApiResponse.success()로 감싸서 반환합니다.

@PatchMapping("/email")
public ResponseEntity<?> updateEmail(
  @RequestBody UpdateEmailReqDTO requestDTO, 
  @AuthenticationPrincipal User user) {
  
    // 서비스로직
    UpdateEmailResDTO responseDTO = userService.updateEmail(user.getId(), requestDTO);

    // 성공메시지
    String message = messageProvider.getMessage(UserSuccessCode.USER_EMAIL_UPDATE.getMessage());

    // 응답
    return ResponseEntity
              .status(UserSuccessCode.USER_EMAIL_UPDATE.getHttpStatus())
              .body(ApiResponse.success(
                UserSuccessCode.USER_EMAIL_UPDATE, 
                message, 
                responseDTO));
}

# UserService

- userRepository.findById(userId)로 사용자 정보를 조회한 후 User 형태로 저장합니다. 사용자 정보가 유효하지 않다면 UserErrorCode.USER_NOT_FOUND 예외를 던집니다.

- 변경하려는 이메일 주소가 기존 이메일과 동일한지 체크하고 동일하면 UserErrorCode.USER_EMAIL_SAME 예외를 던집니다.

- 변경하려는 이메일 주소가 중복되는지 체크하고 중복된다면 UserErrorCode.USER_EMAIL_DUPLICATED 예외를 던집니다.

- User 객체에 새로운 이메일 주소와 변경 날짜를 저장하고 userRepository.save(user)을 사용해 DB에 저장합니다.

- Controller 계층으로 user 데이터를 기반으로 UpdateEmailResDTO 객체를 생성해 반환합니다.

@Transactional
public UpdateEmailResDTO updateEmail(Long userId, UpdateEmailReqDTO requestDTO) {

  // 사용자 조회 및 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));
  
  // 기존과 변경 이메일이 동일한지 체크
  String newEmail = requestDTO.getEmail();
  if (user.getEmail().equals(newEmail)) {
    throw new BaseException(UserErrorCode.USER_EMAIL_SAME);
  }

  // 이메일 중복 체크
  if (userRepository.existsByEmail(newEmail)) {
    throw new BaseException(UserErrorCode.USER_EMAIL_DUPLICATED);
  }

  // user객체에 변경할 이메일, 수정된 날짜 저장 후 DB에 user객체 저장.
  user.setEmail(newEmail);
  user.setUpdatedAt(LocalDateTime.now());
  userRepository.save(user);

  // 반환
  return new UpdateEmailResDTO(user);
}

< 사용자: 본인 비밀번호 변경 >

# UpdatePasswordReqDTO

- 사용자 비밀번호 변경 요청 DTO 정의

- 변경할 비밀번호, 변경할 비밀번호 확인용 (password, repassword)

@Getter
public class UpdatePasswordReqDTO {
  private String password;
  private String repassword;
}

# UpdatePasswordResDTO

- 사용자 비밀번호 변경 응답 DTO 정의

- 사용자 고유번호, 사용자 아이디, 수정 날짜 (id, username, updatedAt)

- User 엔티티를 파라미터로 받는 생성자 함수 정의

@Getter
public class UpdatePasswordResDTO {
  private Long id;
  private String username;
  private LocalDateTime updatedAt;

  public UpdatePasswordResDTO(User user) {
    this.id = user.getId();
    this.username = user.getUsername();
    this.updatedAt = user.getUpdatedAt();
  }
}

# UserController

- PATCH /api/v1/user/me/email 로 요청합니다.

- 파라미터로 UpdatePasswordReqDTO(password, repassword)를 전달받고, @AuthenticationPrincipal 형태로 사용자 정보를 가져와 User 엔티티 형태로 저장합니다.

- Servicer 계층으로 user.getId()와 requestDTO(password, repassword)를 전달합니다.

- 결과는 UpdatePasswordResDTO 형태로 저장합니다.

- 클라이언트 측으로 성공 메시지와 결과 데이터를 ApiResponse.success()로 감싸서 반환합니다.

@PatchMapping("/password")
public ResponseEntity<?> updatePassword(
  @RequestBody UpdatePasswordReqDTO requestDTO, 
  @AuthenticationPrincipal User user) {
  
    // 서비스로직
    UpdatePasswordResDTO responseDTO = userService.updatePassword(user.getId(), requestDTO);

    // 성공메시지
    String message = messageProvider.getMessage(UserSuccessCode.USER_PASSWORD_UPDATE.getMessage());
  
  return ResponseEntity
            .status(UserSuccessCode.USER_PASSWORD_UPDATE.getHttpStatus())
            .body(ApiResponse.success(
              UserSuccessCode.USER_PASSWORD_UPDATE, 
              message, 
              responseDTO));
}

# UserService

- userRepository.findById(userId)로 사용자 정보를 조회한 후 User 형태로 저장합니다. 사용자 정보가 유효하지 않다면 UserErrorCode.USER_NOT_FOUND 예외를 던집니다.

- 새로운 비밀번호와 확인용 새로운 비밀번호를 변수에 각각 저장합니다.

- 기존 비밀번호와 새로운 비밀번호가 동일한지 체크합니다. 만약 동일하면 UserErrorCode.USER_PASSWORD_SAME 예외를 던집니다.

- 새로운 비밀번호와 확인용 새 비밀번호가 동일한지 체크하고 만약 동일하지 않으면 UserErrorCode.USER_PASSWORD_MISMATCH 예외를 던집니다.

- 새로운 비밀번호를 passwordEncoder.encode(newPassword)를 사용하여 암호화 하여 변수에 저장합니다.

- 암호화한 비밀번호는 User 객체에 저장하고, 수정날짜도 저장합니다.

- userRepository.save(user)를 사용하여 DB에 비밀번호와 수정날짜를 저장합니다.

- Controller 계층으로 user 데이터를 기반으로 UpdatePasswordResDTO 객체를 생성해 반환합니다.

@Transactional
public UpdatePasswordResDTO updatePassword(Long userId, UpdatePasswordReqDTO requestDTO) {

  // 사용자 조회 및 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));
  
  // 새로운 비밀번호, 확인용 새로운 비밀번호를 변수에 저장
  String newPassword = requestDTO.getPassword();
  String rePassword = requestDTO.getRepassword();
  
  // 기존 비밀번호와 새로운 비밀번호가 동일한지 체크
  if (passwordEncoder.matches(newPassword, user.getPassword())) {
    throw new BaseException(UserErrorCode.USER_PASSWORD_SAME);
  }

  // 새 비밀번호와 확인용 새 비밀번호가 동일한지 체크
  if (!newPassword.equals(rePassword)) {
    throw new BaseException(UserErrorCode.USER_PASSWORD_MISMATCH);
  }

  // 새로운 비밀번호 암호화, 수정된 날짜 저장 후 DB에 user 객체 저장.
  String encodedNewPassword = passwordEncoder.encode(newPassword);
  user.setPassword(encodedNewPassword);
  user.setUpdatedAt(LocalDateTime.now());
  userRepository.save(user);

  // 반환
  return new UpdatePasswordResDTO(user);
}

< 사용자: 회원 탈퇴 >

# DeleteAccountReqDTO

- 사용자 회원 탈퇴 요청 DTO 정의

- 현재 비밀번호 (password)

@Getter
public class DeleteAccountReqDTO {
  private String password;
}

# DeleteAccountResDTO

- 사용자 회원 탈퇴 응답 DTO 정의

- 사용자 고유번호, 사용자 아이디, 상태, 삭제 날짜 (id, username, status, inactiveAt)

- User 엔티티를 파라미터로 받는 생성자 함수 정의

@Getter
public class DeleteAccountResDTO {
  private Long id;
  private String username;
  private UserStatus status;
  private LocalDateTime inactiveAt;

  public DeleteAccountResDTO(User user) {
    this.id = user.getId();
    this.username = user.getUsername();
    this.status = user.getStatus();
    this.inactiveAt = user.getInactiveAt();
  }
}

# UserController

- POST /api/v1/user/me/withdraw 로 요청합니다.

- 파라미터로 DeleteAccountReqDTO(password)를 전달받고, @AuthenticationPrincipal 형태로 사용자 정보를 가져와 User 엔티티 형태로 저장합니다.

- Servicer 계층으로 user.getId()와 requestDTO(password)를 전달합니다.

- 결과는 DeleteAccountResDTO 형태로 저장합니다.

- HttpServletResonse를 사용하여 Cookie에 저장될 JWT를 다시 설정합니다. 이때 토큰은 빈 값으로 정하고 .maxAge()를 0으로 설정합니다. 그 다음 응답시 쿠키를 전달합니다.

- 클라이언트 측으로 성공 메시지와 결과 데이터를 ApiResponse.success()로 감싸서 반환합니다.

@PostMapping("/withdraw")
public ResponseEntity<?> deleteAccount(
  @RequestBody DeleteAccountReqDTO requestDTO,
  @AuthenticationPrincipal User user,
  HttpServletResponse response) {
  
    // 서비스로직
    DeleteAccountResDTO responseDTO = userService.deleteAccount(user.getId(), requestDTO);

    // 쿠키 삭제
    ResponseCookie deleteCookie = ResponseCookie.from("JWT", "")
              .httpOnly(true)
              .secure(true)
              .path("/")
              .maxAge(0)
              .sameSite("Strict")
              .build();
    
    // 응답시 전달할 쿠키
    response.addHeader("Set-Cookie", deleteCookie.toString());

    // 성공메시지
    String message = messageProvider.getMessage(UserSuccessCode.USER_ACCOUNT_DELETED.getMessage());

    // 응답
    return ResponseEntity
              .status(UserSuccessCode.USER_ACCOUNT_DELETED.getHttpStatus())
              .body(ApiResponse.success(
                UserSuccessCode.USER_ACCOUNT_DELETED, 
                message, 
                responseDTO));
}

# UserService

- userRepository.findById(userId)로 사용자 정보를 조회한 후 User 형태로 저장합니다. 사용자 정보가 유효하지 않다면 UserErrorCode.USER_NOT_FOUND 예외를 던집니다.

- 요청받은 비밀번호와 로그인한 사용자의 비밀번호가 일치한지 확인 후 일치하지 않으면 UserErrorCode.INVALID_PASSWORD 예외를 던집니다.

- User 객체에 상태를 INACTIVE로 저장하고 inactiveAt도 현재 날짜로 저장 후, userRepository.save(user)를 사용하여 DB에 User를 저장합니다.

- Controller 계층으로 user 데이터를 기반으로 DeleteAccountResDTO 객체를 생성해 반환합니다.

@Transactional
public DeleteAccountResDTO deleteAccount(Long userId, DeleteAccountReqDTO requestDTO) {

  // 사용자 조회 및 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));
  
      
  // 비밀번호가 일치하는지 체크
  String password = requestDTO.getPassword();
  if (!passwordEncoder.matches(password, user.getPassword())) {
    throw new BaseException(UserErrorCode.INVALID_PASSWORD);
  }

  // INACTIVE로 상태 변경 + inactiveAt 시각 저장 후 DB에 user 객체 업데이트 
  user.setStatus(UserStatus.INACTIVE);
  user.setInactiveAt(LocalDateTime.now());
  userRepository.save(user);

  // 반환
  return new DeleteAccountResDTO(user);
}

# UserRepository

- INACTIVE 상태이고, inactiveAt이 30일 지난 사용자를 찾는 메서드를 정의합니다.

List<User> findByStatusAndInactiveAtBefore(UserStatus status, LocalDateTime inactiveAt);

# UserStatusScheduler

- @Scheduled 어노테이션을 사용하여 updateInactiveUsersToDeleted()가 매일 새벽 3시에 자동으로 실행되도록 합니다.

- updateInactiveUsersToDeleted() 메서드는 다음과 같은 내용의 코드가 작성되어 있습니다.

- 사용자의 상태가 INACTIVE이고 INACTIVE 상태가 된지 30일 지난 사용자를 찾아서 usersToDelete 변수에 저장합니다.

- User 형태로 특정 사용자의 정보 중 상태를 DELETED로 변경하고 삭제 변경 날짜도 현재로 변경해준 뒤 userRepository.save(user)를 사용하여 DB에 저장합니다.

@Slf4j
@Component
@RequiredArgsConstructor
public class UserStatusScheduler {
  
  private final UserRepository userRepository;

  // 매일 새벽 3시에 실행 (corn 표현식: 초 분 시 일 월 요일)
  @Scheduled(cron = "0 0 3 * * *")
  @Transactional
  public void updateInactiveUsersToDeleted() {
    log.info("UserStatusScheduler started - update");

    LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30);

    // INACTIVE 상태면서 inactiveAt이 30일 지난 유저 찾기
    var usersToDelete = userRepository.findByStatusAndInactiveAtBefore(UserStatus.INACTIVE, cutoffDate);

    for (User user : usersToDelete) {
      user.setStatus(UserStatus.DELETED);
      user.setDeletedAt(LocalDateTime.now());
      userRepository.save(user);
      log.info("User id={} status updated to DELETED", user.getId());
    }

    log.info("UserStatusScheduler finished");
  }
}

commit
사용자 정보 조회 성공 화면(좌), 사용자 본인 이메일 변경 성공 화면(우)
사용자 본인 비밀번호 변경 성공 화면(좌), 사용자 회원탈퇴 성공 화면(우)