Hibernate/SpringDataJPA

[JPA] Auditing과 MappedSuperClass(상속)

유혁스쿨 2023. 6. 12. 22:26
728x90
반응형

Auditing

 

JPA에서 'Audit'는 감시하다 라는 뜻으로 사용된다.

각 데이터 마다 '누가', '언제' 데이터를 생성하고 변경했는지 감시한다는 의미이다.

엔티티 클래스에는 공통적으로 들어가는 필드가 있다.

예를들어 '등록일자'와 '수정일자' 같은 것이다.

 

대표적으로 가장 많이 사용되는 필드를 나열해보면 다음과 같다.

  1. 등록자
  2. 등록일자
  3. 수정자
  4. 수정일자

매번 엔티티를 생성하거나 변경헐 때마다 값을 주입해야 하는 번거로움이 있다.

이 같은 번거로움을 해소하기 위해 Spring Data JPA에서는 이러한 값을 자동으로 넣어주는 기능을 제공한다.

 

각각의 기능에 대한 어노테이션은 아래와 같다.

등록자 @CreateBy
등록일자 @CreateDate
수정자 @LastModifiedBy
수정일자 @LastModifiedDate

 

JPA Auditing 기능 활성화

 

가장 먼저 스프링 부트 애플리케이션에 Auditing 기능을 활성화 한다.

서버를 구동시키는 main() 메소드가 있는 클래스에 아래와 같이 @EnabledAuditing 어노테이션을 추가하면 된다.

 

@SpringBootApplication
@EnableJpaAuditing
public class AuditingExampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(AuditingExampleApplication.class, args);
	}

}

위와 같이 @EnabledAuditing 어노테이션을 추가하면 정상적으로 기능이 활성화 되지만

테스트 코드를 작성해서 애플리케이션을 테스트하는 일부 상황에서는 오류가 발생할 수 있다.

예를들어 @WebMvcTest 어노테이션을 지정해서 테스트를 수행하는 코드를 작성하면 애플리케이션 클래스를 호출하는 과정에서 예외가 발생할 수 있다.

이 같은 문제를 해결하기 위해 아래와 같이 별도의 Configuration 설정 클래스를 생성해서 애플리케이션 클래스의 기능과 분리해서 활성화 할 수 있다.

 

@Configuration
@EnabledAuditing
public class JpaAuditingConfiguration{
}

 

@CreateDate / @LastModifiedDate

@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@ToString
@Entity
public class Recipe {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer rno; // 레시피 번호 r_no rno
    @Column
    private String title; //레시피 제목
    @Column(columnDefinition = "longtext")
    private String content; //레시피 내용
    @Column(columnDefinition = "int default 0") //default 0
    private Integer cnt; // 조회수
    @Column(columnDefinition = "boolean default TRUE constraint enabled check(enabled in(TRUE,FALSE))")
    private Boolean enabled; // 삭제여부
    @Column(columnDefinition = "int")
    private Double kcal; //칼로리
    @Column
    private String thumbPath; //썸네일경로
    @Column
    private String stitle; //상세설명
    @Column
    private String mat;//핵심재료
    @Column
    private String source;//양념
    @Column
    private Double rating;//별점
    
    @Column
    private Date regDate; // 등록일자
    @Column
    private Date modDate; // 수정일자
    @Column
    private String regId; // 작성자 아이디
}

위와 같은 Recipe Entity가 존재한다고 할 때, 최하단 아래의 3개의 컬럼에 해당하는 필드를 Auditing 기능에서 공통적으로 주입할 수 있는 데이터 필드로 적용할 수 있다.

    @Column
    private Date regDate; // 등록일자
    @Column
    private Date modDate; // 수정일자
    @Column
    private String regId; // 작성자 아이디

이렇게 직접적으로 컬럼을 선언한다면 아래와 같이 개발자가 서비스단에서 직접 주입해야하는 번거로운 작업이 따른다.

 

@Service
public class RecipeService {

    @Autowired
    RecipeRepository recipeRepository;
    
    public void rcpRegist( String userId,String title, String sTitle, String mat, String source, Double kcal, String toastHtml, String toastMarkdown,
                        MultipartFile file) throws Exception {
                        
        Recipe r = new Recipe();
        
        /* ... setter 중략... */
        r.setRegDate(new Date(System.currentTimeMillis()));
        r.setModDate(new Date(System.currentTimeMillis()));
        
        Recipe save = recipeRepository.save(r);

    }
}

 

그럼 이제부터 JPA Auditing을 적용한 코드를 확인해보자.

/*롬복 및 Entity 어노테이션 중략*/
@EntityListeners(AuditingEntityListener.class) // <<<<===== JPA Auditing 추가 코드
public class Recipe {
   
