사용자 정보 변경

뚜우웅이·2025년 4월 8일

캡스톤 디자인

목록 보기
7/35

사용자 정보 변경

필요한 DTO 클래스 생성

UserDto

public class UserDto {

    public record ProfileUpdateRequest(
            @NotBlank(message = "이름은 필수 입력값입니다.")
            String name,

            @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$", message = "휴대폰 번호 형식이 올바르지 않습니다.")
            String phone
    ) {
    }

    public record PasswordChangeRequest(
            @NotBlank(message = "현재 비밀번호는 필수 입력값입니다.")
            String currentPassword,

            @NotBlank(message = "새 비밀번호는 필수 입력값입니다.")
            @Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.")
            String newPassword
    ) {
    }

    @Builder
    public record UserResponse(
            Long id,
            String email,
            String name,
            String phone,
            String role
    ) {
        public static UserResponse from(User user) {
            return UserResponse.builder()
                    .id(user.getId())
                    .email(user.getEmail())
                    .name(user.getName())
                    .phone(user.getPhone())
                    .role(user.getRole().getDisplayName())
                    .build();
        }
    }
}
  • from(User user) 정적 메서드는 User 엔티티 객체를 받아 UserResponse DTO로 변환하는 편의 메서드다.

  • from(User) 메서드로 엔티티에서 DTO로의 변환 로직을 캡슐화하여 애플리케이션 전체에서 일관된 변환을 보장한다.

사용자 서비스 클래스

UserService

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserDto.UserResponse getUserProfile(Long userId) {
        User user = getUser(userId);

        return UserDto.UserResponse.from(user);
    }

    @Transactional
    public UserDto.UserResponse updateProfile(Long userId, UserDto.ProfileUpdateRequest request) {
        User user = getUser(userId);
        user.updateProfile(request.name(), request.phone());

        log.info("사용자 프로필 업데이트 완료: {}", userId);

        return UserDto.UserResponse.from(user);
    }

    @Transactional
    public void changePassword(Long userId, UserDto.PasswordChangeRequest request) {
        User user = getUser(userId);

        // 현재 비밀번호 확인
        if (!passwordEncoder.matches(request.currentPassword(), user.getPassword())) {
            throw new UserException.PasswordMismatchException();
        }

        // 새 비밀번호가 현재 비밀번호와 동일한지 확인
        if (passwordEncoder.matches(request.newPassword(), user.getPassword())) {
            throw new UserException.PasswordSameAsOldException();
        }

        // 비밀번호 변경
        user.changePassword(passwordEncoder.encode(request.newPassword()));
        log.info("사용자 비밀번호 변경 완료: {}", userId);
    }

    private User getUser(Long userId) {
        return userRepository.findById(userId)
                .orElseThrow(() -> new UserException.UserNotFoundException(userId));
    }
}
  • 사용자 프로필 정보 조회
  • 사용자 프로필 정보 업데이트(이름, 전화번호)
  • 사용자 비밀번호 변경(현재 비밀번호 검증 포함)

각 메서드는 사용자 ID를 통해 해당 사용자를 식별하고, 적절한 비즈니스 로직을 수행한 후 필요한 경우 결과를 DTO 형태로 반환한다. 또한 중요한 작업은 로그로 기록하여 추적 가능성을 제공한다.

예외처리

    public static class PasswordMismatchException extends UserException {
        public PasswordMismatchException() {
            super("현재 비밀번호가 일치하지 않습니다.", HttpStatus.BAD_REQUEST, "PASSWORD_MISMATCH");
        }
    }

    public static class PasswordSameAsOldException extends UserException {
        public PasswordSameAsOldException() {
            super("새 비밀번호는 현재 비밀번호와 달라야 합니다.", HttpStatus.BAD_REQUEST, "PASSWORD_SAME_AS_OLD");
        }
    }

UserExceptionBaseException을 상속하고 있다. BaseExceptionGlobalExceptionHandler에서 처리를 하고 있기 때문에 개별 핸들러를 사용하지 않아도 된다.

사용자 컨트롤러

UserController

@Slf4j
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Tag(name = "사용자", description = "사용자 정보 관리 API")
public class UserController {

    private final UserService userService;

    @Operation(summary = "사용자 프로필 조회", description = "로그인한 사용자의 프로필 정보를 조회합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "프로필 조회 성공"),
            @ApiResponse(responseCode = "401", description = "인증 실패"),
            @ApiResponse(responseCode = "404", description = "사용자 정보를 찾을 수 없음")
    })
    @GetMapping("/me")
    public ResponseEntity<ResponseDTO<UserDto.UserResponse>> getProfile(
            @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails) {
        // 현재 인증된 사용자의 프로필 조회

        log.info("사용자 프로필 조회: {}", userDetails.getUserId());
        UserDto.UserResponse response = userService.getUserProfile(userDetails.getUserId());
        return ResponseEntity.ok(ResponseDTO.success(response, "프로필 조회에 성공했습니다."));
    }

    @Operation(summary = "사용자 프로필 수정", description = "로그인한 사용자의 이름과 전화번호를 수정합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "프로필 수정 성공"),
            @ApiResponse(responseCode = "400", description = "잘못된 요청"),
            @ApiResponse(responseCode = "401", description = "인증 실패"),
            @ApiResponse(responseCode = "404", description = "사용자 정보를 찾을 수 없음")
    })
    @PatchMapping("/me")
    public ResponseEntity<ResponseDTO<UserDto.UserResponse>> updateProfile(
            @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
            @Parameter(description = "프로필 수정 정보", required = true)
            @Valid @RequestBody UserDto.ProfileUpdateRequest request) {

        log.info("사용자 프로필 수정: {}", userDetails.getUserId());
        UserDto.UserResponse response = userService.updateProfile(userDetails.getUserId(), request);
        return ResponseEntity.ok(ResponseDTO.success(response, "프로필이 성공적으로 수정되었습니다."));
    }

    @Operation(summary = "비밀번호 변경", description = "현재 비밀번호 확인 후 새 비밀번호로 변경합니다.")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "비밀번호 변경 성공"),
            @ApiResponse(responseCode = "400", description = "잘못된 요청 또는 현재 비밀번호 불일치"),
            @ApiResponse(responseCode = "401", description = "인증 실패"),
            @ApiResponse(responseCode = "404", description = "사용자 정보를 찾을 수 없음")
    })
    @PostMapping("/me/password")
    public ResponseEntity<ResponseDTO<Void>> changePassword(
            @Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails userDetails,
            @Parameter(description = "비밀번호 변경 정보", required = true)
            @Valid @RequestBody UserDto.PasswordChangeRequest request) {

        log.info("비밀번호 변경 요청: {}", userDetails.getUserId());
        userService.changePassword(userDetails.getUserId(), request);
        return ResponseEntity.ok(ResponseDTO.success(null, "비밀번호가 성공적으로 변경되었습니다."));
    }
}
  • @Parameter(hidden = true): Swagger 문서에서 이 파라미터를 숨긴다.
  • @AuthenticationPrincipal: Spring Security에서 현재 인증된 사용자 정보를 주입한다.

테스트

사용자 프로필 조회 테스트

사용자 프로필 수정 테스트


사용자 정보는 Spring Security에서 받아온 정보를 토대로 수정을 한다.

비밀번호 변경 테스트

실패케이스

현재 비밀번호 틀림

새로운 비밀번호가 현재 비밀번호와 일치

profile
공부하는 초보 개발자

0개의 댓글