이전 포스팅에 이어 Entity Listener 정리.

 

예제 환경은 이전 포스팅과 마찬가지로 아래와 같다.

  • Intelli J
  • SpringBoot 2.6.2
  • Lombok
  • Gradle 7.3.2

 

createdAt과 updatedAt을 활용해 계속 Listener를 테스트해보고 있는데 이것들은 많이 사용되는 데이터이기 때문에 스프링에서는 별도의 기본 Listener를 제공하고 있다.

 

이 기본 Listener를 사용하기 위해 Application에 @EnableJpaAuditing 어노테이션을 추가하고

각 엔티티의 @EntityListeners에서 value로 MyEntityListener가 아닌 AuditingEntityListener를 추가해준다.

 

그리고 각 Entity에서 createdAt에는 @CreatedDate, updatedAt에는 @LastModifiedDate 어노테이션을 붙여주면 자동으로 해당 값을 처리해주게 된다.

 

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

@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
@EntityListeners(value = { AuditingEntityListener.class, UserEntityListener.class } )
public class User implements Auditable {
  
  @Id
  @GenereatedValue
  private Long id;
  
  @NonNull
  private String name;
  
  @NonNull
  private String email;
  
  @Column(updatable = false)
  @CreatedDate
  private LocalDateTime createdAt;
  
  @LastModifiedDate
  private LocalDateTime updatedAt;
  
  @Enumerated(value = EnumType.STRING)
  private Gender gender;
  
}


//UserHistory Entity

@Data
@NoArgsConstructor
@Entity
@EntityListeners(value = AuditingEntityListener.class)
public class User implements Auditable {
  
  @Id
  @GenereatedValue
  private Long id;
  
  private Long userId;
  
  private String name;
  
  private String email;
  
  @CreatedDate
  private LocalDateTime createdAt;
  
  @LastModifiedDate
  private LocalDateTime updatedAt;
  
}

그리고 테스트를 실행해보면 정상적으로 잘 처리된다.

 

 

이렇게 기본적으로 제공하는 리스너를 사용함으로써 기존에 사용하던 MyEntityListener는 사용하지 않아도 된다.

 

이 AuditingListener에서는 몇가지를 제공해주고 있는데 안에 들어가보면 여러 어노테이션들이 있다.

그중에 createdDate안에 있는 LastModifiedBy가 있는데

이건 생성 또는 수정한 사람의 정보를 함께 저장할 수 있는 기능이다.

누가 언제 생성했는지 누가 언제 수정했는지에 대한 것인데 스프링 시큐리티의 인증정보를 가져와 사용하는 방법으로 활용하면 좀 더 편리하게 사용이 가능하다.

 

 

그럼 여기까지가 이제 Entity Listener를 사용하는 방법들이었고 좀 더 실용적인 방법이 있다.

 

지금은 각 엔티티에 createdAt과 updatedAt이 중복되고 있다. 둘다 같은 형태로 사용하고 있는데 계속 반복해서 엔티티들에 들어가있는 상태다.

이런것 또한 분리해서 처리해주는게 좀 더 실용적이라고 볼 수 있다.

 

지금까지의 정리된 부분들은 이렇게 쓸 수 있구나 정도로 넘어가고 실용적인 방법으로 넘어간다.

 

일단 엔티티에 중복되는 createdAt과 updatedAt을 삭제한다.

그리고 BaseEntity라는 새로운 클래스를 생성해 그 안에 createdAt과 updatedAt을 넣어준다.

 

@Data
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public class BaseEntity {

  @Column(updatable = false, columnDefinition = "datetime(6) default now(6)")
  @CreatedDate
  private LocalDateTime createdAt;
  
  @Column(columnDefinition = "default(6) default now(6)")
  @LastModifiedDate
  private LocalDateTime updatedAt;
  
}
//User Entity

