안녕세계
[JPA] Entity Life Cycle 본문
[JPA] Entity Life Cycle
Junhong Kim 2024. 8. 6. 22:36JPA 엔티티의 생명주기
JPA를 제대로 활용하려면, 단순히 save()나 delete()를 호출하는 걸 넘어서 엔티티가 어떤 과정을 거쳐 상태가 변경되고, 언제 DB에 반영되는지를 이해할 필요가 있습니다. 이를 이해하는 데 핵심이 되는 개념이 바로 JPA 엔티티의 생명주기(Entity Life Cycle)입니다. 엔티티 객체는 “생성된 상태”에서 시작해, “영속성 컨텍스트에 저장되고”, “변경되며”, “삭제되는” 흐름을 하나의 생애 주기로 관리 됩니다. 이 흐름을 제대로 파악하여 불필요한 쿼리를 방지하고, 의도하지 않은 오류를 막을 수 있습니다.
엔티티 상태
Transient(비영속 상태)
Transient 상태는 엔티티가 생성만 된 상태로, 데이터베이스와 아무런 관계가 없는 상태입니다. 즉, new 키워드로 객체를 생성한 직후이며 아직 영속성 컨텍스트와 연결되지 않았습니다. 이 상태의 객체는 JPA에서 관리하지 않으므로 데이터베이스에 저장되지 않습니다.
// Transient 상태
User user = new User();
user.setName("kim");
Persistent(영속 상태, Managed)
Persistent 상태는 엔티티가 영속성 컨텍스트에 등록된 상태입니다. EntityManager.persist() 또는 Spring Data JPA의 repository.save() 메서드를 호출하거나, 조회 메서드(em.find() 등)를 통해 가져온 엔티티가 이에 해당됩니다. 영속 상태의 엔티티는 JPA가 변경 사항을 추적하고, 트랜잭션 커밋 시점에 자동으로 데이터베이스와 동기화합니다.
// Persistent 상태로 전환
em.persist(user);
// 이 상태에서는 변경 사항이 자동으로 추적됩니다.
user.setName("lee");
// 별도의 save 호출 없이 트랜잭션 커밋 시 자동으로 데이터베이스에 반영됩니다.
Detached(준영속 상태)
Detached 상태는 한때 영속 상태였지만, 영속성 컨텍스트와의 연결이 끊어진 상태입니다. 보통 트랜잭션이 종료되거나, em.detach() 또는 em.clear()가 호출되었을 때 발생합니다.
// 영속 상태였던 객체를 준영속 상태로 변경
em.detach(user);
user.setName("강감찬");
// 준영속 상태에서는 변경 사항이 더 이상 자동으로 반영되지 않습니다.
준영속 상태에서 다시 영속 상태로 변경하려면 em.merge(entity)를 사용하면 됩니다.
em.merge(user); // 다시 Persistent 상태로 복귀
Removed(삭제 예약 상태)
Removed 상태는 엔티티가 삭제 예약된 상태로, 실제 데이터베이스에서는 트랜잭션이 커밋될 때 삭제됩니다. 트랜잭션이 롤백되면 삭제 예약은 취소되어 다시 Persistent 상태로 되돌아갑니다.
// 삭제 예약 상태로 전환
em.remove(user);
// 트랜잭션 커밋 시점에 실제로 DB에서 삭제됩니다.
Life Cycle Callback
JPA에서는 엔티티 상태가 변경될 때 자동으로 실행되는 메서드를 지정할 수 있도록 어노테이션을 제공합니다.
이벤트 시점 | 어노테이션 | 설명 |
저장 전 | @Prepersist | persist() 직전 호출 |
저장 후 | @PostPersist | INSERT 직후 호출 *참고 @PostPersist는 엔티티 매니저가 실제로 INSERT SQL을 데이터베이스에 내보내는 flush 직후에 호출됩니다. 이 시점은 아직 트랜잭션이 확정(commit)되기 전일 수 있으며, 이후에 롤백이 발생하면 콜백은 이미 실행된 상태가 됩니다. 커밋 이후 실행하고 싶다면 TransactionalEventListener를 사용해야합니다. |
삭제 전 | @PreRemove | removed() 직전 호출 |
삭제 후 | @PostRemoved | DELETE 직후 호출 |
수정 전 | @PreUpdate | 변경감지 후 UPDATE 직전 호출 |
수정 후 | @PostUpdate | UPDATE 직후 호출 |
조회 후 | @PostLoad | 엔티티 로드 직후 호출 |
위 어노테이션을 엔티티 메서드에 직접 붙이거나, @EntityListners로 분리된 리스너 클래스에 정의 가능합니다.
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String username;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// 영속 컨텍스트에 처음 저장되기 직전
@PrePersist
protected void onPrePersist() {
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;
}
// 영속 상태 엔티티가 변경 감지된 후, 업데이트 직전
@PreUpdate
protected void onPreUpdate() {
this.updatedAt = LocalDateTime.now();
}
// getters / setters...
}
엔티티 내부에 메서드를 정의하면 해당 클래스에 한정된 로직을 관리할 수 있습니다.
// 1) Listener 클래스
public class AuditListener {
@PrePersist
public void setCreatedOn(Object target) {
if (target instanceof Auditable) {
Auditable aud = (Auditable) target;
LocalDateTime now = LocalDateTime.now();
aud.setCreatedAt(now);
aud.setUpdatedAt(now);
}
}
@PreUpdate
public void setUpdatedOn(Object target) {
if (target instanceof Auditable) {
((Auditable) target).setUpdatedAt(LocalDateTime.now());
}
}
}
// 2) Auditable 인터페이스 (공통 필드와 메서드 정의)
public interface Auditable {
void setCreatedAt(LocalDateTime createdAt);
void setUpdatedAt(LocalDateTime updatedAt);
}
// 3) 엔티티 클래스
@Entity
@EntityListeners(AuditListener.class)
public class Order implements Auditable {
@Id @GeneratedValue
private Long id;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@Override public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
@Override public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
// getters / setters...
}
- @EntityListeners(AuditListener.class)를 붙이면 Order뿐 아니라 Auditable을 구현한 다른 엔티티에도 동일한 리스너를 재사용할 수 있습니다.
- 로직을 분리해두면 여러 엔티티에 걸친 공통 콜백을 일관성 있게 관리할 수 있습니다.
'Server > JPA' 카테고리의 다른 글
[JPA] JPA 사용시 유의사항 (0) | 2025.04.19 |
---|---|
[JPA] Soft Delete 적용하기 : @Where vs @SQLRestriction (1) | 2025.03.15 |
[JPA] 성능 최적화 (0) | 2023.05.06 |