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 ๊ตฌํ˜„์„ ํ†ตํ•ด ์œ ์—ฐํ•˜๊ฒŒ ๋“ฑ๋ก์ž์™€ ์ˆ˜์ •์ž๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.