    /**
     * ... 기존 필드 중략
     */
     
    @CreatedDate // <<<<===== JPA Auditing : 데이터 생성 날짜를 자동으로 주입
    @Column
    private Date regDate; // 등록일자
    
    @LastModifiedDate // <<<<===== JPA Auditing : 데이터 수정 날짜를 자동으로 주입
    @Column
    private Date modDate; // 수정일자
}

사용한 주요 어노테이션은 다음과 같다.

  • @EntityListeners : 엔티티를 데이터베이스에 적용하기 전후로 콜백을 요청할 수 있게 하는 어노테이션이다.
  • AuditingEntityListner : 엔터티의 Auditing 정보를 주입하는 JPA 엔터티 리스너 클래스이다.
  • @CreateDate : 데이터 생성 날짜를 자동으로 주입하는 어노테이션이다.
  • @LastModifiedDate : 데이터 수정 날짜를 자동으로 주입하는 어노테이션 이다.

위와 같이 설정하면 앞서 서비스단의 예시처럼 매번 LocalDataTime.now() 메소드를 사용해 시간을 주입하지 않아도 자동으로 값이 생성되는 것을 볼 수 있다.

 

@CreateBy / @LastMidifiedBy

 

누가 엔터티를 생성했고 수정했는지 자동으로 값을 주입하는 기능이다.

이 기능을 사용하려면 AuditorAware를 스프링 빈으로 등록해야 한다.

또한 프로젝트상에서 JWTToken을 사용하였기 때문에 Security와 Token을 활용하여 현재 로그인 되어 있는 정보를 반환받아야 한다.

@RequiredArgsConstructor
@Component
public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<Long> getCurrentAuditor() {

        /* JWT를 활용한 Security (AuthenticationToken의 UserDetails 데이터 */
        AuthenticationToken authenticationToken = (AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        if (authenticationToken == null || !authenticationToken.isAuthenticated()) {
            log.debug("Not found AuthenticationToken");
            return null;
        }

        User authenticationUser = (User) authenticationToken.getDetails();
        log.debug("Found AuthenticationToken: {}", authenticationUserDto);
        return Optional.of(authenticationUser.getUserId());
        
        /* 순수 Security (Authentication의 Principal 데이터) */
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (null == authentication || !authentication.isAuthenticated()) {
            return Optional.empty();
        }
        
        User user = (User) authentication.getPrincipal();
        return Optional.of(user.getUserId());
    
    }
}

LoginUserAuditorAware 클래스를 만들고, AuditorAware을 구현하면서 String 타입의 UserId를 반환하도록 설정했다.

SecurityContextHolder에서 객체가 있는지 확인하고, 없으면 null 있으면 SpringSecurityFilter로부터 UserDetails를 상속받은 User 도메인 클래스타입으로 로그인 정보가 담긴 User 객체를 반환받는다.

해당 User객체로 부터 id를 추출한 뒤 Optional객체에 담아 return하게 되는데 내부적으로 @CreatedBy와 @LastModifiedBy에 반환해준다.

 

이렇게 만든 빈(AuditorAwareImpl)을 참조할 수 있도록 @EnableJpaAuditing의 auditorAwareRef 속성값으로 등록 해준다.

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class JpaAuditConfiguration {
    @Bean
    public AuditorAware<Long> auditorProvider() {
        return new AuditorAwareImpl();
    }
}

이제 @CreatedBy와 @ModifienBy를 엔터티에 추가해서 사용하기만 하면 된다!

 

공통 엔터티(BaseEntity)

코드의 중복을 없애기 위해서는 각 엔터티에 공통으로 들어가게 되는 컬럼(필드)를 하나의 클래스로 빼는 작업을 수행해야 한다.

@Setter @Getter
@ToString
@MappedSuperclass // <<<<===== Auditing을 위한 엔터티 상속 어노테이션
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditingEntity{

    @CreatedDate
    @Column
    private Date regDate;
    
    @LastModifiedDate
    @Column
    private Date modDate;
    
    @CreatedBy // <<<<=====  등록자
    @Column
    private String regId;
    
    @ModifiedBy // <<<<===== 수정자
    @Column
    private String modId; 
    
}

 

@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@ToString(CallSuper = true) // 부모 클래스인 AuditingBaseEntity 필드를 포함하는 역할 수행
@Entity
public class Recipe extends AuditingBaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer rno;
    
    /* 필드 중략 */
    
}
  • @MappedSuperClass : JPA의 엔터티 클래스가 상속받을 경우 자식 클래스에게 매핑 정보를 전달한다.
  • CallSuper = true : 부모 클래스의 필드를 포함하는 역할을 수행한다.
  • 단, abstract 키워드를 선언해준다.(이유...)
728x90
반응형