스프링부트

spring data MongoDB - auditing 설정 및 활용

알쓸개잡 2024. 2. 26.

MongoDB 애플리케이션을 운영하다 보면 데이터 생성, 수정 이력(작성자, 생성시간, 수정자, 수정시간) 필드가 대부분 공통으로 존재한다. Spring Data MongoDB의 Auditing 기능은 이러한 도큐먼트 생성 이력을 자동으로 관리할 수 있는 기능을 제공한다.

이번 포스팅에서는 spring data MongoDB 에서 Auditing 기능 사용법에 대해서 정리하고자 한다.

 

Auditing 기능 활성화

@Configuration
@EnableMongoAuditing
public class MongoDBConfigurationByDirect {
...
}

@EnableMongoAuditing 어노테이션은 Spring Data MongoDB에서 제공하는 auditing 기능을 활성화하기 위해서 사용한다.

@EnableMongoAuditing을 사용하면 엔티티가 데이터베이스에 저장되거나 업데이트될 때 자동으로 생성시간, 수정시간, 생성자, 수정자 등의 정보를 기록할 수 있다.

 

@EnableMongoAuditing 어노테이션은 다음과 같은 속성을 제공한다.

  • auditorAwareRef : AuditorAware 인터페이스를 구현한 빈의 이름을 지정한다. 이를 통해 엔티티의 생성자와 수정자를 결정하는 로직을 커스터마이징 할 수 있다.
  • setDates : 생성 및 수정 날짜를 자동으로 설정할지 여부를 결정한다. (default: true)
  • modifyOnCreate : 엔티티 생성 시 'lastModifiedDate'를 설정할지 여부를 결정한다. (default: true)
  • dateTimeProviderRef : 날짜와 시간을 제공하는 빈의 이름을 지정한다. 이를 통해 날짜와 시간 값의 생성 방식을 커스터마이징 할 수 있다.

Auditable 베이스 클래스 만들기

여러 문서에서 공통으로 사용할 기본 감시 필드를 생성한다.

엔티티(도큐먼트) 클래스에 감사데이터로 사용할 필드에 @CreatedBy (생성자), @CreatedDate (생성시간), @LastModifiedBy (수정자),  @LastModifiedDate (수정시간) 어노테이션을 지정한다.

@Getter
@Setter
public abstract class Auditable implements Persistable<UUID> {

    @Id
    private UUID id;
    
    @CreatedBy
    private String createdBy;

    @CreatedDate
    private Instant createdDate;

    @LastModifiedBy
    private String lastModifiedBy;

    @LastModifiedDate
    private Instant lastModifiedDate;
    
    @Transient
    private boolean isNew;

    @Override
    public boolean isNew() {
        return isNew || id == null;
    }
}

일반적으로 Audit 필드만 정의된 base 클래스를 정의 후 이를 상속하여 사용하는 패턴을 많이 사용한다.

Persistable 인터페이스를 확장하여 isNew() 메서드를 오버라이드 할 수 있는데, 이 메서드는 저장되는 document 가 새로 생성된 것인지를 판별할 수 있도록 한다. 샘플 코드에서는 isNew 필드 값이 true 혹은 id가 null인 경우 새로 생성하는 document로 판단한다.

isNew 필드를 추가한 이유는 새로 생성한 document라고 하더라도 사용자가 직접 id를 지정할 수 있기 때문이다.

isNew() 메서드를 정의하지 않으면 Document 저장시 createdDate 필드는 auditing 되지 않고 lastModifiedDate 필드만 업데이트된다. 해당 문서가 새로 생성되는 것인지 여부를 판단할 수 없어서 인듯 하다.

 

@Document(collection = "person")
@Getter
@Setter
@ToString
public class PersonDocument extends Auditable {
    private final String personName;
    private final Integer personAge;
    private Gender personGender;
    private String personNation;
    private final String comment;


    public static PersonDocument create( String personName, Integer personAge,
                                         Gender personGender, String personNation, String comment ) {
        PersonDocument doc = new PersonDocument();
        doc.setPersonName(personName);
        doc.setPersonAge(personAge);
        doc.setPersonGender(personGender);
        doc.setComment(comment);
        doc.setNew(true);
        return doc;
    }    
}

