JPA Auditing으로 엔티티 변경 이력 관리하기
Spring Data JPA에서는 기본적으로 제공해주는 기능 중 편리한 기능들이 많은데, 그중 내게 충격을 주었던 기능이 바로 Auditing이었다.
MyBatis에서는 일일이 설정하거나 지정해줘야 했던 부분이 기능으로 있다니..
오늘은 JPA Auditing에 대해서 자세히 알아보고, 아직 사용해보지 못한 AuditorAware 기능과 실무에서 겪었던 트러블 슈팅까지 추가해서 기록해두려고 한다.
JPA Auditing
Audit은 사전적으로 ‘감사하다’, ‘단속하다’라는 뜻을 가지고 있다. JPA에서는 Auditing을 제공하여 엔티티가 언제 혹은 누가 생성하거나 변경했는지를 자동으로 추적(단속)하는 기능을 제공한다.
Auditing을 사용하기 위해서는 엔티티 클래스에 Auditing 정보를 포함한 메타데이터를 추가하고, 어노테이션을 사용하거나 특정 인터페이스를 구현하여 정의할 수 있다.
Auditing 적용
Application.class
@EnableJpaAuditing
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(CaffeineApplication.class, args);
}
}
@EnableJpaAuditing
- JPA Auditing을 활성하기 위한 어노테이션으로, 이렇게 Application단의 main 메소드에 붙여주거나 Config 클래스를 따로 생성하여 설정해줄 수 있다.
BaseTimeEntity.class
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseDateTimeEntity {
@CreatedDate
private LocalDateTime createdDateTime;
@LastModifiedDate
private LocalDateTime modifiedDateTime;
}
@MappedSuperclass
- JPA Entity 클래스들이 BaseTimeEntity 클래스를 상속할 경우 필드들(createdDateTime, modifiedDateTime)을 컬럼으로 인식하도록 한다.
@EntityListeners(AuditingEntityListener.class)
- BaseTimeEntity 클래스에 Auditing 기능을 포함시킨다.
@CreatedDate
- Entity가 생성되어 저장될 때 시간이 자동 저장된다.
@LastModifiedDate
- 조회한 Entity의 값을 변경할 때 시간이 자동 저장된다.
➕ @MappedSuperclass란?
@Documented
@Target(TYPE)
@Retention(RUNTIME)
public @interface MappedSuperclass
- MappedSuperclass는 상위 클래스로부터 매핑 정보를 상속 받는 클래스임을 지정하는 어노테이션이다.
- 지정된 클래스 자체는 별도의 테이블이 생성되지 않는다. (== 상속하는 하위 클래스에만 적용된다.)
- 하위 클래스에서는 상속된 매핑 정보가 해당 클래스의 테이블에 적용되고,
@AttributeOverride
,@AssociationOverride
애노테이션 (또는 XML)을 사용하면 매핑 정보를 재정의할 수도 있다.
AuditorAware
그렇다면 AuditorAware는 뭘까? 아직 직접 써본 적 없지만 생성일, 수정일처럼 생성자, 수정자 같은 사용자 정보도 JPA Auditing을 통해 자동으로 입력해줄 수 있다. 생성일, 수정일과는 구현체를 설정하고 그 구현체를 JWT 같은 인증 토큰을 검사할 때 같이 넣어주면 된다.
UserAuditAware.class
@Service
public class UserAuditAware implements AuditorAware<Long> {
@Override
public Optional<Long> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty();
}
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return Optional.of(userDetails.getId());
}
}
Application.class
@EnableJpaAuditing(auditorAwareRef = "userAuditorAware")
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(CaffeineApplication.class, args);
}
}
위에서 설정해준 main 메서드에 구현해준 클래스의 빈 이름을 설정해주면 해당 클래스를 이용하여 사용자 정보를 자동으로 등록할 수 있게 된다.
트러블 슈팅 💫
어라 왜 update문의 결과를 확인하는데 createdDateTime이 null로 업데이트 되지❓🤔
생성일 컬럼에 이름을 설정하기 위해 @Column
어노테이션을 붙였는데, @Column
의 옵션 중 하나인 updatable의 기본값이 true라서 값을 설정해주지 않은 생성일 컬럼에 null이 업데이트된 것이다.
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Column {
/**
* (Optional) Whether the column is included in SQL UPDATE
* statements generated by the persistence provider.
*/
boolean updatable() default **true**;
}
그래서 @Column
을 사용할 거라면 아래처럼 updatable 옵션을 false로 지정해주는 것이 필요하다.
@CreatedDate
@Column(name = "created_date_time", updatable = false)
private LocalDateTime createdDateTime;
다시 생각해보니 @Column
어노테이션을 꼭 써야할까? 하는 의문이 들었다. 내가 @Column
을 썼던 이유는 Java의 카멜케이스를 데이터베이스의 스네이크케이스와 연결시키고 싶어서였다.
그런데 아래처럼 YML 파일에 설정하면 자동 연결이 된다니 💦 앞으로는 이런 설정 하나로 코드를 줄이기 위해 한번씩은 꼭 찾아보고 작업할 필요를 느꼈다.
spring:
jpa:
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
➕ 추가로 @CreatedDate
/@CreatedBy
는 기본적으로 엔티티가 처음 저장될 때 자동으로 값이 설정되지만, 직접 변경하거나 Native Query로는 수정 가능하기 때문에 @Column(updatable = false)
을 설정하는 것이 안전하다.
참고