[F-Lab 모각코 챌린지 58일차] BaseEntity, Exception

부추·2023년 7월 28일
0

F-Lab 모각코 챌린지

목록 보기
58/66

# createdAt, updatedAt 필드를 추가하자

레시피 업로드 기능을 만들고 싶었다. 보통 커뮤니티에서 생각하는 "게시글"과 매우 비슷한 기능이다. 기본적으로 조회수와 북마크 기능을 가지고 있다. 추가로, 해당 게시글을 작성하거나 수정하여 DB에 반영된 일시를 체크하고 싶었다. 더 프로그래밍적으로 말하면 createdAt, updatedAt 필드를 엔티티 객체이 추가하고 db table에 컬럼으로 추가하고 싶었다.

물론, JAVA 진영에서 제공하는 현재 JVM의 시간을 캡쳐해서 엔티티에 지정해줄 수도 있지만, 그걸 직접 하는건 ㅋㅋ... 횡단 로직에 많은 시간을 쏟게되는 꼴이기도 하고, JPA 측에서 제공하는 관련 기능이 있는ㄷ

그러기 위해서 가장 먼저 필요한건, DB의 시간을 감시하는 "Auditor"을 추가하는 것이다. Component 위에 @EnableJpaAuditing 어노테이션을 추가하는 것이 일반적이다. 아예 @SpringBootApplication이 붙은 메인 클래스에 붙이는 사람들도 있던데 ,, 나는 해당 어노테이션만을 붙이기 위한 @Configuration 클래스를 아예 따로 만들었다. 이름은 JpaAuditingConfiguration이다.

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfiguration {
}

이렇게 어노테이션을 붙여줌으로써, JPA 구현체 측(hibernate)에서 Auditing을 할 수 있도록 구성했다. 뭘 auditing할까? 어노테이션이 존재하는 라이브러리 패키지에 들어가보자.
당연히 어노테이션 코드만 보면 무슨 역할을 하는지 정확히 이해가 안가지만, 대충 date, create, time 등이 있는것을 보니 우리가 원하는 기능(createdAt, updatedAt 추가)을 수행해줄 것 같다.

준비는 끝났다. 이제 적용할 엔티티를 찾아야 한다. 엔티티 클래스에 다음과 같은 어노테이션과 필드를 추가하면 끝이다.

@Entity
@EntityListeners(AuditingEntityListener.class)
public class MyEntity {
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @CreatedDate
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

이제 MyEntity에 대해, 새로운 엔티티 데이터를 만들어 save()하면 created_at컬럼에 그 순간의 날짜와 시간이 저장되고 @Transactional 메소드 안에서 엔티티 값을 수정해 update 쿼리가 나가면 updated_at 컬럼에는 그 순간의 날짜와 시간이 저장된다 !!


# @MappedSuperClass

하지만 created_atupdated_at이 필요한 모든 엔티티 클래스에 위 작업을 하는 것은 번거롭다. 스프링이 지향하는, 그리고 객체지향이 지향하는 (음? 말이 중복되네.) 것 중 하나가 반복되는 횡단 관심사를 분리하는 것이다. 생성일과 수정일을 기록하는 작업은 메인 비즈니스 로직과 관련이 떨어지며, 필요할 때마다 반복되는 작업이다.

공통되는 것을 묶어 상위 클래스로 정의하는 객체지향적 방법을 쓸 수 있을 것 같은데.. JPA에서 그렇게 할 수 있나?

있다. @MappedSuperClass를 이용해서.

해당 어노테이션이 붙은 BaseTimeEntity를 만들자. 이전 MyEntity에 붙였던 @EntityListeners와 같은 어노테이션을 붙였다.

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}
  • @Getter : BaseTimeEntity를 상속받은 엔티티 클래스가
  • constructor가 없으면 기본 생성자가 만들어지므로 생성자에 관한건 생각 안해도 된다.
  • @MappedSuperClass는 이 클래스를 상속받은 엔티티 클래스들이 super class의 필드 값을 가질 것이라고 명시해주는 어노테이션이다.

이제 MyEntity의 코드는 간단해진다.

@Entity
public class MyEntity extends BaseTimeEntity {
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}

extends BaseTimeEntity로 끝. 이전에 두 개의 필드를 가졌을 때와 동일하게 동작한다.


# Exception을 핸들링해보자.

Spring의 에러 핸들링은 @ControllerAdvice class+ @ExceptionHandler method 구조로 동작한다.

@ControllerAdvice가 붙은 클래스의 method들은 @Controller 클래스들의 global aspect로서 작동한다. @ControllerAdvice 내부의 method들에 @InitBinder 혹은 @ModelAttribute를 붙여 요청으로로 들어온 값들을 바인딩하는 작업을 직접 수행할 수도 있고, @ExceptionHandler를 붙여 모든 컨트롤러를 아우르는 예외 처리 핸들러로서 작동하게 할 수도 있다.

