저번시간에 mock객체를 이용한 테스트코드에 대해 블로그를 적었었다.
테스트 코드를 반영하면서 올린 기존 코드가 수정된 부분이 많아서
참을 수 없어 호다닥 글을 적어보려고 한다.
리팩토링할 수 있어서 행복한 코드의 세계~
목표 : http form 데이터 전송으로 처리하던 로직 -> rest api로 변경하기 위해 json형식으로 데이터를 주고 받기.
목표대로 수정하다보니 하나 둘 씩 기존의 코드의 다른 문제점들도 보이기 시작했는데
1. 의미없는 optional 반환
처음에는 mapper에서만 optiona로 반환으로 수정을 하였다가
굳이 optional로 반환하지 않고 User를 조회하는 mapper에서 있을 경우 User반환, 없을 경우 예외를 반환하기로 하였다.
2. 컨트롤러에서 도메인 객체 등장?
optional<user> 컨트롤러까지 넘어오고 있었다. 그래서 다시 User값을 꺼내는 처리를 컨트롤러에서 하고 있고 도메인인 User객체가 그대로 드러났다. 서비스단에서 처리하여 데이터 전송객체로 반환하도록 수정했다.
3. 구조 수정
restcontroller와 view리턴하는 컨트롤러를 쪼갰다. api 구조상으로는 좋지 않은 것 같기도 한데 이전보다는 직관적으로 분류가 되었다.
(하나의 pr에 많은 내용이 담기면 좋지 않은데 바로 수정하게 된다..
그래서 커밋이라도 잘게 쪼개고 메시지를 자세히 담아서 반영하고 있다. )
수정한 코드는 이렇다.
로그인 컨트롤러 (확실히 코드가 깔끔해졌다. 짱이다!)
@Slf4j
@RestController
@RequestMapping("/login")
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@PostMapping
public ResponseEntity<LoginResponseDTO> login(@RequestBody @Valid LoginRequestDTO loginRequestDTO) {
LoginResponseDTO loginResponseDTO = loginService.checkLoginForm(loginRequestDTO);
return ResponseEntity.ok(loginResponseDTO);
}
}
로그인 서비스
@Slf4j
@Service
@RequiredArgsConstructor
public class LoginService {
private final LoginUserService loginUserService;
public LoginResponseDTO checkLoginForm(LoginRequestDTO loginRequestDTO) {
String email = loginRequestDTO.getEmail();
User userEmail = User.builder().email(email).build();
String password = loginRequestDTO.getPassword();
User userByEmail = loginUserService.getUserByEmail(userEmail);
boolean equals = userByEmail.getPassword().equals(password);
if (equals) {
return LoginResponseDTO.builder().id(userByEmail.getUserId()).name(userByEmail.getName()).email(userByEmail.getEmail()).build();
} else {
throw new NoSuchElementException("비밀번호가 일치하지 않습니다.");
}
}
}
이메일 확인하는 로그인유저서비스 (볼수록 이름 마음에 안드는데..흠..)
@Service
@RequiredArgsConstructor
public class LoginUserService {
private final UserMapper userMapper;
public User getUserByEmail(User user) {
return Optional.ofNullable(userMapper.getByEmail(user.getEmail())).orElseThrow(() -> new NoSuchElementException("해당하는 이메일이 없습니다."));
}
}
다음 목표: @ExceptionHandler를 이용한 전역 API 예외처리
사실은 스프링 강의에서 들었던 내용인데 이론만 공부하고 프로젝트에 적용을 안 했었다!
생각보다 ExceptionHandler 사용 방법은 간단했다.
@Controller(뷰 반환)나 @RestController(json또는 xml 데이터 반환)를 사용해 특정 클래스 구조 안에서만
@ExceptionHandler로 예외를 처리할 수도 있지만
나는 전역에서 관리하고 싶었고 바디값에 예외를 담아서 json형식으로 응답을 반환할 것이기 때문에
@RestcontrollerAdvice를 사용했다.
@Valid 자바 어노테이션을 이용해 바인딩 오류도 전역에서 처리하고자 하였는데
이상하게 전역에서 예외가 안 잡혀서 낑낑댔다. 그 이유는 곧 나온다. 호이~
@Valid 자바 어노테이션을 사용할 경우, 발성하는 예외와
@Validated 스프링 어노테이션을 사용할 경우 발성하는 예외가 다르다는 점을 유의해야하고
@Valid 자바 어노테이션을 사용할 경우에도 상황에 따라 예외타입이 다르다고 한다.
1. @RequestBody에 매핑되는 DTO 클래스 검증:
DTO 에 대한 유효성 검사에서 실패하면 MethodArgumentNotValidException 예외가 발생하고,
이 예외는 스프링의 DefaultHandlerExceptionResolver에 의해 처리되며
기본적으로 HTTP 상태 코드 400 (Bad Request)를 반환한다.
2. @PathVariable 또는 @RequestParam 검증:
경로 파라미터(@PathVariable)나 쿼리 파라미터(@RequestParam) 등에 대한 유효성 검사에서 실패하면 ConstraintViolationException 예외가 발생한다.
이 예외는 스프링의 DefaultHandlerExceptionResolver에 기본 핸들러가 선언되어 있지 않아
기본적으로 HTTP 상태 코드 500 (Internal Server Error)를 반환하는데 이 상황에서는
커스텀 예외 핸들러를 추가하여 HTTP 상태 코드 400으로 처리할 수 있다고 한다.
(@Validated 스프링 어노테이션은 BindException이 유효형 검사에 실패한 경우에 발생)
나는 @Valid를 사용하고 있었고 전역에서 예외를 처리해 json으로 반환할 것이기 때문에
다음과 같이 적용하였다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public ResponseEntity<ExceptionResponse> processValidationError(MethodArgumentNotValidException exception) {
List<String> Messages = new ArrayList<>();
exception.getBindingResult().getFieldErrors().forEach((e) -> {
String errorMessage = e.getField() + ": " + e.getDefaultMessage();
Messages.add(errorMessage);
});
ExceptionResponse elementNotFound = ExceptionResponse.builder().message("유효한 값이 아닙니다").exceptions(Messages).status(HttpStatus.BAD_REQUEST).build();
return new ResponseEntity<>(elementNotFound, elementNotFound.getStatus());
}
@ExceptionHandler(NoSuchElementException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND) //명시적 선언, 하지만 ErrorResponse의 status세부 설정이 우선권 가진다.
public ResponseEntity<ExceptionResponse> handleNoSuchElementException(NoSuchElementException exception) {
ExceptionResponse elementNotFound = ExceptionResponse.builder().message(exception.getMessage()).exceptions(Collections.singletonList("Element not found")).status(HttpStatus.NOT_FOUND).build();
return new ResponseEntity<>(elementNotFound, elementNotFound.getStatus());
}
}
허무하게도 내가 적용이 안된 이유는
첫번째 - 기존 컨트롤러 메서드에서 try catch를 제거하지 않아서 예외가 catch문으로 넘어가서 전역에서 잡을 수가 없었다!
두번째 - 그리고 컨트롤러 메서드에 bindingresult 파라미터가 사용하지 않더라도 추가되어 있어서
수동으로 예외를 잡고 있어서 전역에서 호출되지 않았었다.
코드 수정할 때 새로운 코드도 중요하지만 기존 코드의 설정을 잘 제거해놔야겠다.
이메일 조회 실패시 예외, 해당 메일의 비번 일치여부 실패시 예외를 던져서 실패요인도 담아냈다.
'스프링' 카테고리의 다른 글
트랜잭션 개념과 트랜잭션 격리수준, 그리고 스프링 트랜잭션 (김영한-스프링 DB 1편) (1) | 2024.02.04 |
---|---|
에러핸들링, 예외처리 어떻게 할까? (1) | 2023.12.21 |
강의) 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (0) | 2023.12.05 |
강의) 스프링 핵심 원리 - 기본편 (0) | 2023.12.05 |
책) 스프링 입문을 위한 자바 객체지향원리와 원리 (0) | 2023.12.05 |