이번엔 예외처리를 맡아서 코드를 작성해보았다.
전역에서 발생하는 예외들을 처리하기 위한 클래스
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 {
private ExceptionRepository repository;
public GlobalExceptionAdvice(ExceptionRepository repository) {
this.repository = repository;
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
return ErrorResponse.of(e.getBindingResult());
}
@ExceptionHandler
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleConstraintViolationException(ConstraintViolationException e) {
return ErrorResponse.of(e.getConstraintViolations());
}
}
발생한 에러 정보들을 다듬기 위한 클래스
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;
/*
@NotNull 같은 어노테이션으로 DTO에서 검증에 실패할 경우 FieldError 발생
@검증 어노테이션 사용시 핸들러 메서드에 @Valid 사용해야 함
@검증 어노테이션에서 검증 실패 시 @Valid 검증도 실패하지만 우선적으로 FieldError를 catch
그래서 ConstraintViolationError는 @PathVariable 같은 DTO에서 어노테이션으로 처리 하지 못한 부분들 처리 */
@Getter
public class ErrorResponse {
/* MethodArgumentNotValidException 에서 발생하는 에러 정보를 담는 변수
필드에 접근할 때 생기는 오류
-> DTO 유효성 검증 실패한 에러 정보 */
private List<FieldError> fieldErrors;
/* ConstraintViolationException 에서 발생하는 에러 정보를 담는 변수
DB 제약 조건을 위반한 에러
-> URI 변수 값의 유효성 검증에 실패한 에러 정보 */
private List<ConstraintViolationError> violationErrors;
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.of(bindingResult), null);
}
// Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
return new ErrorResponse(null, ConstraintViolationError.of(violations));
}
public static ErrorResponse of(ExceptionCode exceptionCode) {
return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage());
}
@Getter
public static class FieldError {
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;
}
public static List<FieldError> of(BindingResult bindingResult) { // BindingResult : 어노테이션 기반 검증 실패 에러 정보
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;
}
// ConstraintViolation : @Valid 검증 실패 에러 정보
public static List<ConstraintViolationError> of(Set<ConstraintViolation<?>> constraintViolations) {
return constraintViolations.stream()
.map(constraintViolation -> new ConstraintViolationError(
constraintViolation.getPropertyPath().toString(),
constraintViolation.getInvalidValue().toString(),
constraintViolation.getMessage()))
.collect(Collectors.toList());
}
}
}
FieldError 필드에는 BindingResult를 다듬어 넣어주고 ConstraintViolationError 필드에는 ConstraintViolationException을 다듬어 넣어준다.
각 생성자는 접근제어자를 private으로 설정하고 of 메서드를 오버로딩하여 생성할 수 있게 작성하였다!
of 메서드의 로직은 거의 유사하다.
받아온 에러 정보를 스트림에 넣고 내부 클래스인 FieldError와 ConstraintViolationError의 필드들인 에러가 발생한 필드나 경로, 거절된 값, 이유를 할당해주었다.
-> 발생한 오류 정보를 ErrorResponse.of()에 전달해주면 해당 오류의 정보들이 정리된다.
비즈니스 로직에서 발생하는 예외들을 잡아 어떤 예외가 발생하였는지 알 수 있게 Enum 타입으로 커스텀 예외 코드를 작성하였다.
import lombok.Getter;
@Getter
public enum ExceptionCode {
MEMBER_NOT_FOUND(404, "Member not found"),
MEMBER_NAME_EXISTS(409, "Member Name exists"),
MEMBER_EMAIL_EXISTS(409, "Member Email exists"),
MEMBER_NOT_AUTHENTICATED(401, "Member not Authenticated"),
QUESTION_NOT_FOUND(404, "Question not found"),
ANSWER_NOT_FOUND(404,"Answer not found"),
COMMENT_NOT_FOUND(404, "Comment not found");
private int status;
private String message;
ExceptionCode(int status, String message) {
this.status = status;
this.message = message;
}
}
RuntimeException을 상속받아 발생하는 예외들을 커스터마이즈 하기 위한 클래스. 위에 작성했던 ExceptionCode를 필드로 갖는다!
import lombok.Getter;
@Getter
public class BusinessLogicException extends RuntimeException{
private ExceptionCode exceptionCode;
public BusinessLogicException(ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
}
즉 예외를 catch 해서 상황에 맞는 exceptionCode를 생성자에 넣고 BusinessLogicException을 throw 할 수 있다.
...
@ExceptionHandler
public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
ErrorResponse response = ErrorResponse.of(e.getExceptionCode());
return new ResponseEntity<>(response, HttpStatus.valueOf(e.getExceptionCode().getStatus()));
}
...
// BusinessLogic Exception 응답을 위한 필드
private int status;
private String message;
private ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
}
...
public static ErrorResponse of(ExceptionCode exceptionCode) {
return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage());
}
위에서 봤던 FieldError, ConstraintViolationError와 방식은 유사하다.
exceptionCode에 담긴 status와 message를 필드에 할당하고 생성하는 식이다.
처리하지 못한 예외 로그를 DB에 따로 보관하면 좋다고하여 한번 구현해 보았다.
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Setter
@Getter
public class ExceptionLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long logId;
@Column(nullable = false)
private String exception;
@Column(nullable = false)
private String message;
public ExceptionLog(String exception, String message) {
this.exception = exception;
this.message = message;
}
public ExceptionLog() {
}
}
테이블을 따로 생성하기 위해 엔티티를 생성해주었다.
exception 컬럼엔 어떤 에러인지 message엔 에러 메세지를 담을 예정이다.
import org.springframework.data.jpa.repository.JpaRepository;
public interface ExceptionRepository extends JpaRepository<ExceptionLog, Long> {
}
기본적인 CRUD를 위해 JpaRepository를 상속받는 인터페이스를 생성하였다.
...
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ExceptionLog handleException(Exception e) {
ExceptionLog exceptionLog = new ExceptionLog(e.getClass().getSimpleName(), e.getMessage());
return repository.save(exceptionLog);
}
위에서 처리되지 못한 예외 발생시 해당 예외를 잡아 클래스의 간단한 이름과 메세지로 exceptionLog를 생성하고 DB에 저장한다!!
생각보다 쉽게 구현되었던 것 같다.
그리고 생각보다 쉽게 오류들이 발생했다....................
이제 해당 오류들도 처리를 해야겠다..
예상치 못한 곳들에서 오류들이 터져나온다.
우선 오류 메세지가 생각보다 더 긴 경우 String 타입으로 DB에 담을때 오류가 발생했다..
찾아보니 데이터 타입에 String에 대응하는 varchar 외에 Text 등 다른 타입들도 있었다.
Text 타입의 경우 varchar와 최대 길이는 동일하나 대용량 텍스트 데이터를 여러 개의 레코드로 나누어 저장하는 방식으로 최대 4GB까지 저장할 수 있다...고 한다...... 결국 길이가 아니라 크기가 문제였나보다.
그렇게 데이터 타입을 변경한 ExceptionLog 엔티티 클래스
...
@Column(nullable = false, columnDefinition = "TEXT")
private String message;
...
columnDefinition = "TEXT" 어트리뷰트로 지정 가능하다!!!
요청에 대한 응답을 보내는데 재귀적으로 응답을 생성하는 오류를 마주쳤다..
찾아보니 양방향 순환참조 라고 부르는 것 같다.
question과 answer를 1:n, n:1 양방향 매핑을 해놨었는데 Question responseDto에서 Answer 객체 자체를 필드로 가지고 있어서였던것 같다.
question과 answer의 관계에서 answer가 fk로 questionId를 가지고 관계의 주인이 된다.
question은 관계의 주인이 아니므로 answer의 fk를 갖지 않지만
JPA는 서버에서 클라이언트에게 question 정보를 보내줄때 answer 정보도 같이 보내주고
관계의 주인인 answer도 서버에서 클라이언트에게 정보를 보내줄 때 question의 정보를 보내준다.
즉 question -> answer -> question 이런식으로 서로 정보를 계속 보내주게 되어 이런 문제가 발생한다는 것 같다..
우선 DTO의 응답값을 question 객체가 아닌 questionId로 변경해주니 해결 되었다.
단방향 매핑이나 @JsonIgnore ,@JsonManagedReference, @JsonBackReference, @JsonIgnoreProperties 같은 어노테이션으로도 해결 할 수 있다고 한다..!