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");
}
}
UserException은 BaseException을 상속하고 있다. BaseException은 GlobalExceptionHandler에서 처리를 하고 있기 때문에 개별 핸들러를 사용하지 않아도 된다.
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에서 받아온 정보를 토대로 수정을 한다.

현재 비밀번호 틀림

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