spring

[Spring] JPA Auditing

허몽구 2023. 8. 10. 12:29

이번에 프로젝트를 리팩토링하면서 글을 생성할 때 생성일자와 수정일자를 자동으로 저장되게 할 수는 없을까? 라는 의문점이 들었다. 

각각의 테이블마다 createAt 이라는 컬럼을 넣어 사용하다보니 중복되는 컬럼이 너무 보기 싫었다. (?)

public class User {
  private Long id;
  private String name;
  private int age;
  private LocalDateTime createdAt;
  private String createdBy;
}

public class Member {
  private Long id;
  private User user;
  private LocalDateTime createdAt;
  private String createdBy;
}

예시로 두 개만 넣어놨지만, 여러 엔티티를 만들게 된다면 중복되는 createAt, createBy 등의 컬럼이 생성될 것이다.

모든 엔티티가 동일한 컬럼을 가지고 수행해야 한다면 공통 엔티티를 생성하고 데이터 요청 시에 해당 엔티티의 컬럼이 업데이트 되도록 할 수는 없을까? 라는 생각이 들었다. 

그렇게 방법을 찾아보다 알게된 것이 Auditing 이었다.

 

JPA Auditing은 JPA Entity에 대한 변경 이력을 추적하고 기록하는 기능을 제공한다.

서비스를 운영하며 데이터가 생성되거나 수정됨을 기록하는 것이 중요하다는 것을 알았기 때문에 이 Auditing을 한번 써보려고 한다.

 

@SpringBootApplication
@EnableJpaAuditing
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

우선 @EnableJpaAuditing 어노테이션을 사용하여 Auditing을 활성화시킨다.

보통 Springboot 를 실행시키는 클래스 상단에 많이 사용한다고 한다.

 

이후 공통(기본) 엔티티 코드를 작성해보자.

@MappedSuperclass
@Getter
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

위와 같이 작성하면 해당 엔티티 클래스가 JPA 이벤트를 발생시켰을 때 Auditing 을 수행하여 값을 업데이트 한다.

 

Auditing을 공부하면서 새롭게 알게된 어노테이션이 있다.

 

1) MappedSuperClass

MappedSuperClass 어노테이션은 엔티티 클래스 사이에서 공통적으로 사용되는 매핑 정보를 정의하는 기능을 제공한다.

이 어노테이션을 사용하면 여러 엔티티 클래스에서 중복되는 매핑 정보를 한 곳에 모아둘 수 있다.

즉, 부모 엔티티에 필드를 선언하고 단순히 속성만 상속받아 사용하고 싶을 때 사용하는 어노테이션이다.

위 BaseEntity처럼 공통 엔티티를 생성하고 Auditing 기능을 사용할 엔티티 클래스를 위해 해당 어노테이션을 사용한다. 

 

일반적으로 @MappedSuperclass 어노테이션이 적용된 클래스는 실제로 데이터베이스에 매핑되지 않는다.

테이블로 생성되지는 않지만 @MappedSuperclass로 표시된 클래스의 매핑 정보가 하위 엔티티에 상속된다.

 

2) EntityListeners(AuditingEntityListener.class)

엔티티의 라이프사이클 이벤트에 대한 리스너 클래스를 지정하는데 사용된다.

어려운 말로 썼지만, 풀어서 설명하자면 엔티티의 상태 변화(생성, 수정, 삭제 등) 에 대응하여 특정 동작을 실행할 수 있도록 하는 것을 나타낸다.

 

예를 들어, 어떤 엔티티가 데이터베이스에 저장되기 전에 특정 값을 검증하고 싶다고 가정해보자.

이런 경우에 엔티티의 라이프사이클 이벤트를 감지하고 그에 따른 동작을 처리할 수 있는 리스너 클래스를 작성하고 이를 @EntityListeners 어노테이션을 사용하여 해당 엔터티 클래스에 지정할 수 있는 것이다.

 

@EntityListeners 어노테이션은 엔티티 클래스 또는 @MappedSuperclass로 표시된 상위 클래스에 적용할 수 있다.

이 어노테이션을 사용하여 엔티티에 대한 리스너 클래스를 연결할 수 있다.

 

3) @Column(updatable = false)

해당 BaseEntity를 JPA가 테이블에 접근하는 시점에만 JPA가 사용하도록 해야 하기 때문에, 즉 다른 개발자에 의해 수정되지 않도록 updatable을 false로 하는 것을 권장한다.  

 

이제 기본 엔티티를 생성했으니, BaseEntity를 사용할 엔티티를 생성해보자.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Entity
public class User extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private int age;
}

위와 같이 BaseEntity를 상속한 User 엔티티를 생성해줬다. 

중복코드를 없애면서, 여러 엔티티에서 생성시각과 수정시각을 쉽게 추적할 수 있다.