πŸ“š μ‹œλ¦¬μ¦ˆ/- λ°°μ›Œλ³΄μž Spring Data JPA

[λ°°μ›Œλ³΄μž Spring Data JPA] JPA Auditing κΈ°λŠ₯을 μ‚¬μš©ν•΄μ„œ 생성, μˆ˜μ • 일자 μžλ™ν™”ν•˜κΈ°

Wonit 2021. 4. 11. 18:46

ν•΄λ‹Ή 글은 λ°°μ›Œλ³΄μž Spring Data JPA μ‹œλ¦¬μ¦ˆ μž…λ‹ˆλ‹€.
ν•΄λ‹Ή μ‹œλ¦¬μ¦ˆμ˜ λ‚΄μš©μ΄ μ΄μ–΄μ§€λŠ” ν˜•νƒœμ΄λ―€λ‘œ κΈ€μ˜ λ‚΄μš© 쀑에 μƒλž΅λ˜λŠ” 말듀이 μžˆμ„ 수 μžˆμœΌλ‹ˆ, μžμ„Έν•œ 사항은 μ•„λž˜ 링크λ₯Ό μ°Έκ³ ν•΄μ£Όμ„Έμš”!


μ„œλΉ„μŠ€λ₯Ό μš΄μ˜ν•  λ•Œ μ‚¬μš©μžμ˜ 기본적인 둜그λ₯Ό DB에 남겨야 ν•  λ•Œκ°€ μžˆλ‹€.

 

이λ₯Όν…Œλ©΄ λ§ˆμ§€λ§‰ 둜그인 μ‹œκ°„μ΄λΌλ˜μ§€ μ—”ν‹°ν‹° 생성 μ‹œκ°„, λ³€κ²½λœ μ‹œκ°„κ³Ό λ³€κ²½ν•œ μ‚¬λžŒμ˜ 이름등등.

 

예λ₯Ό λ“€μ–΄λ³΄μž.

public class User {
  private Long id;
  private String name;
  private String address;
}

public class OrderItem {
  private Long id;
  private User userId;
  private Item itemId;
}

public class Item {
  private Long id;
  private String name;
  private String description;
}

이와 같은 μ—”ν‹°ν‹°κ°€ μ‘΄μž¬ν•œλ‹€κ³  ν•΄λ³΄μž.

 

예λ₯Ό λ“€μ–΄ 각각의 생성 μ‹œκ°„μ„ μΆ”κ°€ν•΄μ•Ό ν•œλ‹€κ³  ν•΄λ³΄μž.

public class User {
  private Long id;
  private String name;
  private String address;
  private LocalDateTime createdAt;
  private String createdBy;
}

public class OrderItem {
  private Long id;
  private User userId;
  private Item itemId;
  private LocalDateTime createdAt;
  private String createdBy;
}

public class Item {
  private Long id;
  private String name;
  private String description;
  private LocalDateTime createdAt;
  private String createdBy;
}

이럴 경우 λͺ¨λ“  μ—”ν‹°ν‹°κ°€ LocalDateTime createdAt, String createdBy 에 λŒ€ν•œ 연산을 μˆ˜ν–‰ν•΄μ•Ό ν•œλ‹€.

 

근데 생각 ν•œ 번 ν•΄λ³΄μž. λͺ¨λ“  μ—”ν‹°ν‹°κ°€ λ™μΌν•œ 연산을 μˆ˜ν–‰ν•΄μ•Ό ν•œλ‹€λ©΄ 연산을 μ²˜λ¦¬ν•˜λŠ” κΈ°λ³Έ μ—”ν‹°ν‹°λ₯Ό λ§Œλ“€κ³  λͺ¨λ“  데이터 μš”μ²­μ‹œμ— μ—”ν‹°ν‹°κ°€ μ—…λ°μ΄νŠΈλœλ‹€λ©΄ λ˜μ§€ μ•Šμ„κΉŒ?

