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_at
과 updated_at
이 필요한 모든 엔티티 클래스에 위 작업을 하는 것은 번거롭다. 스프링이 지향하는, 그리고 객체지향이 지향하는 (음? 말이 중복되네.) 것 중 하나가 반복되는 횡단 관심사를 분리하는 것이다. 생성일과 수정일을 기록하는 작업은 메인 비즈니스 로직과 관련이 떨어지며, 필요할 때마다 반복되는 작업이다.
공통되는 것을 묶어 상위 클래스로 정의하는 객체지향적 방법을 쓸 수 있을 것 같은데.. JPA에서 그렇게 할 수 있나?
있다. @MappedSuperClass
를 이용해서.
해당 어노테이션이 붙은 BaseTimeEntity
를 만들자. 이전 MyEntity
에 붙였던 @EntityListeners
와 같은 어노테이션을 붙였다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
@Getter
: BaseTimeEntity
를 상속받은 엔티티 클래스가 @MappedSuperClass
는 이 클래스를 상속받은 엔티티 클래스들이 super class의 필드 값을 가질 것이라고 명시해주는 어노테이션이다.이제 MyEntity
의 코드는 간단해진다.
@Entity
public class MyEntity extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
extends BaseTimeEntity
로 끝. 이전에 두 개의 필드를 가졌을 때와 동일하게 동작한다.
Spring의 에러 핸들링은 @ControllerAdvice
class+ @ExceptionHandler
method 구조로 동작한다.
@ControllerAdvice가 붙은 클래스의 method들은 @Controller 클래스들의 global aspect로서 작동한다. @ControllerAdvice 내부의 method들에 @InitBinder 혹은 @ModelAttribute를 붙여 요청으로로 들어온 값들을 바인딩하는 작업을 직접 수행할 수도 있고, @ExceptionHandler를 붙여 모든 컨트롤러를 아우르는 예외 처리 핸들러로서 작동하게 할 수도 있다.
스프링에는 예외를 처리하는 HandlerExceptionResolver 인터페이스가 있는데, mvc의 컨트롤러 아래에서 예외가 발생하면 그를 구현한 3개의 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()));
}
}