JPA에서 발생하는 생성, 수정, 삭제 시간을 추적해보자
본 필자는 회사에서 일하면서 고객의 정보를 변경하면 수정 이력을 나타내는 기능과 수정 했던 사람을 추적해야 하는 기능을 구현해야 했다.
기존에는 직접 업데이트가 될 때 마다 전 후 비교를 통해 시간을 업데이트 시키는 방식으로 복잡하게 비즈니스 로직이 수행되었는데 필자는 이걸 좀 더 수월하게 이용하기 위해서 JPA Auditing 기술을 이용하고자 했다.
JPA Auditing 이란?
데이터베이스에서 누가 수정하고 언제 수정되었냐는 기록은 매우 중요할 수 있는 데이터이다. JPA에서는 이러한 기능을 추적할 수 있는 기능을 제공하고 있는데 그것이 바로 JPA Auditing
기술이다.
이 기술을 사용하면 자동으로 JPA에서 자동으로 생성이나 수정 발생 시 시간을 매핑하여 데이터베이스에 넣어준다.
사용해보자!
보통 요즘은 BaseTimeEntity를 이용하여 상속을 통해 모든 Entity에 해당 속성들을 넣어준다. 그렇다면 코드를 보자
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
@CreatedDate
@Column(name = "create_at", updatable = false)
private LocalDateTime createAt;
@LastModifiedDate
@Column(name = "update_at")
private LocalDateTime updateAt;
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;
@LastModifiedBy
@Column(name = "updated_by")
private String updatedBy;
public LocalDateTime createAt() { return createAt; }
public LocalDateTime updateAt() { return updateAt; }
public String createdBy() { return createdBy; }
public String updatedBy() { return updatedBy; }
}
- @EntityListeners : Entity를 DB에 적용하기 이전, 이후에 커스텀 콜백을 적용하기 위한 클래스
- class AuditingEntityListener : 영속화 및 업데이트 시 Auditing 정보를 캡처하는 클래스
- @CreatedDate : 데이터 생성 날짜를 자동으로 저장해주는 어노테이션
- @LastModifiedDate : 데이터 수정 날짜를 자동으로 저장해주는 어노테이션
- @CreatedBy : 데이터 생성 시 생성한 사용자 저장
- @LastModifiedBy : 데이터 업데이트 시 업데이트한 사용자 저장
물론 원래는 생성/업데이트 시간과 생성한 사용자에 대한 정보는 따로 분리해야 하지만 편의상 하나로 묶었다.
이제 상속을 해줄 BaseTimeEntity에 대한 내부 작성이 끝났다. 이제 사용하기 위해 활성화를 시켜보자
@EnableJpaAuditing // 활성화를 위한 어노테이션
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- JPA Auditing 기술을 사용하기 위해선 꼭 해당 어노테이션을 Application 클래스에 적용하여 활성화를 시켜줘야한다.
자 그럼 우린 개발자기 때문에 테스트를 통해 실제 결과를 확인해봐야 한다.
해당 엔티티는 게시판 엔티티다 현재 BaseEntity를 상속받아 사용한다. 또한 CreatedBy와 lastModifiedBy 정보를 저장하기위해 AuditorAware를 시큐리티에서 사용자 정보를 가져와 이벤트 발생 시 저장하는 클래스를 정의하였다.
@Configuration
public class SpringSecurityAuditorAware implements AuditorAware<Long> {
@Override
public Optional<Long> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(authentication -> {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
boolean isUser = authorities.contains(new SimpleGrantedAuthority("USER"));
CustomUserDetails principal = (CustomUserDetails) authentication.getPrincipal();
return isUser ? principal.getId() : null;
});
}
}
각 CreatedBy와 LastModifiedBy에는 사용자의 식별값(PK)이 들어갈 예정이다. 그럼 테스트 결과를 보자
@Test
@DisplayName("게시판 정보를 수정하고 수정 날짜와 수정자를 기록한다.")
@WithAuthUser
void update() {
// given
BoardUpdateRequest request =
BoardUpdateRequest.of(Title.from("수정된 게시판 제목입니다."), Content.from("안녕하세요 수정된 게시판 내용입니다."));
// when
boardService.update(request.toEntity(), 1L, 1L);
Board board = boardRepository.findById(1L).orElseThrow(
() -> new BoardNotFoundException(ErrorCode.BOARD_NOT_FOUND));
// then
assertThat(board.getTitle()).isEqualTo(Title.from("수정된 게시판 제목입니다."));
assertThat(board.getContent()).isEqualTo(Content.from("안녕하세요 수정된 게시판 내용입니다."));
assertThat(board.getLastModifiedBy()).isEqualTo(1L);
assertThat(board.getLastModifiedTime()).isAfter(board.getCreateTime());
}
다음 코드로 테스트를 진행할 예정이다.
테스트는 잘 성공한다!!!
쿼리도 잘 나가는 것을 알 수 있다. !!
DB도 결과적으로 잘 나오는 것을 알 수 있다!!!