JPA EntityListener를 알아보자

이윤설·2024년 6월 22일

개요

JPA Entity Listeners는 JPA 엔티티의 생명주기 이벤트에 대한 콜백을 제공하는 메커니즘이다.
이를 통해 데이터베이스 연산(삽입, 업데이트, 삭제 등)이 발생할 때 특정 로직을 자동으로 실행할 수 있다.

사용하는 이유

  • 우리가 보통 게시판에 글을 작성할 때, 작성시간 및 수정시간을 입력하는 경우는 없을 것이다. 즉, 이것은 자동으로 업데이트 되는데, JPA를 사용하면 EntityListener라는 것이 이 역할을 담당한다.
  • 일반적으로 Auditing(데이터의 변경 기록을 추적하고 데이터 변화에 대한 이력을 기록하는 것)하는 용도로 많이 사용된다. 참고로 SpringData JPA에서만 Auditing 기능을 제공한다.
  • user.setCreatedAt(), user.setUpdatedAt()과 같은 필드는 로직에 실수로 안넣을 확률이 높지만 중요한 데이터이다. 따라서 이러한 실수와 반복적인 코드를 줄이기 위해 사용하는 경우가 많다.

종류

  1. @PrePersist: 엔티티가 처음으로 데이터베이스에 저장되기 전에 호출된다.
  2. @PostPersist: 엔티티가 데이터베이스에 저장된 후에 호출된다.
  3. @PreUpdate: 엔티티가 데이터베이스에 업데이트되기 전에 호출된다.
  4. @PostUpdate: 엔티티가 데이터베이스에 업데이트된 후에 호출된다.
  5. @PreRemove: 엔티티가 데이터베이스에서 삭제되기 전에 호출된다.
  6. @PostRemove: 엔티티가 데이터베이스에서 삭제된 후에 호출된다.
  7. @PostLoad: 엔티티가 데이터베이스에서 로드된 후에 호출된다.

예시

  1. @PrePersist: 엔티티 생성 시간 설정
  2. @PostPersist: 신규 사용자 등록 후 환영 이메일 전송
  3. @PreUpdate: 게시글의 수정시간 업데이트
  4. @PostUpdate: 게시글 수정 후 변경 로그 작성
  5. @PreRemove: 삭제 전 연관된 데이터 처리
  6. @PostRemove: 삭제 후 로그 기록
  7. @PostLoad: 로드 후 데이터 초기화 또는 추가 처리

cf. Update Vs. Load

🐳 Update

  • 데이터베이스에 이미 존재하는 레코드의 데이터를 변경하는 작업이다.
  • 사용자가 게시글의 내용을 수정하고 저장할 때, 해당 게시글의 레코드가 업데이트된다.

🐳 Load

  • 데이터베이스에서 레코드를 읽어와서 엔티티 객체로 변환하는 작업이다.
  • 엔티티가 처음으로 데이터베이스에서 조회될 때, 즉 JPA가 데이터베이스에서 데이터를 읽어서 엔티티 객체로 변환될 때 수행된다.
  • ex. 사용자가 게시글을 보기 위해 웹 페이지를 요청할 때, 해당 게시글의 데이터가 데이터베이스에서 로드된다.

예시코드

실제로 나의 게시판 프로젝트에 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인지 파악할 수 있는걸까?

  1. 영속성 컨텍스트
    JPA는 엔티티 매니저를 통해 엔티티의 상태를 관리한다.
    엔티티 매니저는 영속성 컨텍스트라는 내부 저장소를 사용하여 엔티티의 상태를 추적한다. 이를 통해 JPA는 엔티티가 새로운 것인지, 이미 존재하는 것인지, 업데이트된 것인지, 삭제된 것인지 등을 결정할 수 있다.

  2. 엔티티 상태
    엔티티는 JPA에서 다음과 같은 상태를 갖는다.

  • Transient (비영속 상태): 새로운 엔티티로, 영속성 컨텍스트에 의해 관리되지 않는 상태
  • Persistent (영속 상태): 영속성 컨텍스트에 의해 관리되는 상태다. 데이터베이스에 저장될 준비가 되어 있는 상태
  • Detached (준영속 상태): 한때 영속 상태였지만 현재는 영속성 컨텍스트에 의해 관리되지 않는 상태
  • Removed (삭제 상태): 삭제가 예약된 상태
  1. 주요 메서드와 상태 변화
  • 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 상태가 된다.
    이는 엔티티를 데이터베이스에서 로드하는 경우이다.

  1. EntityListener 작동
    a. 상태변화 감지:
    엔티티 매니저는 엔티티의 상태 변화를 감지한다. 엔티티의 상태는 persist(), merge(), remove(), find() 등의 메서드 호출을 기반으로 상태가 변화한다.
    b. 이벤트 트리거:
    엔티티의 상태가 변화하면, JPA는 해당 상태 변화에 맞는 생명주기 이벤트를 트리거한다. 예를 들어, 새로운 엔티티가 persist() 메서드를 통해 영속성 컨텍스트에 추가되면 @PrePersist 이벤트가 트리거된다.
    c. 콜백 메서드 호출:
    트리거된 이벤트에 맞는 콜백 메서드가 호출된다. 예를 들어, @PrePersist 이벤트가 트리거되면, 해당 엔티티 리스너의 prePersist 메서드(ex. 게시글 createdAt 등록)가 호출된다.

SpringData JPA의 EntityListener

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

profile
화려한 외면이 아닌 단단한 내면

0개의 댓글