Syncope.T-*
Published 2022. 9. 1. 00:00
Audit BackEnd/Spring
728x90

개요

모든 데이터에는 누가 등록했는지 구별하는 컬럼이 존재한다. 하지만 해당 컬럼을 일일이 다 입력해주는 건 너무 지겨운 작업이다.

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;
}
📌
우리는 해당 BaseEntity를 JPA가 테이블에 접근하는 시점에만 JPA가 불러오게끔 하고픈데 만약 개발자에 의해 수정되면 안되기 때문에 updatablefalse로 해주는 것을 권장한다.

추가로 로그인하지 않은 비회원일 경우에도 insert를 한다면 nullabletrue로 설정해주어야 오류가 발생하지 않는다.

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가 덮어씌워지는 상황이 발생한다.

아래의 방법들로 이 경우를 대비해야 한다.

  1. @LastModifiedBy를 사용하지말고 동역할을 하는 Column으로 생성하여 게시글 수정시에서만 lastModifiedBy를 Update하도록 하자.
  2. JPQL로 직접 조회수 Update Query를 작성하여 Service 로직을 구성한다. @LastModifiedBy 가 작동되는 시점은 Repository의 Save()와 Dirty Checking이 일어날때 작동된다. Save()가 아닌 JPQL로직접 Query를 실행시키는것이 되므로 LastModifiedBy가 최신화되지않는다.
profile

Syncope.T-*

@Syncope

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...