JPA Auditing์ด๋?
JPA Auditing์ ์ํฐํฐ์ ์์ฑ ๋ฐ ์์ ์ ๋ํ ๊ฐ์ฌ ์ ๋ณด(๋ฑ๋ก ์๊ฐ, ์์ ์๊ฐ, ๋ฑ๋ก์, ์์ ์)๋ฅผ ์๋์ผ๋ก ๊ด๋ฆฌํด์ฃผ๋ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ ๋๋ค. ์ด๋ฅผ ํ์ฉํ๋ฉด ๋งค๋ฒ ์ํฐํฐ๋ฅผ ์์ฑํ๊ฑฐ๋ ์์ ํ ๋ ์๊ฐ์ ์๋์ผ๋ก ์ค์ ํ๊ฑฐ๋, ์์ ์๋ฅผ ์ง์ ํ๋ ๋ฒ๊ฑฐ๋ก์ ์์ด ์๋์ผ๋ก ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
์์ JPA ์ฌ์ฉ์ Auditing ์ค์
์์ JPA ํ๊ฒฝ์์๋ @PrePersist
์ @PreUpdate
์ฝ๋ฐฑ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ํฐํฐ๊ฐ ์์ํ๋๊ธฐ ์ ๊ณผ ์
๋ฐ์ดํธ ๋๊ธฐ ์ ์ ์๋์ผ๋ก ๋ ์ง๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค.
@MappedSuperclass
@Getter
public class BaseEntity {
@Column(updatable = false)
private LocalDateTime createdDate;
private LocalDateTime updatedDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
createdDate = now;
updatedDate = now;
}
@PreUpdate
public void preUpdate() {
updatedDate = LocalDateTime.now();
}
}
@Entity
@Getter
public class Member extends BaseEntity { ... }
Spring Data JPA ํ์ฉ
Spring Data JPA๋ฅผ ์ฌ์ฉํ๋ฉด @EnableJpaAuditing
์ด๋
ธํ
์ด์
๊ณผ AuditingEntityListener
๋ฅผ ํตํด ๋ ๊ฐ๋จํ๊ฒ Auditing ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ต๋๋ค.
๋จผ์ , Spring Boot ๋ฉ์ธ ํด๋์ค ๋๋ ๋ณ๋์ ์ค์ ํด๋์ค์ @EnableJpaAuditing
์ด๋
ธํ
์ด์
์ ์ถ๊ฐํฉ๋๋ค.
@SpringBootApplication
@EnableJpaAuditing
public class SpringApplication {
public static void main(String[] args) {
SpringApplication.run(SpringApplication.class, args);
}
}
์ด์ด์, BaseEntity ํด๋์ค์ @CreatedDate
๋ฐ @LastModifiedDate
์ด๋
ธํ
์ด์
์ ์ฌ์ฉํด ์๋์ผ๋ก ์๊ฐ์ด ๊ธฐ๋ก๋๋๋ก ์ค์ ํฉ๋๋ค.
๐ ์ค๋ฌด์์๋ ์ธ์ ์ ๋ณด๋, ์คํ๋ง ์ํ๋ฆฌํฐ ๋ก๊ทธ์ธ ์ ๋ณด์์ ID๋ฅผ ๋ฐ์
- BaseEntity
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass // ๊ฐ์ฒด์ ์
์ฅ์์ ๊ณตํต ๋งคํ ์ ๋ณด๊ฐ ํ์ํ ๋ ์ฌ์ฉ
@Getter
public class BaseEntity {
// ๋ฑ๋ก์ผ
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
// ์์ ์ผ
@LastModifiedDate
private LocalDateTime lastModifiedDate;
// ๋ฑ๋ก์
@CreatedBy
@Column(updatable = false)
private String createdBy;
// ์์ ์
@LastModifiedBy
private String lastModifiedBy;
}
- Member
public class Member extends BaseEntity{
...
}
- test
@Test
public void JpaEventBaseEntity() throws Exception {
//given
Member member = new Member("member1");
memberRepository.save(member); //@PrePersist
Thread.sleep(100);
member.setUsername("member2");
em.flush(); //@PreUpdate
em.clear();
//when
Member findMember = memberRepository.findById(member.getId()).get();
//then
System.out.println("findMember.createdDate = " + findMember.getCreatedDate());
System.out.println("findMember.updatedDate = " + findMember.getLastModifiedDate());
System.out.println("findMember.createdBy = " + findMember.getCreatedBy());
System.out.println("findMember.lastModifiedBy = " + findMember.getLastModifiedBy());
}
๊ตฌํ2
์ค๋ฌด์์ ๋๋ถ๋ถ์ ์ํฐํฐ๋ ๋ฑ๋ก์๊ฐ, ์์ ์๊ฐ์ด ํ์ํ์ง๋ง, ๋ฑ๋ก์, ์์ ์๋ ์์ ์๋ ์๋ค.
๊ทธ๋์ ๋ค์๊ณผ ๊ฐ์ด Base ํ์ ์ ๋ถ๋ฆฌํ๊ณ , ์ํ๋ ํ์ ์ ์ ํํด์ ์์ํ๋ค.
- BaseTimeEntity
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
- BaseEntity
public class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
Spring Security์์ ํตํฉ
Spring Security ํ๊ฒฝ์์๋ ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ฅผ ์ํฐํฐ์ ์์ฑ์ ๋ฐ ์์ ์๋ก ์๋์ผ๋ก ๊ธฐ๋กํ๊ณ ์ ํ ๋, AuditorAware
๊ตฌํ์ฒด๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์ด ๊ตฌํ์ฒด๋ getCurrentAuditor()
๋ฉ์๋์์ ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class AuditingConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return new SpringSecurityAuditorAware();
}
private static class SpringSecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken) {
return Optional.empty();
}
return Optional.ofNullable(authentication.getName());
}
}
}
@EnableJpaAuditing
: Spring Data JPA์ Auditing ๊ธฐ๋ฅ์ ํ์ฑํํฉ๋๋ค.AuditorAware<String>
: ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๋ฐํํ๋ ๋น์ ๋ฑ๋กํฉ๋๋ค.
๋ค๋ฅธ ๊ฒฝ์ฐ์ ๋ํ ์ฒ๋ฆฌ
Spring Security๊ฐ ์๋ ๋ค๋ฅธ ๋ณด์ ํ๊ฒฝ์ด๋, ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ์ฌ์ฉํ์ง ์๋ ๊ฒฝ์ฐ, AuditorAware
๊ตฌํ์ฒด์์ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก ํ์ฌ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ธ์
์ ๋ณด, JWT ํ ํฐ, ํน์ HTTP ํค๋ ๋ฑ ๋ค๋ฅธ ๋ฉ์ปค๋์ฆ์ ํตํด ์ฌ์ฉ์๋ฅผ ์๋ณํ๊ณ ์ด๋ฅผ getCurrentAuditor()
์์ ๋ฐํํ๋๋ก ๊ตฌํํ ์ ์์ต๋๋ค.
๋ธ๋ก๊ทธ ๊ธ: Spring Security์ JPA Auditing์ ํ์ฉํ ์๋ ๋ฑ๋ก์ ๋ฐ ์์ ์ ์ฒ๋ฆฌ
JPA Auditing์ด๋?
JPA Auditing์ ์ํฐํฐ์ ์์ฑ ๋ฐ ์์ ์์ ์ ์๋์ผ๋ก ๋ ์ง ๋ฐ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ธฐ๋กํ๋ ๊ธฐ๋ฅ์ ๋๋ค. ์ด ๊ธฐ๋ฅ์ ํ์ฉํ๋ฉด ๊ฐ๋ฐ์๊ฐ ์๋์ผ๋ก ๊ฐ ์ํฐํฐ์ ์์ฑ ์๊ฐ, ์์ ์๊ฐ, ์์ฑ์, ์์ ์๋ฅผ ๊ด๋ฆฌํ์ง ์์๋ ๋ฉ๋๋ค.
BaseEntity ํด๋์ค ๊ตฌ์ฑ
์ํฐํฐ์ ๊ณตํต์ ์ธ ์์ฑ์ ๊ด๋ฆฌํ๊ธฐ ์ํด BaseEntity ํด๋์ค๋ฅผ ๊ตฌ์ฑํฉ๋๋ค. ์ด ํด๋์ค๋ ๋ชจ๋ ์ํฐํฐ์ ์์ ํด๋์ค๋ก์, ์์ฑ ๋ฐ ์์ ์๊ฐ, ์์ฑ์, ์์ ์ ์ ๋ณด๋ฅผ ํฌํจํฉ๋๋ค.
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
Spring Security์์ ํตํฉ
Spring Security ํ๊ฒฝ์์๋ ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ฅผ ์ํฐํฐ์ ์์ฑ์ ๋ฐ ์์ ์๋ก ์๋์ผ๋ก ๊ธฐ๋กํ๊ณ ์ ํ ๋, AuditorAware
๊ตฌํ์ฒด๋ฅผ ์ฌ์ฉํฉ๋๋ค. ์ด ๊ตฌํ์ฒด๋ getCurrentAuditor()
๋ฉ์๋์์ ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๋ฐํํฉ๋๋ค.
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class AuditingConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
}
๋ค๋ฅธ ๊ฒฝ์ฐ์ ๋ํ ์ฒ๋ฆฌ ์์
Spring Security๊ฐ ์๋ ๊ฒฝ์ฐ, ๋ค์์ AuditorAware
๊ตฌํ์ฒด๋ฅผ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํ๋ ์์์
๋๋ค:
์์ 1: ์ธ์ ์ ๋ณด ํ์ฉ
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.map(attributes -> (HttpSession) ((ServletRequestAttributes) attributes).getRequest().getSession(false))
.map(session -> (String) session.getAttribute("username"));
}
์์ 2: JWT ํ ํฐ์์ ์ฌ์ฉ์ ์ ๋ณด ์ถ์ถ
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.map(attributes -> ((ServletRequestAttributes) attributes).getRequest().getHeader("Authorization"))
.filter(authHeader -> authHeader.startsWith("Bearer "))
.map(authHeader -> authHeader.substring(7))
.map(this::extractUsernameFromJWT);
}
private String extractUsernameFromJWT(String token) {
// JWT ํ ํฐ์์ username ์ถ์ถ ๋ก์ง
}
์์ 3: ThreadLocal์ ์ฌ์ฉํ ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ ๋ฐ ์กฐํ
public class UserContextHolder {
private static final ThreadLocal<String> userHolder = new ThreadLocal<>();
public static void setUsername(String username) {
userHolder.set(username);
}
public static String getUsername() {
return userHolder.get();
}
public static void clear() {
userHolder.remove();
}
}
@Bean
public AuditorAware<String> auditorProvider() {
return UserContextHolder::getUsername;
}
๋ง๋ฌด๋ฆฌ
JPA Auditing๊ณผ Spring Security๋ฅผ ํตํฉํจ์ผ๋ก์จ, ์ ํ๋ฆฌ์ผ์ด์
์ ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ์ ๊ฐํํ๊ณ , ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ด๋ ฅ์ ๋ณด๋ค ํจ๊ณผ์ ์ผ๋ก ์ถ์ ํ ์ ์์ต๋๋ค. ์ด ์ค์ ์ ํตํด, ์ํฐํฐ๊ฐ ์์ฑ๋๊ฑฐ๋ ์์ ๋ ๋ ์๋์ผ๋ก ์๊ฐ๊ณผ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ๊ธฐ๋ก๋์ด, ์ ํ๋ฆฌ์ผ์ด์
์ ์ ๋ขฐ์ฑ์ ๋์ผ ์ ์์ต๋๋ค. ๋ํ, Spring Security๊ฐ ์๋ ๋ค๋ฅธ ํ๊ฒฝ์์๋ ์ ์ ํ AuditorAware
๊ตฌํ์ ํตํด ์ ์ฐํ๊ฒ ๋ฑ๋ก์์ ์์ ์๋ฅผ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.