Auditing을 적용하고자 하는 Document는 Auditable을 확장하여 사용한다.

 

AuditorAware 구현

실제로 누가 생성하고 수정했는가를 기록하기 위해서 AuditorAware를 구현한다.

AuditorAware 인터페이스를 구현하여 이를 빈드로 등록하면 @CreatedBy, @LastModifiedBy 어노테이션에 지정된 생성자와 수정자 정보를 결정하는 로직을 커스텀하게 작성할 수 있다.

일반적으로 Spring Security와 함께 현재 인증된 사용자의 정보를 SecurityContextHolder 스레드 로컬로부터 가져와서 해당 사용자의 정보를 생성자, 수정자로 자동 지정할 수 있다.

 

import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Optional;

public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null || !authentication.isAuthenticated()) {
            return Optional.of("system");
        }

        return Optional.ofNullable(authentication.getName());
    }
}

위 코드는 AuditorAware<String> 인터페이스를 구현하며, <String>은 생성자, 수정자의 데이터 타입을 나타낸다.

AuditorAwareImpl 클래스는 빈으로 등록되어야 한다.

AuditorAware 인터페이스는 getCurrentAuditor() 메서드 하나만 가진 함수형 인터페이스다.

따라서 빈을 생성 시에 lambda로 생성이 가능하다.

@EnableMongoAuditing 어노테이션의 auditorAwareRef 속성에 빈 이름을 지정한다.

import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.EnableMongoAuditing;

@Configuration
@EnableMongoAuditing(auditorAwareRef = "auditorAwareImpl")
public class MongoConfig {
    // AuditorAware 빈 정의
    @Bean
    public AuditorAware<String> auditorAwareImpl() {
        return new AuditorAwareImpl();
    }
}
AuditorAware 클래스를 구현한 빈이 반드시 auditorAwareRef 속성에 지정되지 않아도 동작하지만 AuditorAware를 구현한 클래스는 반드시 빈으로 등록되어야 한다.

AuditorAware 인터페이스의 getCurrentAuditor() 메서드가 반환하는 값은 엔티티가 최초로 저장될 때 @CreatedBy 어노테이션이 지정된 필드와 @LastModifiedBy 어노테이션이 지정된 필드에 모두 할당된다. 

이후 엔티티가 수정될 대마다 @LastModifiedBy 어노테이션이 지정된 필드는 새로운 수정자 정보로 업데이트하지만 @CreatedBy 필드는 최초 생성 시 할당된 값이 유지된다.

 

createdDate, lastModifiedDate 필드 타입은 Instant 타입을 사용

타임존 문제를 피하기 위해서는 ISO-8601 + Instant 조합을 사용하는 것이 좋다.

spring data mongodb는 document를 저장시 내부적으로 ISO-8601(UTC) 형식의 날짜로 변환하여 저장한다.

이를 통해서 타임존 문제를 해결 할 수 있다. 만약 LocalDateTime 필드를 사용하게 된다면 타임존 정보가 없기 때문에 시간 처리에 대한 다양한 문제가 발생할 수 있다.

Instant 타입을 사용함으로써 얻는 이점은 다음과 같다.

  • 서버/클라이언트/DB 타임존에 대한 혼란을 해소한다.
  • 글로벌 서비스를 하는데는 필수적이다.
  • MongoDB의 날짜 타입(Date)와 매핑이 가능하다.

 

생성된 Document

Auditing 이 적용되어 생성된 document 샘플은 다음과 같다.

{
  "_id": {
    "$binary": {
      "base64": "P7jmfXy7RtecgiWcweHsBQ==",
      "subType": "04"
    }
  },
  "personName": "test-name",
  "personAge": 100,
  "personGender": "성별 없음",
  "personNation": "Korea",
  "comment": "test-name 코멘트",
  "createdBy": "system",
  "createdDate": {
    "$date": "2024-02-25T16:39:14.264Z"
  },
  "lastModifiedBy": "system",
  "lastModifiedDate": {
    "$date": "2024-02-25T16:39:14.264Z"
  },
  "_class": "com.example.springmongodb.documents.PersonDocument"
}

 

끝.

댓글

💲 추천 글