이 λ•Œ 즉, 생성일/μˆ˜μ •μΌ/μƒμ„±μžλ₯Ό μžλ™ν™”ν•  λ•Œ μ‚¬μš©ν•˜λŠ”κ²Œ λ°”λ‘œ JPA Auditing 이닀.

 

Spring Data JPA μ—μ„œμ˜ Auditing

Spring Data JPA μ—μ„œλŠ” μœ„μ˜ Auditing κΈ°λŠ₯을 μ œκ³΅ν•œλ‹€.

 

Auditing은 Spring Data JPA μ—μ„œλ§Œ μ‚¬μš©ν•  수 μžˆλŠ” κ°œλ…μ€ μ•„λ‹ˆλ‹€.

 

JPA μžμ²΄μ μœΌλ‘œλ„ Auditing κΈ°λŠ₯을 μ‚¬μš©ν•  수 μžˆμ§€λ§Œ Spring Data JPA μ—μ„œλŠ” 더 κΉ”λ”ν•˜κ³  μ‰½κ²Œ μ œκ³΅ν•œλ‹€.

 

ν˜„μž¬ μ‹œλ¦¬μ¦ˆμ˜ λͺ©μ μ€ Spring Data JPAλ₯Ό ν•™μŠ΅ν•˜λŠ” κ²ƒμ΄λ―€λ‘œ JPA μ—μ„œμ˜ Auditing 에 λŒ€ν•΄μ„œλŠ” μƒλž΅ν•˜κ² λ‹€.
λ§Œμ•½ κΆκΈˆν•˜λ‹€λ©΄ @PrePersist, @PostPersist, @PreUpdate, @PostUpdate ν‚€μ›Œλ“œλ‘œ μ°Ύμ•„λ³Ό 것을 μΆ”μ²œν•œλ‹€.

@EnableJpaAuditing μ‚¬μš©ν•˜κΈ°

Spring Data JPA μ—μ„œ JPA λ₯Ό μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” SpringBoot μ„€μ • ν΄λž˜μŠ€μ— @EnableJpaAuditing 을 μ μ–΄μ€˜μ•Όν•œλ‹€.

 

보톡 Springboot λ₯Ό μ‹€ν–‰μ‹œν‚€λŠ” 클래슀 상단에 많이 μ‚¬μš©ν•˜κ³ λŠ” ν•œλ‹€.

@EnableJpaAuditing
@SpringBootApplication
public class DatajpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(DatajpaApplication.class, args);
    }

}

그리고 μš°λ¦¬κ°€ Auditing을 ν•  ν•„λ“œλ₯Ό κ°–λŠ” κΈ°λ³Έ μ—”ν‹°ν‹°λ₯Ό μƒμ„±ν•˜μž.

 

처음 λ³΄λŠ” μ½”λ“œλ“€μ΄ λ‚˜μ˜€λ”λΌλ„ κ±±μ •ν•˜μ§€ 말자! ν•˜λ‚˜μ”© 같이 μ•Œμ•„λ³Ό κ²ƒμ΄λ‹ˆ

@EntityListeners(AuditingEntityListener.class) // 1
@MappedSuperClass // 2
@Getter // 3
public BaseEntity {
  @CreatedDate // 4
  @Column(updatable = false) // 5
  private LocalDateTime createdDate;
}

μ΄λ ‡κ²Œ μž‘μ„±ν•˜λ©΄ ν•΄λ‹Ή μ—”ν‹°ν‹° ν΄λž˜μŠ€κ°€ JPA μ΄λ²€νŠΈκ°€ λ°œμƒν•œλ‹€λ©΄ Auditing 을 μˆ˜ν–‰ν•˜μ—¬ 값을 μ—…λ°μ΄νŠΈ ν•œλ‹€.


