1. Jpa Auditing 이란?
- 엔티티에서 발생한 이벤트를 추적하고 기록함
- 이벤트 : 생성(create), 수정(update), 삭제(delete) 등
- Spring Data JPA 에서 기본적으로 Auditing 정보를 추가하는 기능 제공함
2. Jpa Auditing 주요 기능
- @CreatedDate
- 엔티티 생성 시간을 자동으로 기록함
@CreatedDate
private LocalDateTime createdAt;
- @LastModifiedDate
- 엔티티 최종 수정 시간을 자동으로 기록함
- 엔티티 생성 시 생성 시간이 저장됨
@LastModifiedDate
private LocalDateTime editedAt;
- @CreatedBy
- 엔티티를 생성한 사용자 정보를 자동으로 기록함
@CreatedBy
private User createdBy;
- @LastModifiedBy
- 엔티티를 최종 수정한 사용자 정보를 자동으로 기록함
- 엔티티 생성 시 생성한 사용자 정보가 저장됨
@LastModifiedBy
private User editedBy;
3. Base Entity
- 만약 모든 엔티티에 생성 시간, 수정 시간 등을 기록해야 한다면?
- 모든 엔티티에 Jpa Auditing의 기능에 해당하는 어노테이션을 붙여야 함
- 따라서 Base Entity 도입
- Auditing 관련 엔티티 생성
- Auditing이 필요한 실제 엔티티에서는 Base Entity를 상속받으면 중복 코드 줄일 수 있음!
- 예시 코드
- BaseEntity
@MappedSuperClass // Super 클래스로 지정하는 어노테이션
@EntityListners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime editedAt;
}
- 상속받는 엔티티 (Post)
@Entity
public class Post extends BaseEntity {
@Id
private Long id;
// BaseEntity의 필드들을 상속받음!
}
⇒ 엔티티 내부의 코드 복잡성이 줄어들고 중복 코드 감소!
4. Jpa Auditing + BaseEntity 사용 방법 및 예제
📌 사전 설명
- 스프링 시큐리티 사용 시 @CreatedBy, @LastModifiedBy 에 대해서 아래와 같이 작성 가능
1. SecurityContextHolder.getContext().getAuthentication()을 통해 현재 인증된 사용자의 Authentication 객체를 가져와서 사용자의 id, 이름 등을 가져옴
2. Post 객체에 User에 대한 참조 추가 가능
@CreatedBy private User createdBy; // User를 직접 참조 // 혹은 @CreatedBy private Long createdBy; // User.getId()등 통한 id만 참조
- 이 예제는 간단한 예제로 스프링 시큐리티 사용 x
- 여기서는 Post 생성, 수정 시 Header에 “loginUserId” (Long 타입) 을 담아서 넘길 것
⇒ createdBy, lastModifiedBy에는 Header에 담긴 userId 를 담음!
4-1. 의존 라이브러리
- build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
4-2. SpringBootApplication 수정
- @SpringBootApplication 에 해당하는 클래스 (여기서는 JpaAuditingExApplication.java) 에 @EnableJpaAuditing 어노테이션 추가
@EnableJpaAuditing
@SpringBootApplication
public class JpaAuditingExApplication {
public static void main(String[] args) {
SpringApplication.run(JpaAuditingExApplication.class, args);
}
}
4-3. @CreatedBy, @LastModifiedBy 관련 사용자 정보 설정
- AuditorAwareImpl
@Component
@RequiredArgsConstructor
public class AuditorAwareImpl implements AuditorAware<Long> {
private final HttpServletRequest request;
@Override
public Optional<Long> getCurrentAuditor() {
Long userId = Long.valueOf(request.getHeader("loginUserId"));
return Optional.ofNullable(userId);
}
}
⇒ 위에 작성했듯이 JPA Auditing 에 사용할 사용자 정보를 Header에 포함시켜 요청 전송할 것
이 loginUserId 가 엔티티 생성자(createdBy), 엔티티 수정자(lastModifiedBy)가 됨!
4-4. domain
- BaseEntity
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@CreatedBy
private Long createdBy;
@LastModifiedDate
private LocalDateTime editedAt;
@LastModifiedBy
private Long editedBy;
}
- Post
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class Post extends BaseEntity{
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;
private String title;
private String content;
public Post(String title, String content) {
this.title = title;
this.content = content;
}
}
- DTO > PostDto
@Data
public class PostDto {
private String title;
private String content;
}
4-5. repository
- PostRepository
public interface PostRepository extends JpaRepository<Post, Long> {
}
4-6. service
- PostService
- 간단한 조회, 생성, 수정 로직을 포함
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
public List<Post> findAll() {
return postRepository.findAll();
}
public Post findOne(Long postId) {
return postRepository.findById(postId).orElse(null);
}
@Transactional
public Post save(PostDto postDto) {
Post post = new Post(postDto.getTitle(), postDto.getContent());
return postRepository.save(post);
}
@Transactional
public Post update(Long postId, PostDto postDto) {
Post post = postRepository.findById(postId).orElse(null);
if(post == null) return null;
post.setTitle(postDto.getTitle());
post.setContent(postDto.getContent());
return postRepository.save(post);
}
}
4-7. controller
- PostApiController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/posts")
public class PostApiController {
private final PostService postService;
@GetMapping
public List<Post> findAll() {
return postService.findAll();
}
@GetMapping("/{postId}")
public Post findOne(@PathVariable("postId") Long postId) {
return postService.findOne(postId);
}
@PostMapping
public Post create(@RequestBody PostDto postDto) {
return postService.save(postDto);
}
@PutMapping("/{postId}")
public Post update(@PathVariable("postId") Long postId, @RequestBody PostDto postDto) {
return postService.update(postId, postDto);
}
}
5. 실행 결과
5-1. Post 엔티티 생성 (POST localhost:8080/api/posts)
- Header에 “loginUserId” : 1 추가
5-2. 생성된 Post 엔티티 MySQL로 확인
- created_at : 생성 시간 자동 저장됨을 확인
- edited_at : 최종 수정 시간 자동 저장됨을 확인
- created_by : 생성한 사용자 정보 (header에 담긴 id) 자동 저장됨을 확인
- edited_by : 수정한 사용자 정보 (header에 담긴 id) 자동 저장됨을 확인
5-3. Post 엔티티 수정 (PUT localhost:8080/api/posts/1)
- 이번에는 Header에 "loginUserId" : 2 추가
5-4. 수정한 Post 엔티티 MySQL로 확인
- created_at : 생성 시간이 저장됨을 확인 (변화 x)
- edited_at : 최종 수정 시간 자동 저장됨을 확인
- created_by : 생성한 사용자 정보가 저장됨을 확인 (변화 x)
- edited_by : 최종 수정한 사용자 정보 자동 저장됨을 확