피보나치 수열 num번째 값을 리턴하시오.
public int fibonacci(int num) {
int result = 0;
if(num==1) return 0;
else if(num==2||num==3) return 1;
else {
result = fibonacci(num - 2) + fibonacci(num - 1);
}
return result;
}
제한시간 초과...
public int fibonacci(int num) {
ArrayList<Integer> arrList =new ArrayList<>(num);
arrList.add(0);
arrList.add(1);
int result = fibonacci(arrList.get(num-1))+fibonacci(arrList.get(num-2));
arrList.add(result);
return result;
}
뭔가 리스트를 이용해서 값을 저장해두고 꺼내와서 재귀 횟수를 절반 가까이 줄일 수 있을 것 같은데 잘안된다. 초기값 설정이 안된다.
list를 다루는 스킬이 부족한 것 같아 일단 포기하고 배열로 시도해 봤다.
public int fibonacci(int num) {
if(num==0) return 0;
else if(num>0) {
int[] result = new int[num + 1];
result[0] = 0;
result[1] = 1;
return fibo(num, result);
}
return 0;
}
public int fibo(int num, int[] arr){
if(num==1) return 1;
if(num==0) return 0;
arr[num] = fibo(num-2, arr)+fibo(num-1, arr);
return arr[num];
}
배열로 해서 올바른 결과가 나오기는 하는데 이것도 시간초과가 나온다.
이런식으로 할 것 같으면 조삼모사 같다...
public int fibonacci(int num) {
if(num==0) return 0;
else if(num>0) {
int[] result = new int[num + 1];
result[0] = 0;
result[1] = 1;
return fibo(num, result);
}
return 0;
}
public int fibo(int num, int[] arr){
if(arr[num]==0) {
if (num == 1) return 1;
if (num == 0) return 0;
arr[num] = fibo(num - 2, arr) + fibo(num - 1, arr);
}
return arr[num];
}
디버깅해보니 배열의 n번째 요소에 값이 이미 들어갔는데 n번째 요소값을 찾아주기 위한 반복이 한번 더 반복되는 것을 볼 수 있었다.(재귀를 두번하기 때문)
따라서 앞 재귀에서 이미 값을 채워 넣었다면 필요없는 재귀를 추가하지 말고 중단할 수 있도록 arr[num]==0
조건을 추가하니 시간복잡도가 줄어들어 모든 테스트에 통과할 수 있었다.
비슷한 방식으로 다시 list를 이용해 풀어보면
public int fibonacci2(int num) {
ArrayList<Integer> list = new ArrayList<>();
list.add(0);
list.add(1);
return fibo2(list, num);
}
public int fibo2(ArrayList<Integer> list, int num) {
if (list.size() <= num) {
list.add(fibo2(list, num - 1) + fibo2(list, num - 2));
}
return list.get(num);
}
이렇게도 풀 수 있다.
결국 시간복잡도를 줄이기 위한 키포인트는 이미 구한 n번째 피보나치 수열값을 다시 구하지 않도록 하는 것이였다.
// 클라이언트에서 핸들러 메서드에 요청을 전송했을 때
// 각 메서드의 맞는 유효한 데이터가 아니면 유효성 검증에 실패하고, ``MethodArgumentNotValidException``이 발생
@ExceptionHandler // ``MethodArgumentNotValidException``이 발생하면 전달해주는 애너테이션
public ResponseEntity handleException(MethodArgumentNotValidException e) { // 예외처리 메서드
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
// MethodArgumentNotValidException 객체(e)에서 getBindingResult().getFieldErrors()를 통해 발생한 에러 정보를 확인할 수 있다.
return new ResponseEntity(fieldErrors, HttpStatus.BAD_REQUEST);
}
Controller 클래스에 위와 같이 MethodArgumentNotValidException
이 발생했을 때(유효하지 않은 요청일 때) @ExceptionHandler
애너테이션을 이용해 예외를 처리하는 메서드로 유효성 검증 실패의 원인을 가져올 수 있다.
그런데 위와 같이 하면 List(fieldErrors
)에 필요없는 정보가 많이 담겨 있다.
fieldErrors
를 편집할 클래스(ErrorResponse
클래스)를 추가하면 클라이언트에 표기해줄 오류문의 가독성을 더 좋게할 수 있다.
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@Getter
@AllArgsConstructor
public class ErrorResponse {
private List<FieldError> fieldErrors;
@Getter
@AllArgsConstructor // 해당 객체 내에 있는 모든 변수들을 인수로 받는 생성자를 만들어냄
public static class FieldError{ // ErrorResponse 클래스의 static 멤버 클래스
private String field;
private Object rejectedValue;
private String reason;
// public FieldError(String field, Object rejectedValue, String reason) { // << @AllArgsConstructor가 이걸 만들어 줌
// this.field = field;
// this.rejectedValue = rejectedValue;
// this.reason = reason;
// }
}
}
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e) { // 예외처리 메서드
final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
// MethodArgumentNotValidException 객체(e)에서 getBindingResult().getFieldErrors()를 통해 발생한 에러 정보를 확인할 수 있다.
List<ErrorResponse.FieldError> errors = fieldErrors.stream()
.map(error -> new ErrorResponse.FieldError(
error.getField(),
error.getRejectedValue(),
error.getDefaultMessage()))
.collect(Collectors.toList());
return new ResponseEntity(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
각 Controller 클래스마다 코드 중복이 발생
예외의 종류별로 @ExceptionHandler
를 추가한 에러 처리 핸들러 메서드를 만들어 줘야 한다.
@RestControllerAdvice
@RestControllerAdvice
public class GlobalExceptionAdvice {
// 유효성 검증에 실패 예외(MethodArgumentNotValidException) 처리 메서드
@ExceptionHandler
public ResponseEntity handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
~~~
return ~~~;
}
// 제약 조건 위반예외(ConstraintViolationException) 처리 메서드
@ExceptionHandler
public ResponseEntity handleConstraintViolationException(
ConstraintViolationException e) {
~~~
return ~~~;
}
}
위와 같이 클래스 하나에 오류처리를 몰아서 코드의 중복을 없앨 수 있다.
GlobalExceptionAdvice 클래스
- 다양한 종류의 예외를 처리하는 메서드들을 모아둔 클래스
import com.codestates.section3week1.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
@RestControllerAdvice
public class GlobalExceptionAdvice { // 예외처리 클래스
@ExceptionHandler // 예외 전달해주는 애너테이션
//@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { // 유효성 검증에 실패 예외(MethodArgumentNotValidException) 처리 메서드
final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
return new ResponseEntity(response, HttpStatus.BAD_REQUEST);
}
// 핸들러 메서드의 URI 변수인 “/{member-id}”에 0이 넘어올 경우, ConstraintViolationException이 발생
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintViolationException(
ConstraintViolationException e) {
final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
return response;
}
}
@RestControllerAdvice 애너테이션을 사용하면 ResponseEntity로 래핑할 필요가 없다.
@RestControllerAdvice
는 @ResponseBody가 추가로 붙어있어, 응답을 JSON으로 내려준다.ResponseEntity
를 사용하지 않아도 위 handleConstraintViolationException()
메서드와 같이 @ResponseStatus
애너테이션을 사용해 HttpStatus.BAD_REQUEST
도 포함시켜 응답을 보낼 수 있다.ErrorResponse 클래스
- 예외응답을 간결화하는 메서드를 각 예외 타입별로 멤버 클래스 형태로 정리해둔 클래스
package com.codestates.section3week1.response;
import lombok.Getter;
import org.springframework.validation.BindingResult;
import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Getter
public class ErrorResponse { // 에러문 간략화해서 필요한 정보만 담는 용도의 클래스
private List<FieldError> fieldErrors;
private List<ConstraintViolationError> violationErrors;
// ErrorResponse 생성자
private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
this.fieldErrors = fieldErrors;
this.violationErrors = violationErrors;
}
// BindingResult에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(BindingResult bindingResult){
return new ErrorResponse(FieldError.of1(bindingResult), null);
}
// Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
// 메서드 오버로딩
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of2(violations));
}
@Getter
public static class FieldError{ // ErrorResponse 클래스의 static 멤버 클래스
private String field;
private Object rejectedValue;
private String reason;
private FieldError(String field, Object rejectedValue, String reason) {
this.field = field;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
private static List<FieldError> of1(BindingResult bindingResult) { // MethodArgumentNotValidException = BindingResult
final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.collect(Collectors.toList());
}
}
@Getter
public static class ConstraintViolationError {
private String propertyPath;
private Object rejectedValue;
private String reason;
private ConstraintViolationError(String propertyPath, Object rejectedValue, String reason) {
this.propertyPath = propertyPath;
this.rejectedValue = rejectedValue;
this.reason = reason;
}
private static List<ConstraintViolationError> of2(Set<ConstraintViolation<?>> constraintViolations) {
return constraintViolations.stream()
.map(constraintViolation -> new ConstraintViolationError(
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue().toString(),
constraintViolation.getMessage()))
.collect(Collectors.toList());
}
}
}
BindingResult
인터페스를 구현하고 Exception
클래스를 상속받은 BindException
클래스을 상속받은 클래스가 MethodArgumentNotValidException
이다.
대충 말하자면 MethodArgumentNotValidException
는
MethodParameter객체 + BindingResult객체라고 할 수 있다.
여기서 원하는 에러문은 BindingResult에 있기 때문에 getBindingResult()
메소드를 이용해 MethodParameter객체를 버리고 가져온 것이다.
BindingResult 인터페이스