자! 무슨 μƒˆλ‘œμš΄ μ–΄λ…Έν…Œμ΄μ…˜λ“€μ΄ μ‘΄μž¬ν•œλ‹€. ν•˜λ‚˜μ”© μ•Œμ•„λ³΄μž.

  1. @EntityListeners(AuditingEntityListener.class)
  2. @MappedSuperClass
  3. @Getter
  4. @CreatedDate
  5. @Column(updatable = false)

@EntityListeners

@EntityListeners λŠ” μ—”ν‹°ν‹°λ₯Ό DB에 μ μš©ν•˜κΈ° μ „, 이후에 μ»€μŠ€ν…€ μ½œλ°±μ„ μš”μ²­ν•  수 μžˆλŠ” μ–΄λ…Έν…Œμ΄μ…˜μ΄λ‹€.

 

@EntityListeners 의 인자둜 μ»€μŠ€ν…€ μ½œλ°±μ„ μš”μ²­ν•  클래슀λ₯Ό μ§€μ •ν•΄μ£Όλ©΄ λ˜λŠ”λ°, Auditing 을 μˆ˜ν–‰ν•  λ•ŒλŠ” JPA μ—μ„œ μ œκ³΅ν•˜λŠ” AuditingEntityListener.class λ₯Ό 인자둜 λ„˜κΈ°λ©΄ λœλ‹€.

 

 

그럼 μœ„μ— λ³΄λŠ”λ°”μ™€ 같이@PrePersist μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ JPA 의 Auditing κΈ°λŠ₯을 Spring Data JPA κ°€ μ‚¬μš©ν•˜κ²Œ λ˜λŠ” 것이닀.

 

@MappedSuperClass

@MappedSuperClass 은 μ—”ν‹°ν‹°μ˜ 곡톡 λ§€ν•‘ 정보가 ν•„μš”ν•  λ•Œ 주둜 μ‚¬μš©ν•œλ‹€.

 

즉, λΆ€λͺ¨ 클래슀(μ—”ν‹°ν‹°)에 ν•„λ“œλ₯Ό μ„ μ–Έν•˜κ³  λ‹¨μˆœνžˆ μ†μ„±λ§Œ λ°›μ•„μ„œ μ‚¬μš©ν•˜κ³ μ‹Άμ„ λ•Œ μ‚¬μš©ν•˜λŠ” 방법이닀.

 

μš°λ¦¬λŠ” BaseEntityλ₯Ό μƒμ„±ν•˜κ³  Auditing κΈ°λŠ₯이 ν•„μš”ν•œ μ—”ν‹°ν‹° ν΄λž˜μŠ€μ—μ„œ μ‚¬μš©ν•  것이기 λ•Œλ¬Έμ— @MappedSuperClass μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜λŠ” 것이닀.

@CreatedDate

@CreatedDate μ–΄λ…Έν…Œμ΄μ…˜μ€ Spring Data JPA의 Auditing μ—μ„œ κ°€μž₯ ν₯미둜운 μ–΄λ…Έν…Œμ΄μ…˜μ΄λ‹€.

 

사싀 이 μ–΄λ…Έν…Œμ΄μ…˜λ„ Spring Data JPA 의 고유 κΈ°λŠ₯은 μ•„λ‹ˆκ³  Spring Data 에 μžˆλŠ” μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ Spring Data μ—μ„œ 좔상화 해놓은 것이닀.

CreatedDate의 javadoc 에 λ‚˜μ˜¨ μ„€λͺ…을 μ°Έκ³ ν•΄λ³΄μž.

 

Declares a field as the one representing the date the entity containing the field was created.
번) ν•„λ“œλ₯Ό ν¬ν•¨ν•˜λŠ” μ—”ν‹°ν‹°κ°€ μž‘μ„±λœ λ‚ μ§œλ₯Ό λ‚˜νƒ€λ‚΄λŠ” ν•„λ“œλΌκ³  μ„ μ–Έν•œλ‹€.