@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
@EntityListeners(value = UserEntityListener.class)
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class User extends BaseEntity implements Auditable {
  
  @Id
  @GenereatedValue
  private Long id;
  
  @NonNull
  private String name;
  
  @NonNull
  private String email;
  
  @Enumerated(value = EnumType.STRING)
  private Gender gender;
  
}


//UserHistory Entity

@Data
@NoArgsConstructor
@Entity
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class User extends BaseEntity implements Auditable {
  
  @Id
  @GenereatedValue
  private Long id;
  
  private Long userId;
  
  private String name;
  
  private String email;
  
}

BaseEntity에 붙어있는 @MappedSuperclass 어노테이션은 해당 클래스의 필드를 상속받는 엔티티의 컬럼으로 포함시키겠다는 의미다.

즉, BaseEntity 안에 있는 createdAt과 updatedAt을 상속받고 있는 User 엔티티와 UserHistory 엔티티의 컬럼으로 포함된다는 의미다.

그래서 각 엔티티에서는 createdAt과 updatedAt을 따로 작성하지 않아도 된다.

 

그리고 BaseEntity에서 EntityListener를 명시하고 있기 때문에 상속받는 엔티티에서는 EntityListener를 명시할 필요가 없다.

단, User 엔티티에서는 UserHistory 데이터를 같이 처리하기 위해 UserEntityListener를 처리하도록 해야 하기 때문에 명시 해야한다.

 

각 엔티티에 추가적으로 붙은 어노테이션으로 @EqualsAndHashCode(callSuper = true)와 @ToString(callSuper = true)가 있다.

 

이 두 어노테이션을 붙이지 않은 상태로 테스트코드를 실행하게 되면

Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is Intentional, add ‘@EqualsAndHashCode(callSuper=false)’ to your type.

이런 오류가 발생한다.

equals/hashCode 구현을 생성하지만 이 클래스가 java.lang.Object를 확장하지 않더라도 superClass에 대한 호출이 없다. 이것이 의도적인 경우 유형에 @EqualsAndHashCode(callSuper = false)를 추가하라는 내용이다.

 

super class인 BaseEntity에 대한 정보가 처리되지 않는다는 것이다.

callSuper의 경우 이 호출이 제대로 처리되고 있지 않은 상황인데 이걸 의도적으로 한게 맞다면 false를 추가해 내가 의도적으로 이렇게 호출이 제대로 처리되지 않도록 했다 라고 명시하라는 것인데 지금은 의도적으로 한것이 아니기 때문에 true로 선언해 처리해줘야 한다.

 

@ToString(callSuper = ture)의 경우는 누락되면 별다른 오류가 발생하지 않고 createdAt과 updatedAt이 출력되지 않는다.

강의에서도 별다른 언급이 없어서 고민을 좀 해봤다.

 

@EqualsAndHashCode에서 발생한 오류처럼 동일하게 super class인 BaseEntity 호출이 제대로 처리되지 않은 상태라 callSuper = true 라고 명시하지 않는다면 BaseEntity에 대한 ToString을 무시해서 출력이 안되는것이라는 생각이 든다.

 

그래서 callSuper = true로 설정해 상속받고 있는 클래스까지 ToString을 생성하겠다는 것이라고 생각한다.

 

 

 

여기까지 완성된 코드를 전체적으로 보면 각 엔티티에서 중복되는 createdAt과 updatedAt은 BaseEntity라는 하나의 엔티티에 넣어 이것을 상속받는 모든 엔티티에서 컬럼으로 포함시킬 수 있게 되었고

상속받는 엔티티에서 데이터추가나 수정을 한다면 createdAt과 updatedAt에 현재 시간을 바로바로 넣어줄 수 있게 되었다.

 

일반적으로 이렇게 중복되는 컬럼들을 따로 모아 Entity를 작성하고 그 Entity를 상속받도록 하는 형태의 개발방식으로 많이 진행한다고 한다.

 

 

 

Reference

  • 패스트캠퍼스 java/spring 초격차 패키지 Spring Data JPA

 

 

+ Recent posts