스프링에는 예외를 처리하는 HandlerExceptionResolver 인터페이스가 있는데, mvc의 컨트롤러 아래에서 예외가 발생하면 그를 구현한 3개의 Resolver들이 순차적으로 작동하여 예외 처리를 하게된다.

  • ExceptionHandlerExceptionResolver : 예외가 발생한 컨트롤러 클래스 -> @ControllerAdvice 클래스 순으로 컴포넌트를 탐색하며 해당 에러에 맞는 @ExceptionHandler를 찾는다. 핸들러를 찾으면 method를 작동시킨다.
    • ExceptionHandler에선 HttpRequestServlet, HttpResponseServlet, Model 등을 인자로 받아 이를 활용할 수 있다. controller의 일반적인 handler method와 비슷하게 동작할 수 있다.
    • @ExceptionHandler 어노테이션의 value 값으로 특정 Exception class을 설정해 해당 예외가 발생했을때 exception을 처리하는 방식이다. exception 객체는 handler method의 인자로 바인딩된다.
    • 전술했듯 @ControllerAdvice와 함께 쓰여 전역적 예외 핸들링이 가능하다.
  • ResponseStatusExceptionResolver : 적절한 @ExceptionHandler가 존재하지 않을 때, 던져진 예외 클래스에 @ResponseStatus가 붙었는지, 혹은 예외 자체가 ResponseStatusException인지 확인한다. 맞다면 예외의 response status를 수정하여 서블릿에 전달하고, 서블릿이 에러를 다시 BasicErrorController로 전달하여 처리하게끔 한다.
    • ResponseStatus / ResponseStatusException : 스프링 환경에서 발생한 에러에 status를 설정할 수 있게 해주는 annotation / exception class
      status만을 수정할 수 있다는 점에서 ExceptionHandler보다 유연성이 떨어진다
    • 결국 에러가 서블릿까지 전달되고 WAS에 의해 서블릿으로 에러 요청이 한 번 더 일어난다
  • DefaultHandlerExceptionResolver : 발생한 예외가 ResponseStatusExcepion도 아닐때 작동하는 resolver. 스프링에서 발생한 예외를 처리한다.

위 Resolver들로 처리가 안 된 예외는 스프링부트의 자동설정에 맞게 구성된 뒤 BasicController로 전달된다.

ExceptionHandlerExceptionResolver를 제외하고는 모두 예외가 서블릿까지 다시 전달된다. 그리고 이를 받은 WAS(톰캣 등)가 에러 요청을 서블릿에 한 번 더 보낸다. 이때 이 에러 요청에 동작하는 컨트롤러는 스프링 컨텍스트에 기본으로 등록된 BasicErrorController이고, ResponseStatus를 따로 설정하지 않았을 때 상태 코드의 기본 값은 500이다.


개발중인 스프링 프로젝트에서 나올 수 있는 예상 가능한 에러는 전부 커버해야한다. 더해서 그것이 어떤 종류의 에러인지, 왜 발생했는지 구체적으로 알수록 좋다. 그래야 그에 맞는 대처를 할 수 있기 때문이다. 이런 이유 때문에 스프링에서 예외 처리를 할 땐 예외 처리에 유연성이 가장 좋은 @ExceptionHandler를 작성하는 것이 선호된다.


대충 ResponseEntity의 body 값으로 넣어줄 객체 하나를 만들었다.

@AllArgsConstructor
@Getter
public class ExceptionResponse {
    private String message;
}

그리고 @RestControllerAdvice 코드 ...

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler({
            HttpRequestMethodNotSupportedException.class,
            HttpMediaTypeNotSupportedException.class,
            HttpMediaTypeNotAcceptableException.class,
            MissingPathVariableException.class,
            MissingServletRequestParameterException.class,
            MissingServletRequestPartException.class,
            ServletRequestBindingException.class,
            MethodArgumentNotValidException.class,
            NoHandlerFoundException.class,
            AsyncRequestTimeoutException.class,
            ErrorResponseException.class,
            ConversionNotSupportedException.class,
            TypeMismatchException.class,
            HttpMessageNotReadableException.class,
            HttpMessageNotWritableException.class,
            BindException.class
    })
    public ResponseEntity<ExceptionResponse> handleMvcException(Exception e) {
        log.error("Mvc Exception ", e);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(new ExceptionResponse(
                        e.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ExceptionResponse> handleGlobalException(Exception e) {
        log.error("Unknown error ", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(new ExceptionResponse(
                        e.getMessage()));
    }
}
profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글