javadoc 에 λ‚˜μ˜¨ λ‚΄μš©μ„ 보면 μš°λ¦¬κ°€ ν•΄λ‹Ή ν•„λ“œλ₯Ό μ„ μ–Έν•˜λ©΄ μ—”ν‹°ν‹°κ°€ μž‘μ„±λœ λ‚ μ§œ, created 된 λ‚ μ§œλ₯Ό μ‚¬μš©ν•  수 있게 λœλ‹€λŠ” 것이닀.

 

이와 λΉ„μŠ·ν•œ μ–΄λ…Έν…Œμ΄μ…˜μ΄ λͺ‡ 개 더 μ‘΄μž¬ν•œλ‹€.

 

  • CreatedDate
    • ν•΄λ‹Ή μ—”ν‹°ν‹°κ°€ 생성될 λ•Œ, μƒμ„±ν•˜λŠ” μ‹œκ°μ„ μžλ™μœΌλ‘œ μ‚½μž…ν•΄μ€€λ‹€.
  • CreatedBy
    • ν•΄λ‹Ή μ—”ν‹°ν‹°κ°€ 생성될 λ•Œ, μƒμ„±ν•˜λŠ” μ‚¬λžŒμ΄ λˆ„κ΅¬μΈμ§€ μžλ™μœΌλ‘œ μ‚½μž…ν•΄μ€€λ‹€.
    • μƒμ„±ν•˜λŠ” 주체λ₯Ό μ§€μ •ν•˜κΈ° μœ„ν•΄μ„œ AuditorAware<T> λ₯Ό μ§€μ •ν•΄μ•Ό ν•œλ‹€.
      • μ΄λŠ” Spring Security 와 ν•¨κ»˜ 닀뀄야 ν•˜λŠ” λ‚΄μš©μ΄λ―€λ‘œ μΆ”ν›„ μ—…λ‘œλ“œ μ˜ˆμ •
  • LastModifiedDate
    • ν•΄λ‹Ή μ—”ν‹°ν‹°κ°€ μˆ˜μ •λ  λ•Œ, μˆ˜μ •ν•˜λŠ” μ‹œκ°μ„ μžλ™μœΌλ‘œ μ‚½μž…ν•΄μ€€λ‹€.
  • LastModifiedBy
    • ν•΄λ‹Ή μ—”ν‹°ν‹°κ°€ μˆ˜μ •λ  λ•Œ, μˆ˜μ •ν•˜λŠ” 주체가 λˆ„κ΅¬μΈμ§€ μžλ™μœΌλ‘œ μ‚½μž…ν•΄μ€€λ‹€.
      • μƒμ„±ν•˜λŠ” 주체λ₯Ό μ§€μ •ν•˜κΈ° μœ„ν•΄μ„œ AuditorAware<T> λ₯Ό μ§€μ •ν•΄μ•Ό ν•œλ‹€.
        • μ΄λŠ” Spring Security 와 ν•¨κ»˜ 닀뀄야 ν•˜λŠ” λ‚΄μš©μ΄λ―€λ‘œ μΆ”ν›„ μ—…λ‘œλ“œ μ˜ˆμ •

μœ„μ—μ„œ μ–ΈκΈ‰ν•˜μ˜€λ“― CreatedBy 와 LastModifiedBy μ–΄λ…Έν…Œμ΄μ…˜μ€ μΆ”ν›„ Spring Security μ‹œλ¦¬μ¦ˆμ™€ ν•¨κ»˜ 이야기 해보렀 ν•œλ‹€.
λ§Œμ•½ κΆκΈˆν•˜λ‹€λ©΄ AuditorAware 적용 ν‚€μ›Œλ“œλ‘œ 검색해볼 것을 μΆ”μ²œν•œλ‹€.

@Column(updatable = false)

μ΄λŠ” JPA의 κΈ°λ³Έ μ–΄λ…Έν…Œμ΄μ…˜ μ—μ„œ λ‚˜μ˜¨ @Columnκ³Ό λ™μΌν•˜λ‹€.

 

