우아한 테크코스 Level3 팀 프로젝트의 게시물 등록 기능 개발 중,
게시물 등록 시간을 저장해야 하는 기능을 구현해야 했습니다.
팀원들과 적용한 방법에 대해 소개해보겠습니다.
방법만 알고 싶으신 분은, 아래의 동작하는 코드와 글의 최하단에 요약만 보셔도 이해를 하실 수 있습니다.
먼저 동작하는 코드입니다.
@Entity
@Getter
@EntityListeners(AuditingEntityListener.class) // 3, 4
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
@CreatedDate // 1
private LocalDateTime createdAt;
}
@SpringBootApplication
@EnableJpaAuditing // 2
public class SokdakApplication {
public static void main(String[] args) {
SpringApplication.run(SokdakApplication.class, args);
}
}
중요한 annotation에 대해서 살펴보겠습니다.
Declares a field as the one representing the date the entity containing the field was created.
엔티티가 생성된 날짜를 나타내는 필드를 선언합니다.
출처 : https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/annotation/CreatedDate.html
생성 날짜를 자동으로 매핑해주는 어노테이션입니다.
생성 날짜를 자동으로 매핑해주고 싶은 엔티티의 필드에 설정하면 됩니다.
Annotation to enable auditing in JPA via annotation configuration.
JPA에서 Auditing을 가능하게 해주는 어노테이션.
출처 : https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/config/EnableJpaAuditing.html
Audit은 감시하다라는 뜻으로, JPA에서 엔티티의 변경을 감지하는 기능입니다.
Auditing은 default로 설정되어 있지 않기 때문에, @EnableJpaAuditing을 직접 지정해주어야합니다.
메인 클래스에 @SpringBootApplication 이외의 어노테이션이 존재하지 않도록 하고 싶으면 아래와 같이 Configure해줄 수 있습니다.
또, @EnableJpaAuditing이 메인클래스에 존재한다면 생길 수 있는 문제가 있기 때문에, 지양하는 것이 좋습니다. 이에 대해서는, 다음에 알아보겠습니다!!
@Configuration
@EnableJpaAuditing
public class JPAConfig {
}
Specifies the callback listener classes to be used for an entity or mapped superclass. This annotation may be applied to an entity class or mapped superclass.
엔티티 혹은 mapped superclass에 사용될 콜백 리스너 클래스를 지정하는 것. 이 annotation은 엔티티 클래스 혹은 mapped superclass에 사용된다.
출처 : https://docs.oracle.com/javaee/7/api/javax/persistence/EntityListeners.html
JPA에서 엔티티의 Persist, Update 등과 같은 특정 이벤트를 감지하고 이벤트에 따른 동작을 수행할 수 있도록 지원해주는 기능입니다.
아래와 같이 콜백리스너 클래스를 속성으로 가지고 지정해줘야합니다.
콜백리스너 객체에 정의된 이벤트들을 감지하고 특정 로직을 실행할 수 있도록 해줍니다.
JPA에서는 아래와 같이 7개의 이벤트를 지원합니다.
다시 돌아와서,
저희는 엔티티를 저장할 때의 시간을 알아야 합니다. persist를 할 때는, 어떤 이벤트가 호출될까요?
@Entity
@Getter
@EntityListeners(TestEntityListener.class)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
@CreatedDate
private LocalDateTime createdAt;
protected Post() {
}
@Builder
private Post(String title, String content) {
this.title = new Title(title);
this.content = new Content(content);
}
public class TestEntityListener {
@PostLoad
public void postLoad(Object o) {
System.out.println("--------------postLoad--------------");
}
@PrePersist
public void prePersist(Object o) {
System.out.println("--------------prePersist--------------");
}
@PostPersist
public void postPersist(Object o) {
System.out.println("--------------postPersist--------------");
}
@PreUpdate
public void preUpdate(Object o) {
System.out.println("--------------preUpdate--------------");
}
@PostUpdate
public void postUpdate(Object o) {
System.out.println("--------------postUpdate--------------");
}
@PreRemove
public void preRemove(Object o) {
System.out.println("--------------perRemove--------------");
}
@PostRemove
public void postRemove(Object o) {
System.out.println("--------------postRemove--------------");
}
}
위와 같은 상황에서 아래와 같은 테스트를 진행하면
@SpringBootTest
class MyTest {
@PersistenceContext
private EntityManager em;
@Transactional
@DisplayName("")
@Test
void testEvent() {
Post post = Post.builder()
.title("제목")
.content("본문")
.build();
em.persist(post);
em.flush();
System.out.println(post);
}
}
아래의 출력문에서 보시다시피, @prePersist와 @postPersist 이벤트를 감지합니다.
--------------prePersist--------------
Hibernate:
insert
into
post
(post_id, content, created_at, title)
values
(default, ?, ?, ?)
2022-07-07 21:06:06.666 TRACE 63680 --- [ Test worker] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [CLOB] - [본문]
2022-07-07 21:06:06.669 TRACE 63680 --- [ Test worker] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [TIMESTAMP] - [null]
2022-07-07 21:06:06.669 TRACE 63680 --- [ Test worker] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [VARCHAR] - [제목]
--------------postPersist--------------
그렇다면 프로덕션에서 EntityListeners에 사용한 AuditingEntityListener는 무엇일까요?
JPA entity listener to capture auditing information on persiting and updating entities.
영속화되고 변경되는 엔티티들의 auditing 정보를 캡쳐하는 JPA entity Listener이다.
출처 : https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/domain/support/AuditingEntityListener.html
엔티티가 영속화(저장)되고 변경되는 경우에, 시간 정보를 자동으로 주입해주는 콜백 리스너 클래스입니다.
package org.springframework.data.jpa.domain.support;
@Configurable
public class AuditingEntityListener {
private @Nullable ObjectFactory<AuditingHandler> handler;
public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
this.handler = auditingHandler;
}
//============== 여기서 등록 시간이 자동 지정됩니다. ================
//Sets modification and creation date and auditor on the target object
// in case it implements Auditable on persist events.
@PrePersist
public void touchForCreate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markCreated(target);
}
}
}
//===========================================================
@PreUpdate
public void touchForUpdate(Object target) {
Assert.notNull(target, "Entity must not be null!");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markModified(target);
}
}
}
}
AuditingEntityListener는 @PrePersist와 @PreUpdate를 지원합니다.
위의 테스트에서 persist()가 호출될 때, @PrePersist와 @PostPersist를 감지하기 때문에,
@PrePersist에서 저장 시간이 자동 등록되는 것을 알 수 있습니다.
필자는 내부 로직이 너무 복잡해서 내부적으로 어떻게 시간을 자동 지정하는지 완벽하게 이해하지 못했습니다.
하지만 방식이 궁금하시다면
위 AuditableBeanWrapper
인터페이스의 setCreatedDate
와
아래 AuditingHandlerSupport
클래스의 192번 라인에 break를 찍으시고 디버깅 모드로 실행해보시면 시간을 자동 지정해주는 방식을 아시는데 도움이 될 것이라고 생각합니다.
시간을 자동 생성 해주는 기능에 대해서 알아보았습니다.
간략히 요약하자면
JPA 마스터가 되는 그날까지.. 화이팅!!!!