π§ JPA @EntityListeners μλ²½ κ°μ΄λ
Entity μλͺ μ£ΌκΈ°λ₯Ό κ°μνλ λλν 리μ€λ! λ°μ΄ν° μ μ₯ λꡬλ₯Ό λλ©μΈ μ΄λ²€νΈ μμ€ν μΌλ‘ μ κ·Έλ μ΄λνμΈμ.
π λͺ©μ°¨
- @EntityListenersκ° λκ°μ?
- κ°μ§ κ°λ₯ν 7κ°μ§ μλͺ μ£ΌκΈ° μ΄λ²€νΈ
- μΈμ μ¬μ©ν΄μΌ ν κΉ?
- Entity λ΄λΆ vs μΈλΆ Listener
- μ€λ¬΄ νμ€ ν¨ν΄
- λ°λμ μμμΌ ν μ£Όμμ¬ν
- @EntityListeners vs Spring Event
- ν΅μ¬ μμ½
1οΈβ£ @EntityListenersκ° λκ°μ?
@EntityListenersλ JPA Entityμ μλͺ μ£ΌκΈ° μ΄λ²€νΈλ₯Ό κ°λ‘μ±μ νΉμ λ‘μ§μ μλμΌλ‘ μ€ννκ² ν΄μ£Όλ κ°λ ₯ν μ΄λ Έν μ΄μ μ λλ€.
π― ν λ¬Έμ₯ μ μ
βμ΄ Entityκ° μ μ₯Β·μμ Β·μμ λ λ, μ΄ ν΄λμ€μ λ©μλλ₯Ό μλμΌλ‘ νΈμΆν΄μ€!β
π‘ κΈ°λ³Έ νν
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Member {
// μ΄μ μ΄ Entityλ κ°μ λμ! π
}
π λμ μ리
Entity λ³κ²½ β JPAκ° κ°μ§ β Listener νΈμΆ β μλ μ€ν
μμ:
-
member.save()νΈμΆ - JPA: βμ κΉ! Memberκ° μ μ₯λλ €κ³ ν΄!β
- Listener: βμ μ₯ μ μμ μ€ν!β
- μ€μ INSERT 쿼리 μ€ν
2οΈβ£ κ°μ§ κ°λ₯ν 7κ°μ§ μλͺ μ£ΌκΈ° μ΄λ²€νΈ
JPAλ Entityμ μ 체 μλͺ μ£ΌκΈ°λ₯Ό 7κ° μμ μΌλ‘ λλ μ κ°μν©λλ€.
| μ΄λ²€νΈ | μ΄λ Έν μ΄μ | μ€ν μμ | μ£Όμ μ©λ |
|---|---|---|---|
| πΎ μ μ₯ μ | @PrePersist |
INSERT μ | μμ±μΌ μ€μ , μ΄κΈ°κ° μΈν |
| β μ μ₯ ν | @PostPersist |
INSERT ν | λ‘κ·Έ κΈ°λ‘, μ΄λ²€νΈ λ°ν |
| βοΈ μμ μ | @PreUpdate |
UPDATE μ | μμ μΌ κ°±μ , κ²μ¦ |
| β μμ ν | @PostUpdate |
UPDATE ν | λ³κ²½ μ΄λ ₯ μ μ₯ |
| ποΈ μμ μ | @PreRemove |
DELETE μ | μ°κ΄ λ°μ΄ν° μ 리 |
| β μμ ν | @PostRemove |
DELETE ν | μΊμ μμ , λ‘κ·Έ |
| π λ‘λ© ν | @PostLoad |
SELECT ν | λ°μ΄ν° νμ²λ¦¬ |
π μλͺ μ£ΌκΈ° νμλΌμΈ
CREATE: @PrePersist β [INSERT] β @PostPersist
UPDATE: @PreUpdate β [UPDATE] β @PostUpdate
DELETE: @PreRemove β [DELETE] β @PostRemove
READ: [SELECT] β @PostLoad
3οΈβ£ μΈμ μ¬μ©ν΄μΌ ν κΉ?
β Case 1: μμ±μΌ/μμ μΌ μλ κ΄λ¦¬ β (κ°μ₯ λνμ !)
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Post {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
μ₯μ :
- β μλμΌλ‘ μκ° κΈ°λ‘
- β κ°λ°μκ° μ κ²½ μΈ νμ μμ
- β ν΄λ¨Ό μλ¬ λ°©μ§
π‘ νμ μ€μ :
@EnableJpaAuditingμ μ€μ ν΄λμ€μ μΆκ°ν΄μΌ μλ!
β Case 2: κ³΅ν΅ λ‘μ§ λΆλ¦¬
public class ValidationListener {
@PrePersist
@PreUpdate
public void validate(Object entity) {
if (entity instanceof Validatable) {
((Validatable) entity).validate();
}
}
}
μ μ© λμ:
- π λ‘κ·Έ κΈ°λ‘
- π κ°μ¬(Audit) μΆμ
- β μν κ²μ¦
- π ν΅κ³ μμ§
β Case 3: λ°μ΄ν° 보μ /μ κ·ν
public class DataNormalizationListener {
@PrePersist
@PreUpdate
public void normalize(Object entity) {
if (entity instanceof Member) {
Member member = (Member) entity;
// μ΄λ©μΌ μλ¬Έμ λ³ν
member.normalizeEmail();
// μ νλ²νΈ νμ ν΅μΌ
member.normalizePhoneNumber();
}
}
}
νμ© μμ:
- π§ μ΄λ©μΌ μλ¬Έμ λ³ν
- π± μ νλ²νΈ ν¬λ§·ν
- βοΈ λ¬Έμμ΄ trim
- π€ λμλ¬Έμ μ κ·ν
β Case 4: Entity μ± μ λΆλ¦¬
β Before: Entityμ λͺ¨λ λ‘μ§
@Entity
public class Member {
private LocalDateTime updatedAt;
public void update(String name) {
this.name = name;
this.updatedAt = LocalDateTime.now(); // π° λ§€λ² μ§μ μ€μ
}
}
β After: Listenerλ‘ λΆλ¦¬
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Member {
@LastModifiedDate
private LocalDateTime updatedAt; // π μλμΌλ‘ μ€μ λ¨!
public void update(String name) {
this.name = name; // λΉμ¦λμ€ λ‘μ§μλ§ μ§μ€
}
}
μ΄μ :
- π― κ΄μ¬μ¬ λΆλ¦¬ (Separation of Concerns)
- π§ͺ ν μ€νΈ μ©μ΄μ± ν₯μ
- π μ½λ κ°λ μ± κ°μ
4οΈβ£ Entity λ΄λΆ vs μΈλΆ Listener
πΉ λ°©μ 1: Entity λ΄λΆμ μ§μ μμ±
@Entity
public class Member {
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
public void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
public void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
| μ₯μ β | λ¨μ β |
|---|---|
| κ°λ¨νκ³ μ§κ΄μ | Entity μ± μ μ¦κ° |
| λ³λ ν΄λμ€ λΆνμ | μ¬μ¬μ© λΆκ°λ₯ |
| λΉ λ₯Έ ꡬν | ν μ€νΈ λ³΅μ‘ |
πΉ λ°©μ 2: μΈλΆ Listener ν΄λμ€ (β¨ κΆμ₯)
// Listener ν΄λμ€
public class AuditListener {
@PrePersist
public void prePersist(Object entity) {
System.out.println("μ μ₯ μ§μ : " + entity);
}
@PreUpdate
public void preUpdate(Object entity) {
System.out.println("μμ μ§μ : " + entity);
}
}
// Entity
@Entity
@EntityListeners(AuditListener.class)
public class Member {
// κΉλ! π¨
}
| μ₯μ β | λ¨μ β |
|---|---|
| κ΄μ¬μ¬ μλ²½ λΆλ¦¬ | ν΄λμ€ μΆκ° νμ |
| μ¬λ¬ Entityμμ μ¬μ¬μ© | μ΄κΈ° μ€μ νμ |
| ν μ€νΈ λ 립μ | - |
| μ μ§λ³΄μ μ¬μ | - |
π μ ν κ°μ΄λ
κ°λ¨ν λ‘μ§ (1-2μ€) β Entity λ΄λΆ λ©μλ
볡μ‘ν λ‘μ§ / μ¬μ¬μ© β μΈλΆ Listener ν΄λμ€ β
5οΈβ£ μ€λ¬΄ νμ€ ν¨ν΄
π BaseTimeEntity ν¨ν΄ (μΆμ ν΄λμ€ νμ©)
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
π― μ€μ Entityμμ μμ
@Entity
public class Post extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
// createdAt, updatedAtμ μλ κ΄λ¦¬λ¨! π
}
@Entity
public class Comment extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
// createdAt, updatedAtμ μλ κ΄λ¦¬λ¨! π
}
β¨ κ³ κΈ ν¨ν΄: μμ±μ/μμ μ μΆμ
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
π§ AuditorAware μ€μ (μμ±μ/μμ μ μλ μ£Όμ )
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> {
// SecurityContextμμ νμ¬ μ¬μ©μ κ°μ Έμ€κΈ°
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.of("system");
}
return Optional.of(authentication.getName());
};
}
}
6οΈβ£ λ°λμ μμμΌ ν μ£Όμμ¬ν
β οΈ μ£Όμ 1: λΉμ¦λμ€ λ‘μ§ λ£μ§ λ§ κ²! (λ§€μ° μ€μ)
β μλͺ»λ μ¬μ©
public class PaymentListener {
@PostPersist
public void processPayment(Order order) {
// κ²°μ μ²λ¦¬ β
paymentService.charge(order);
// μλ¦Ό λ°μ‘ β
notificationService.sendEmail(order);
// μΈλΆ API νΈμΆ β
externalApi.syncOrder(order);
}
}
λ¬Έμ μ :
- π₯ νΈλμμ κ²½κ³κ° λΆλͺ ν
- π₯ μ€ν¨ μ λ‘€λ°± μ²λ¦¬ 볡μ‘
- π₯ μμ μμΈ‘ λΆκ°λ₯
β μ¬λ°λ₯Έ μ¬μ©
public class AuditListener {
@PrePersist
public void setCreatedDate(Object entity) {
// λ¨μν κ° μ€μ λ§ β
if (entity instanceof BaseTimeEntity) {
((BaseTimeEntity) entity).setCreatedAt(LocalDateTime.now());
}
}
}
β οΈ μ£Όμ 2: Repository/Service μ£Όμ λΆκ° (κΈ°λ³Έ)
β μλνμ§ μμ
public class BadListener {
@Autowired // β μ£Όμ
μ λ¨!
private MemberRepository memberRepository;
@PrePersist
public void doSomething(Object entity) {
memberRepository.save(...); // NullPointerException!
}
}
μ΄μ : Listenerλ κΈ°λ³Έμ μΌλ‘ Spring Beanμ΄ μλ!
β ν΄κ²° λ°©λ² 1: Spring BeanμΌλ‘ λ±λ‘
@Component
public class SpringBeanListener {
@Autowired
private MemberRepository memberRepository;
@PrePersist
public void doSomething(Object entity) {
// μ΄μ μλν¨!
}
}
β ν΄κ²° λ°©λ² 2: ApplicationContext νμ©
public class SmartListener {
@PrePersist
public void doSomething(Object entity) {
ApplicationContext context =
BeanUtil.getApplicationContext();
MemberRepository repo =
context.getBean(MemberRepository.class);
// μ¬μ©
}
}
π― λ μ’μ λ°©λ²: Spring Event μ¬μ©
// Listenerλ λ¨μνκ²
@PostPersist
public void onCreated(Member member) {
ApplicationEventPublisher publisher =
BeanUtil.getBean(ApplicationEventPublisher.class);
publisher.publishEvent(new MemberCreatedEvent(member));
}
// λ³λ EventHandlerμμ μ²λ¦¬
@EventListener
public void handleMemberCreated(MemberCreatedEvent event) {
// μ¬κΈ°μ Repository, Service μμ λ‘κ² μ¬μ©
notificationService.sendWelcomeEmail(event.getMember());
}
β οΈ μ£Όμ 3: μμΈ λ°μ μ μ 체 νΈλμμ μ€ν¨
@PrePersist
public void validate(Member member) {
if (member.getAge() < 0) {
throw new IllegalArgumentException("λμ΄λ μμμΌ μ μμ΅λλ€");
// β μ 체 INSERT μ·¨μ! π¨
}
}
κ²°κ³Ό:
- μμΈ λ°μ β DB μ μ₯ μ€ν¨
- νΈλμμ λ‘€λ°±
- λ°μ΄ν° μΌκ΄μ± μ μ§
π‘ Tip: κ²μ¦μ κ°κΈμ Service λ μ΄μ΄μμ!
β οΈ μ£Όμ 4: Cascade μ°μ°κ³Όμ μνΈμμ©
@Entity
@EntityListeners(LoggingListener.class)
public class Order {
@OneToMany(cascade = CascadeType.ALL)
private List<OrderItem> items;
}
@Entity
@EntityListeners(LoggingListener.class)
public class OrderItem {
}
μ£Όμ: Order μ μ₯ μ OrderItemμ Listenerλ λͺ¨λ μ€νλ¨!
7οΈβ£ @EntityListeners vs Spring Event
μΈμ 무μμ μ¬μ©ν΄μΌ ν κΉμ?
| λΉκ΅ νλͺ© | @EntityListeners π§ | Spring Event π’ |
|---|---|---|
| νΈμΆ μμ | DB νΈλμμ μ /ν | λΉμ¦λμ€ λ‘μ§ νλ¦ |
| μ£Όμ μ©λ | κ°μ¬, λ°μ΄ν° 보μ | μλ¦Ό, μΈλΆ μ°λ |
| μμ‘΄μ± μ£Όμ | 볡μ‘ν¨ (κΈ°λ³Έ λΆκ°) | μμ λ‘μ |
| μΈλΆ μμ€ν | β λΉμΆμ² | β μΆμ² |
| νΈλμμ | DB νΈλμμ λ΄λΆ | λ³λ μ μ΄ κ°λ₯ |
| μ€ν μμ | JPAκ° κ²°μ | λͺ μμ μ μ΄ |
| μ±λ₯ | λΉ λ¦ | μλμ μΌλ‘ λλ¦Ό |
π― μ ν κ°μ΄λ
// EntityListeners μ¬μ© β
- μμ±μΌ/μμ μΌ μλ κΈ°λ‘
- λ°μ΄ν° μ κ·ν (trim, μλ¬Έμ λ±)
- κ°λ¨ν κ°μ¬ λ‘κ·Έ
// Spring Event μ¬μ© β
- μ΄λ©μΌ/νΈμ μλ¦Ό λ°μ‘
- μΈλΆ API μ°λ
- 볡μ‘ν λΉμ¦λμ€ λ‘μ§
- μ¬λ¬ μλΉμ€ νΈμΆ
π‘ ν¨κ» μ¬μ©νκΈ°
// 1λ¨κ³: EntityListenerμμ μ΄λ²€νΈ λ°ν
@Component
public class MemberEventListener {
@Autowired
private ApplicationEventPublisher eventPublisher;
@PostPersist
public void onMemberCreated(Member member) {
eventPublisher.publishEvent(new MemberCreatedEvent(member));
}
}
// 2λ¨κ³: EventHandlerμμ λΉμ¦λμ€ λ‘μ§ μ²λ¦¬
@Component
public class MemberEventHandler {
@Autowired
private EmailService emailService;
@EventListener
@Async
public void handleMemberCreated(MemberCreatedEvent event) {
emailService.sendWelcomeEmail(event.getMember());
}
}
8οΈβ£ ν΅μ¬ μμ½
π‘ ν λ¬Έμ₯ μ 리
@EntityListenersλ Entity μλͺ μ£ΌκΈ°μ μλμΌλ‘ λ°μνλ νν¬(Hook)λ€.
π κΈ°μ΅ κ³΅μ
β
μμ±μΌ/μμ μΌ μλ κΈ°λ‘ = @EntityListeners + Auditing
β
λ°μ΄ν° 보μ /κ²μ¦ = @EntityListeners
β λΉμ¦λμ€ λ‘μ§ = Spring Event μ¬μ©
β μΈλΆ API νΈμΆ = Spring Event μ¬μ©
π μ€λ¬΄ 체ν¬λ¦¬μ€νΈ
κΈ°λ³Έ μ€μ
-
@EnableJpaAuditingμ€μ μλ£ -
BaseTimeEntityμΆμ ν΄λμ€ μμ± -
λͺ¨λ Entityκ°
BaseTimeEntityμμ
κ³ κΈ μ€μ
-
AuditorAwareꡬν (μμ±μ/μμ μ μΆμ ) - 컀μ€ν Listener λΆλ¦¬
- Spring Eventμ μν λΆλ΄
μ£Όμμ¬ν
- Listenerμ λΉμ¦λμ€ λ‘μ§ μμ
- μΈλΆ API νΈμΆ μμ
- DIκ° νμνλ©΄ Spring BeanμΌλ‘ λ±λ‘
- μμΈ μ²λ¦¬ μ λ΅ μ립
π μ€μ μμ : μλ²½ν ꡬμ±
1οΈβ£ κΈ°λ³Έ Entity
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
2οΈβ£ μ€μ ν΄λμ€
@Configuration
@EnableJpaAuditing
public class JpaConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(
SecurityContextHolder.getContext()
.getAuthentication()
.getName()
);
}
}
3οΈβ£ μ€μ Entity
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@Enumerated(EnumType.STRING)
private PostStatus status;
public static Post create(String title, String content) {
Post post = new Post();
post.title = title;
post.content = content;
post.status = PostStatus.DRAFT;
return post;
// createdAt, updatedAt, createdBy, lastModifiedByλ μλ μ€μ ! π
}
public void publish() {
this.status = PostStatus.PUBLISHED;
// updatedAt, lastModifiedByλ μλ κ°±μ ! π
}
}
π― λ€μ νμ΅ μ£Όμ
- Spring Event μ¬ν (
@EventListener,@Async) - JPA Auditing κ³ κΈ μ€μ
-
@Embeddedμ@Embeddable - Soft Delete ν¨ν΄ ꡬν
- λ³κ²½ μ΄λ ₯ μΆμ μμ€ν ꡬμΆ
π μ°Έκ³ μλ£
곡μ λ¬Έμ
κ΄λ ¨ μ΄λ Έν μ΄μ
-
@MappedSuperclass- κ³΅ν΅ λ§€ν μ 보 μμ -
@EnableJpaAuditing- Auditing κΈ°λ₯ νμ±ν -
@CreatedDate,@LastModifiedDate- μκ° μλ κ΄λ¦¬ -
@CreatedBy,@LastModifiedBy- μ¬μ©μ μλ κ΄λ¦¬