ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA ] Auditing 기능 살펴보기
    Spring 2020. 11. 26. 22:05
    반응형

    Spring Boot 2.4.0 / Spring Data Jpa / JDK 8 / h2database

     

    개요

    • Auditing 활성화하기
    • BaseEntity 생성하기
    • Entity에 적용하기
    • JUnit으로 테스트해보기 1
    • @CreatredBy, @ModifiedBy 사용하기
    • JUnit으로 테스트 해보기 2
    • 실무에서 사용하기

     

    서론

    ORM(Object Relationship Mapping) 기술인 JPA는 Application의 Entity와 DB의 Table을 매핑하여 사용합니다. DB는 해당 데이터를 누가, 언제 생성 또는 수정했는지 기록하는 것이 꽤나 중요합니다. 이 데이터들은 많은 테이블에서 사용되기 때문에 Entity에도 필드로 중복되어 들어가고, 해당 Entity가 생성 또는 수정될 때마다 개발자가 신경 써서 데이터를 입력해줘야 하는 번거로움이 생기게 됩니다. 이때 사용하는 기술이 Spring Data에서 제공하는 Auditing입니다. Audit는 감독하고 검사하다는 뜻으로, 해당 데이터를 보고 있다가 생성 또는 수정이 발생하면 자동으로 값을 넣어주는 편리한 기능입니다.

     


     

    Auditing 활성화 하기

    가장 먼저 SpringBootApplication에 @EnableJpaAuditing 어노테이션을 추가해줍니다.

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

     


     

    BaseEntity 생성하기

    Auditing이 필요한 Entity에서 상속받을 BaseEntity를 생성합니다.

    @Getter
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class BaseEntity {
    
        @CreatedDate
        @Column(updatable = false)
        private LocalDateTime createdDate;
    
        @LastModifiedDate
        private LocalDateTime modifiedDate;
    
        @CreatedBy
        @Column(updatable = false)
        private String createdBy;
    
        @LastModifiedBy
        private String modifiedBy;
    
    }

    @MappedSuperclass (javax.persistence)

    Entity에서 Table에 대한 공통 매핑 정보가 필요할 때 부모 클래스에 정의하고 상속받아 해당 필드를 사용하여 중복을 제거

     

    @EntityListeners  (javax.persistence)

    Entity를 DB에 적용하기 이전, 이후에 커스텀 콜백을 요청할 수 있는 어노테이션

     

    Class AuditingEntityListener (org.springframework.data.jpa)

    Entity 영속성 및 업데이트에 대한 Auditing 정보를 캡처하는 JPA Entity Listener

     

    @CreatedDate (org.springframework.data)

    데이터 생성 날짜 자동 저장 어노테이션

     

    @LastModifiedDate (org.springframework.data)

    데이터 수정 날짜 자동 저장 어노테이션

     

    @CreatedBy (org.springframework.data)

    데이터 생성자 자동 저장 어노테이션

     

    @LastModifiedBy (org.springframework.data)

    데이터 수정자 자동 저장 어노테이션


     

    Entity에 적용하기

    id와 name을 가지고 있는 간단한 User Entity에 BaseEntity를 상속받습니다.

    @Getter
    @Entity
    @NoArgsConstructor(access = PROTECTED)
    public class User extends BaseEntity {
    
        @Id
        @GeneratedValue
        @Column(name = "user_id")
        private Long id;
    
        private String name;
    
        public User(String name) {
            this.name = name;
        }
        
        public void changeName(String name) {
            this.name = name;
        }
    
    }

     

    JUnit으로 테스트해보기

    @Transactional
    @SpringBootTest
    @Rollback(false)
    class UserTest {
    
        @Autowired
        UserRepository userRepository;
    
        @Test
        @DisplayName("유저 Auditing 테스트")
        void user_test() {
            // given
            User user = new User("userA");
    
            // when
            User savedUser = userRepository.save(user);
    
            // then
            assertThat(savedUser.getName()).isEqualTo(user.getName());
        }
    
    }

    그림처럼 이름이 userA인 Entity가 생성일, 수정일과 함께 테이블에 insert 되었습니다.

     

    userA의 이름을 수정하면 modified_date가 변경되어야 합니다.

    @Transactional
    @SpringBootTest
    @Rollback(false)
    class UserTest {
    
        @Autowired
        UserRepository userRepository;
    
        @Autowired
        EntityManager em;
    
        @Test
        @DisplayName("유저 Auditing 테스트")
        void user_modified_test() {
            // given
            User user = userRepository.findById(1L).orElseThrow(EntityNotFoundException::new);
    
            // when
            /*  주의: JpaRepository의 save가 아닌 더티 체킹으로 수정
            * persistant context에 존재하는 entity는 persist()가 아닌 merge()가 실행됨 */
            user.changeName("userB");
    
            em.flush();
            em.clear();
    
            User findUser = userRepository.findById(1L).orElseThrow(EntityNotFoundException::new);
    
            // then
            assertThat(findUser.getName()).isEqualTo(user.getName());
        }
    
    }

    userA가 userB로 바뀌면서 modified_date의 날짜도 변경되었습니다.

     

    하지만 이상한 점이 하나 있습니다. create_by와 modified_by 컬럼은  null 값이라는 점인데요, 날짜와 다르게 생성자, 수정자는 JPA에서 알 수 없기 때문에 개발자가 어떤 데이터를 기준으로 넣어줄 것인지 알려줘야 합니다.


     

    @CreatedBy, @ModifiedBy 사용하기

    org.springframework.data.domain.AuditorAware를 스프링 빈으로 등록해야 합니다.

    public interface AuditorAware<T> {
    
    	/**
    	 * Returns the current auditor of the application.
    	 *
    	 * @return the current auditor.
    	 */
    	Optional<T> getCurrentAuditor();
    }

    AuditorAware<T> 인터페이스는 Optional<T>를 반환하는 method가 하나 있기 때문에 아래 코드처럼 람다로 AuditorAware<T>를 구현한 객체를 반환할 수 있습니다.

    @Bean
    public AuditorAware<String> auditorProvider() {
      // 람다를 이용
      return () -> Optional.of(UUID.randomUUID().toString());
    
      // 익명 클래스를 이용
      return new AuditorAware<String>() {
        @Override
          public Optional<String> getCurrentAuditor() {
          	return Optional.of(UUID.randomUUID().toString());
        }
      };
    }

     

     

    생성자와 수정자는 랜덤 값인 UUID로 지정하겠습니다.


     

    JUnit으로 테스트해보기

     

    코드는 위와 동일합니다.

     

    UUID의 값이 created_by와 modified_by 컬럼에 자동으로 insert 되었습니다.

     

    random값으로 지정했기 때문에 userA가 userB로 바뀌면서 modified_by의 값도 변경되었습니다. 

     

    ** modified_date와 modified_by를 null로 하고 싶다면 SpringBootApplicaiton의 @EnableJpaAuditing 어노테이션에 (modifyOnCreate = false) 속성을 적용해줍니다. 하지만 컬럼이 null인 것보다 값이 있는 게 유지 보수하기 편합니다.


     

    실무에서 사용하기 

    대부분의 Table에 created_date와 modified_date는 필요하지만 created_by와 modified_by는 특정 테이블에서만 필요한 경우가 많습니다. 그렇기 때문에 created_by와 modified_by는 선택적으로 사용할 수 있도록 MappedSuperclass를 분기하도록 하겠습니다.

     

    생성일, 수정일 필드가 들어있는 BaseTimeEntity를 만들어 최상위 부모로 사용하면 User와 같은 생성자, 수정자가 필요 없는 Entity는 BaseTimeEntity만 상속받아 사용하면 깔끔하게 해결할 수 있습니다.

     

    BaseTimeEntity

    @Getter
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class BaseTimeEntity {
    
        @CreatedDate
        @Column(updatable = false)
        private LocalDateTime createdDate;
    
        @LastModifiedDate
        private LocalDateTime modifiedDate;
    
    }

     

    BaseEntity

    @Getter
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class BaseEntity extends BaseTimeEntity {
    
        @CreatedBy
        @Column(updatable = false)
        private String createdBy;
    
        @LastModifiedBy
        private String modifiedBy;
    
    }
    

     

    이렇게 분기처리를 했지만 createdDate 또는 modifiedDate 둘중 하나의 컬럼만 사용하는 경우에는 Entity에 직접 필드로 추가해주는 방식을 사용합니다.

     

    UserEntity

    @Getter
    @Entity
    @NoArgsConstructor(access = PROTECTED)
    @EntityListeners(AuditingEntityListener.class) // 추가
    public class User extends BaseEntity {
    
        @Id
        @GeneratedValue
        @Column(name = "user_id")
        private Long id;
    
        private String name;
        
        @CreatedDate
        @Column(updatable = false)
        private LocalDateTime createdDate; // 추가
    
        public User(String name) {
            this.name = name;
        }
        
        public void changeName(String name) {
            this.name = name;
        }
    
    }

     

    @EntityListeners 전체 적용하기

    @EntityListeners(AuditingEntityListener.class)를 생략하고 스프링 데이터 JPA가 제공하는 이벤트를 엔티티 전체에 적용하려면 orm.xml에 다음 내용을 등록하면 됩니다.

     

    /resources/META-INF/orm.xml

     

    <?xml version="1.0" encoding="UTF-8"?>
    <entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd"
    version="2.2">

    <persistence-unit-metadata>
    <persistence-unit-defaults>
    <entity-listeners>
    <entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener"/>
    </entity-listeners>
    </persistence-unit-defaults>
    </persistence-unit-metadata>

    </entity-mappings>

     

     

    이상으로 Auditing을 모두 살펴보았습니다. 감사합니다.

    반응형
Designed by Tistory.