updatable 을 μ™œ false 둜 ν–ˆμ„κΉŒ?

 

ν˜Ήμ‹œ λͺ¨λ₯Ό 상황을 λŒ€λΉ„ν•΄μ„œμ΄λ‹€.

μš°λ¦¬λŠ” ν•΄λ‹Ή BaseEntityλ₯Ό JPAκ°€ ν…Œμ΄λΈ”μ— μ ‘κ·Όν•˜λŠ” μ‹œμ μ—λ§Œ JPAκ°€ μ‚¬μš©ν•˜λ„λ‘ ν•˜κ³  싢은데 λ§Œμ•½ κ°œλ°œμžμ— μ˜ν•΄ μˆ˜μ •λ˜λ©΄ μ•ˆλ˜κΈ° λ•Œλ¬Έμ— updatable을 false둜 ν•΄μ£ΌλŠ” 것을 ꢌμž₯ν•œλ‹€.

ν…ŒμŠ€νŠΈ

자 이제 ν…ŒμŠ€νŠΈλ₯Ό ν•΄λ³΄μž.

μš°λ¦¬λŠ” User μ—”ν‹°ν‹°λ₯Ό 생성할 λ•Œ createdBy 와 updatedBy λ₯Ό Auditing ν•˜κ²Œ ν•  것이닀.

  • BaseEntity.class
    • Auditing 을 μˆ˜ν–‰ν•  κΈ°λ³Έ 엔티티이닀.
  • User.class
    • BaseEntity λ₯Ό 상속받아 BaseEntity의 Auditing 을 μ΄μš©ν•  객체이닀.
  • UserRepository.interface
    • κΈ°λ³Έ JpaRepository λ₯Ό 상속받을 μΈν„°νŽ˜μ΄μŠ€μ΄λ‹€.
  • UserRepositoryTest.class
    • ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ ν…ŒμŠ€νŠΈ ν΄λž˜μŠ€μ΄λ‹€.

BaseEntity.class

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(updatable = false)
    private LocalDateTime updatedAt;
}

우리의 μ†Œμ€‘ν•œ Auditor 엔티티이닀.

 

μ•žμ„œ 말 ν–ˆλ˜ 4κ°€μ§€ μ–΄λ…Έν…Œμ΄μ…˜μ„ μΆ”κ°€ν•΄μ£Όλ©΄ λœλ‹€.

User.class

@Entity
public class User extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String address;
    private int age;
}

μ€‘μš”ν•œ 것은 BaseEntity λ₯Ό extends ν•΄μ•Ό ν•œλ‹€λŠ” 것이닀.


μ—­μ‹œ 둬볡 κ΄€λ ¨λœ μ–΄λ…Έν…Œμ΄μ…˜μ€ μ‚­μ œν–ˆλ‹€.

UserRepository.interface

public interface UserRepository extends JpaRepository<User, Long> {
}

UserRepositoryTest.class

@SpringBootTest
@Transactional
@Rollback(false)
class UserRepositoryTest {

    @Autowired
    UserRepository userRepository;

    @Test
    @DisplayName("Auditing κΈ°λŠ₯ 적용")
    void findUser() {
        // given
        User user = User.builder()
                .username("user ")
                .age(20)
                .address("Korfea")
                .build();

        // when
        User savedUser = userRepository.save(user);

        // then
        assertNotNull(savedUser.getCreatedAt());
        assertNotNull(savedUser.getUpdatedAt());
    }
}

ν…ŒμŠ€νŠΈκ°€ ν†΅κ³Όν•˜λŠ” 것을 λ³Ό 수 μžˆλ‹€.

쿼리문도 μ μ ˆν•˜κ²Œ λ‚˜κ°€κ²Œ 되고 h2 λ°μ΄ν„°λ² μ΄μŠ€μ—λ„ 잘 적용된 것을 λ³Ό 수 μžˆλ‹€.