본문 바로가기
Programming/Spring

스프링부트 @CreatedDate 직접 만들어 보기

by peter paak 2021. 1. 26.
728x90

오늘은 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이라는 객체에 현재 날짜를 세팅해줍니다.
  • 주의할 점
    1. 메소드는 void를 리턴해야 합니다.
    2. @EntityListener에 정의된 경우, @EntityListener가 정의된 객체가 파라미터가 되어야 합니다. (여기서는 Person)
    3. 해당 콜백 메소드는 EntityManagerQuery를 호출하지 않습니다.
    4. 콜백 메소드는 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);
    }
}

실행하면 결과는 다음과 같습니다.

순서를 보면 먼저

  1. Person 객체를 생성하고
  2. 생성된 객체에 createAt을 세팅한 후
  3. 마지막으로 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 객체를 받아와 사용한 것으로 보입니다. 혹시 아시는 분 계시면 답변 남겨주시면 감사하겠습니다😄.

728x90