개요
모든 데이터에는 누가 등록했는지 구별하는 컬럼이 존재한다. 하지만 해당 컬럼을 일일이 다 입력해주는 건 너무 지겨운 작업이다.
Spring JPA 기본 베이스 구성에서는 @PrePersist
@PostPersist
@PreUpdate
@PostUpdate
키워드로 구현을 했었다.
그러나Spring Data JPA의 Auditing인 AuditAware를 사용하면 다음과 같이 간단한 매핑을 통해 특정 필드에 지금 로그인한 사람의 정보로 등록자를 자동으로 입력 해줄 수 있다.
Data JPA의 Auditing Keyword는 아래와 같다.
CreatedDate
- 해당 엔티티가 생성될 때, 생성하는 시각을 자동으로 삽입해준다.
CreatedBy
- 해당 엔티티가 생성될 때, 생성하는 사람이 누구인지 자동으로 삽입해준다.
- 생성하는 주체를 지정하기 위해서
AuditorAware<T>
를 지정해야 한다.- 이는 Spring Security 와 함께 다뤄야 하는 내용이다.
LastModifiedDate
- 해당 엔티티가 수정될 때, 수정하는 시각을 자동으로 삽입해준다.
LastModifiedBy
- 해당 엔티티가 수정될 때, 수정하는 주체가 누구인지 자동으로 삽입해준다.
- 생성하는 주체를 지정하기 위해서
AuditorAware<T>
를 지정해야 한다.- 이 또한 Spring Security 와 함께 다뤄야 함
- 생성하는 주체를 지정하기 위해서
- 해당 엔티티가 수정될 때, 수정하는 주체가 누구인지 자동으로 삽입해준다.
1. Bean 등록
import kr.co.welcomefg.domain.account.entity.UserPrincipal;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Optional;
@EnableJpaAuditing
@SpringBootApplication
public class SpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootApplication.class, args);
}
@Bean
public AuditorAware<String> auditorProvider() {
return () -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (null == authentication || !authentication.isAuthenticated() || authentication.getPrincipal().equals("anonymousUser")) {
return Optional.empty();
}
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
return Optional.of(principal.getAccountInfo());
};
}
}
@EnableJpaAuditing
및 @Bean
추가.
SecurityContextHolder에 로그인 되어있는 사용자의 Principal에서 AccountInfo entity를 가져온 경우이다.
그러나 SpringBootApplication 단에 @EnableJpaAuditing
만 적용하고, 아래와 같이 Aware를 구현할 수도 있다.
package com.app.weekly.utils;
import com.app.weekly.domain.User;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class LoginUserAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (null == authentication || !authentication.isAuthenticated() || authentication.getPrincipal().equals("anonymousUser")) {
return Optional.empty();
}
User user = (User) authentication.getPrincipal();
return Optional.of(user.getUserId());
}
}
여기서 중요한건 Bean으로 등록하기위해 @Component
로 등록해야 한다. 런타임에 클래스가 로드하기 위함이다.
2. BaseEntity 생성
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(name="created_date", nullable = false, updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
@Column(name="last_modified_date", nullable = false)
private LocalDateTime lastModifiedDate;
}
import kr.co.welcomefg.domain.account.entity.AccountInfo;
import lombok.Getter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="created_by", nullable = false, updatable = false)
private AccountInfo createdBy;
@LastModifiedBy
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="last_modified_by", nullable = false)
private AccountInfo lastModifiedBy;
}
3. Entity에 적용
@Entity
@Getter
@Table
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Notice extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1461139014929000736L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
BaseEntity
를 상속하면 자동으로 @MappedSuperclass
에 의해 칼럼이 생성된다.
혹여나 @CreatedDate
와 같은 일자 정보만 필요하다면 BaseTimeEntity 를 사용하면 되겠다.
4. Entity에 직접 적용
@Entity
@Getter
@Table
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Notice implements Serializable {
private static final long serialVersionUID = 1461139014929000736L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreatedBy
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="created_by", nullable = false, updatable = false)
private AccountInfo createdBy;
@CreatedDate
@Column(name="created_date", nullable = false, updatable = false)
private LocalDateTime createdDate;
}
만든이 정보만 필요하다면 이와같이 BaseEntity
를 상속하지않고 직접 column을 추가해주자.
BaseEntity
를 상속하지 않으니 클래스에 @EntityListeners
추가가 필요하다
5. 예외상황
예를들어 BaseEntity를 상속한 Board Entity(게시글)이 있다고 가정하자. 게시글을 조회하는 Get Request 마다 조회수 칼럼을 +1 식 Count 하는 서비스를 구성해야 한다면 게시글 Entity의 조회수에 관한 Column 의 Count를 +1을 하는 코드가 삽입될 것이다. Update가 되면 게시글의 lastModifiedBy에 조회자의 Id가 덮어씌워지는 상황이 발생한다.
아래의 방법들로 이 경우를 대비해야 한다.
- @LastModifiedBy를 사용하지말고 동역할을 하는 Column으로 생성하여 게시글 수정시에서만 lastModifiedBy를 Update하도록 하자.
- JPQL로 직접 조회수 Update Query를 작성하여 Service 로직을 구성한다. @LastModifiedBy 가 작동되는 시점은 Repository의 Save()와 Dirty Checking이 일어날때 작동된다. Save()가 아닌 JPQL로직접 Query를 실행시키는것이 되므로 LastModifiedBy가 최신화되지않는다.