JPA Entity Listeners는 JPA 엔티티의 생명주기 이벤트에 대한 콜백을 제공하는 메커니즘이다.
이를 통해 데이터베이스 연산(삽입, 업데이트, 삭제 등)이 발생할 때 특정 로직을 자동으로 실행할 수 있다.
🐳 Update
🐳 Load
실제로 나의 게시판 프로젝트에 EntityListener를 적용해보았다.
게시글의 생성시간 및 수정시간 등록 및 수정 로직을
EntityListener를 통해 자동화 시켰는데, 코드를 살펴보자.
@Getter
@Setter
@NoArgsConstructor
@Entity
@EntityListeners(PostEntityListener.class)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
private String title;
@Lob
private String content;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;
private int views;
private int likes;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "board_id")
private Board board;
private boolean deleted = false;
@Builder
public Post(String title, String content, User user, int views, int likes, Board board) {
this.title = title;
this.content = content;
this.user = user;
this.views = views;
this.likes = likes;
this.board = board;
}
}
public class PostEntityListener {
@PrePersist
public void prePersist(Post post) {
LocalDateTime now = LocalDateTime.now();
post.setCreatedDate(now);
post.setModifiedDate(now);
}
@PreUpdate
public void preUpdate(Post post) {
post.setModifiedDate(LocalDateTime.now());
}
}
이제 테스트코드를 통해 잘 작동하는지 알아보자.
@SpringBootTest
public class PostEntityListenerTest {
@PersistenceContext
private EntityManager entityManager;
@Test
@Transactional
public void testPrePersist() {
// Given
UserStatus userStatus = UserStatus.createUserStatus(
true,
0,
LocalDateTime.now().minusDays(1),
LocalDateTime.now().minusHours(1),
LocalDateTime.now().minusMonths(1),
null,
false
);
User user = User.createUser(
"testuser@example.com",
"password",
LocalDate.of(1990, 1, 1),
"USER",
"testuser",
userStatus
);
userStatus.setUser(user);
Board board = Board.builder()
.name("General")
.description("General Discussion")
.status(BoardStatus.ACTIVE)
.build();
entityManager.persist(user);
entityManager.persist(board);
Post post = Post.builder()
.title("Test Title")
.content("Test Content")
.user(user)
.views(0)
.likes(0)
.board(board)
.build();
LocalDateTime beforePersist = LocalDateTime.now();
// When
entityManager.persist(post);
entityManager.flush();
// Then
assertNotNull(post.getCreatedDate());
assertNotNull(post.getModifiedDate());
assertTrue(post.getCreatedDate().isAfter(beforePersist) || post.getCreatedDate().isEqual(beforePersist));
assertTrue(post.getModifiedDate().isAfter(beforePersist) || post.getModifiedDate().isEqual(beforePersist));
}
@Test
@Transactional
public void testPreUpdate() {
// Given
UserStatus userStatus = UserStatus.createUserStatus(
true,
0,
LocalDateTime.now().minusDays(1),
LocalDateTime.now().minusHours(1),
LocalDateTime.now().minusMonths(1),
null,
false
);
User user = User.createUser(
"testuser@example.com",
"password",
LocalDate.of(1990, 1, 1),
"USER",
"testuser",
userStatus
);
userStatus.setUser(user);
Board board = Board.builder()
.name("General")
.description("General Discussion")
.status(BoardStatus.ACTIVE)
.build();
entityManager.persist(user);
entityManager.persist(board);
Post post = Post.builder()
.title("Original Title")
.content("Original Content")
.user(user)
.views(0)
.likes(0)
.board(board)
.build();
entityManager.persist(post);
entityManager.flush();
LocalDateTime createdDate = post.getCreatedDate();
// When
post.setTitle("Updated Title");
post.setContent("Updated Content");
LocalDateTime beforeUpdate = LocalDateTime.now();
entityManager.merge(post);
entityManager.flush();
// Then
assertNotNull(post.getModifiedDate());
assertTrue(post.getModifiedDate().isAfter(createdDate));
assertTrue(post.getModifiedDate().isAfter(beforeUpdate) || post.getModifiedDate().isEqual(beforeUpdate));
}
}

성공적으로 테스트를 통과했다.
따라서 실제로 EntityListener를 통해 자동으로 생성시간과 수정시간이 등록 및 업데이트 됨을 확인할 수 있다.
그런데 어떤 원리로 EntityListener가 자동으로 해당 데이터가 Persist인지, Update인지, Remove인지, Load인지 파악할 수 있는걸까?
영속성 컨텍스트
JPA는 엔티티 매니저를 통해 엔티티의 상태를 관리한다.
엔티티 매니저는 영속성 컨텍스트라는 내부 저장소를 사용하여 엔티티의 상태를 추적한다. 이를 통해 JPA는 엔티티가 새로운 것인지, 이미 존재하는 것인지, 업데이트된 것인지, 삭제된 것인지 등을 결정할 수 있다.
엔티티 상태
엔티티는 JPA에서 다음과 같은 상태를 갖는다.

persist(Object entity):
엔티티를 영속성 컨텍스트에 저장한다.
엔티티가 Transient 상태에서 Persistent 상태로 변경된다.
이는 새로운 엔티티를 데이터베이스에 저장하는 경우다.
merge(Object entity):
분리된(Detached) 엔티티의 상태를 영속성 컨텍스트에 병합한다.
영속성 컨텍스트에 동일한 식별자를 가진 엔티티가 이미 존재하면, 그 엔티티의 상태를 업데이트한다.
엔티티가 Detached 상태에서 Persistent 상태로 변경된다.
이는 기존 엔티티를 업데이트하는 경우다.
cf. merge가 호출되어 트랜잭션이 시작되면 상태가 Persistent로 변하고, 트랜잭션이 종료(업데이트 완료)되면 다시 detached 된다.
remove(Object entity):
엔티티를 영속성 컨텍스트에서 제거한다.
엔티티가 Persistent 상태에서 Removed 상태로 변경된다.
이는 엔티티를 데이터베이스에서 삭제하는 경우다.
find(Class entityClass, Object primaryKey):
데이터베이스에서 식별자(primary key)에 의해 엔티티를 조회한다.
조회된 엔티티는 영속성 컨텍스트에 의해 관리되는 Persistent 상태가 된다.
이는 엔티티를 데이터베이스에서 로드하는 경우이다.
Spring Data JPA는 Auditing 기능을 제공하여 엔티티가 생성되거나 수정될 때 자동으로 특정 필드를 설정할 수 있도록 지원한다. 이를 위해 @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy와 같은 어노테이션을 사용할 수 있다.
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime modifiedDate;
이처럼 간단하게 구현할 수 있지만, 기본적인 원리를 알면
더욱 좋으니 알고 사용하도록 하자.
이미지 출처: Javatpoint.com
참고: https://milenote.tistory.com/79