오늘은 Entity를 데이터에 저장 시, 시간 정보를 자동으로 입력해주는 @CreatedAt를 직접 구현해보았습니다.
순수 도메인 로직의 테스트 코드를 작성하면서 Entity 객체에 @CreatedDate가 있는지 모르고 객체를 생성하여 null을 보고선 문득 내부적으로는 어떻게 동작을 하는지 궁금해졌습니다.
소스코드는 github에서 찾으실 수 있습니다.
1. 코드
검색을 해본 결과 stack overflow에서 원하던 답변을 찾을 수 있었습니다.
결론만 말하자면, Entity가 영속성 컨테스트에 의해 저장되기 전에 @PrePersist
라는 콜백에 의해 객체에 직접 날짜를 입력하는 방법을 사용하고 있었습니다. 코드를 보면 아래와 같습니다.
먼저 Person
이라는 클래스를 생성합니다.
@ToString
@NoArgsConstructor
@Entity
@EntityListeners(CreatedAtListener.class)
public class Person {
@Id
private Long id;
private String name;
@Setter
private LocalDateTime createdAt;
public Person(Long id, String name) {
System.out.println("1. Create Person entity!");
this.id = id;
this.name = name;
}
}
그리고 CreatedAtListener
라는 클래스 또한 생성해줍니다.
public class CreatedAtListener {
@PrePersist
public void setCreateAt(Person person) {
System.out.println("2. Set createdAt before persistence!");
person.setCreatedAt(LocalDateTime.now());
}
}
코드를 설명하면 다음과 같습니다.
@PrePersist
- 해당 어노테이션이 정의된 메소드는 대상 Entity (여기서는 Person)이 데이터베이스에 저장되기 전에 실행됩니다.
- 이 메소드는 person이라는 객체에 현재 날짜를 세팅해줍니다.
- 주의할 점
- 메소드는 void를 리턴해야 합니다.
@EntityListener
에 정의된 경우,@EntityListener
가 정의된 객체가 파라미터가 되어야 합니다. (여기서는 Person)- 해당 콜백 메소드는
EntityManager
나Query
를 호출하지 않습니다. - 콜백 메소드는 stateless 해야 합니다.
@EntityListener
@PrePersist
라는 콜백 메소드를 Listen하는 어노테이션입니다.- Entity 라이프 사이클에 따라 다양한 콜백 메소드(
@PrePersist
,@PostPersist
등...)를 가지며 이를 실행합니다.
정리하면, @PrePersist
를 가진 콜백 메소드를 정의한 CreatedAtListener
클래스를 @EntityListeners
에 세팅하기만 하면 됩니다.
이제 코드를 한번 실행해보겠습니다.
먼저 PersonRepository를 생성하겠습니다.
public interface PersonRepository extends JpaRepository<Person, Long> {
}
이제 CommandLineRunner
로 코드를 실행해보겠습니다.
@SpringBootApplication
public class SpringbootApplication implements CommandLineRunner {
@Autowired
private PersonRepository personRepository;
@Override
public void run(String... args) throws Exception {
Person person = new Person(1L, "bgpark");
Person newPerson = personRepository.save(person);
System.out.println("saved person : " + newPerson);
}
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
실행하면 결과는 다음과 같습니다.
순서를 보면 먼저
- Person 객체를 생성하고
- 생성된 객체에
createAt
을 세팅한 후 - 마지막으로 Person 객체를 데이터베이스에 저장하게 됩니다.
마지막 실행코드에서 Person 객체를 생성할 때, createdAt없이 생성하였습니다 (new Person(1L, "bgpark")
). 하지만 데이터베이스에 저장이되면서 createdAt
이 추가되었음을 알 수 있습니다.
이렇게 Spring Data Jpa가 기본적으로 제공해주는 AuditingEntityListener
의 동작과정을 따라하면서 내부적으로 어떻게 동작하는지 유추할 수 있게 되었습니다.
2. AuditingEntityListener
와 비교
AuditingEntityListener
를 보면 다음과 같습니다.
public class AuditingEntityListener {
@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);
}
}
}
}
위의 코드와 마찬가지로 @PerPersist
라는 콜백 어노테이션을 가지고 있고 Entity가 저장될 때, Listener에 의해 자동으로 생성 날짜, 업데이트 날짜가 객체에 set된 후 데이터베이스에 저장되게 됩니다. 그리고 아래와 같은 방법으로 사용하게 됩니다.
@EntityListeners(value = AuditingEntityListener.class)
3. 후기
앞서 @PrePersist
를 이용하여 @CreatedDate
의 기능을 직접 작성해보았습니다. @PrePersist
와 같은 콜백 어노테이션에 대해 자세히 아시고 싶은 분들은 Hibernate 문서를 참고하시기 바랍니다. 한가지 의문점은 앞서 실해한 코드에서 @PrePersist
가 실행되기 전에 select
가 실행되어 객체를 가져온 것으로 보이는데 이 부분에 대해서는 다음에 알아봐야 될 것 같습니다. 예상하기로는 @PrePersist
는 영속성 컨텍스트에 있을 때만 관리할 수 있는 것 같습니다. persist
이전에는 영속성 컨텍스트에서 관리되는 객체가 없었기 때문에 select
로 DB에서 Person 객체를 받아와 사용한 것으로 보입니다. 혹시 아시는 분 계시면 답변 남겨주시면 감사하겠습니다😄.