EntityListener의 두 번째 강의입니다. 실습 위주라 필기할 내용은 생각보다 많지 않았고, 새로운 내용이 좀 있습니다. 덕분에 필기하는 시간보다 실습하고 찾아보는 시간이 더 기네요.
UserHistory 를 만들어보자
User가 변경되었을 때 마지막으로 변경한 시간을 남기는 것도 좋지만, 변경 내역이 별도의 테이블에 들어있으면 나중에 확인하기 더 수월합니다. 그래서 UserHistory 를 만듭니다.
방법은 EntityListener를 만드는 것까지는 똑같습니다. EntityListener 를 만드는 시점에서 좀 달라집니다.
히스토리 엔티티
생략
public class UserHistory {
@Id
@GeneratedValue
private Long id;
private Long userId; // <<<<<<
생략
public UserHistory from(User user) {
userId = user.getId(); // <<<<<<
편의성 메소드의 나머지 본문
}
}
히스토리 리포지터리
interface UserHistoryRepo: JpaRepository<UserHistory, Long>
유저에 리스너를 붙이고
@EntityListeners(value = {이전강의에서만든.class, UserHistoryListener.class})
public class User {
생략
}
히스토리 리스너
class UserHistoryListener {
@Autowired
private lateinit var userHistoryRepo: UserHistoryRepo
// ^^^^^^^^^^^^^^^
// 인젝션되지 않습니다.
@PrePersist
@PreUpdate
fun preUserPersistAndUpdate(o: Any) {
(o as? User)?.let {
userHistoryRepo.saveAndFlush(UserHistory().from(it))
}
}
}
EntityListener에 Bean 주입하기
리스너에서 청천벽력같은 일이 일어납니다. @Autowired 로 주입되었어야 할 userHistoryRepo 가 항상 null인 것입니다.
아마 사유는 @EntityListeners 기능이 스프링의 것이 아니고 Java EE, 즉 Jakarta 의 것이어서 그럴 가능성이 높아보입니다. JPA 컨셉이라서 JPA 프로바이더 (Hibernate 같은) 가 제공하는 기능이고, 그렇기에 Spring이 리스너 객체를 관리하지 않아서 그렇다네요.
해결방법은 몇 가지 있는 거 같은데, 강의에서는 스프링의 ApplicationContext (* DI 컨테이너) 에서 Bean (* DI가 생성한 객체) 을 가져오는 유틸리티 클래스를 만들어서 해결합니다.
ApplicationContext 를 받아오는 ApplicationContextAware 인터페이스를 상속받는 방법을 활용하므로, @Component 로 등록하는 건 필수입니다.
@Component // 필수입니다.
class BeanUtils: ApplicationContextAware {
// @Component 가 되는 과정에서 실행되므로, 이 클래스는 @Component 여야 합니다.
// @Component 를 떼면 아래와 같은 에러가 납니다.
// java.lang.NullPointerException
// at kr.co.fastcampus.jpasandbox.service.BeanUtils$Companion.getBean(BeanUtils.kt:13)
override fun setApplicationContext(applicationContext: ApplicationContext) {
BeanUtils.applicationContext = applicationContext
}
companion object {
private var applicationContext: ApplicationContext? = null
fun <T> getBean(clazz: Class<T>): T = applicationContext!!.getBean(clazz)
}
}
applicationContext 가 없으면 그냥 fail fast 하게 짰어요.
ApplicationContextAware 는 인터페이스고, 설명은 아래와 같습니다. (IDE)
Interface to be implemented by any object that wishes to be notified of the ApplicationContext that it runs in.
다른 방법도 있다는데, 거기에 대해서는 자세히 알아보지 않았습니다. 다음 섹션에 나올 내장 리스너에서 사용하는 방법이라는데...
일단 리스너부터 위의 코드를 사용하도록 바꾸죠. 위에서 언급했듯이, EntityListeners 는 JPA 에서 활용되는 객체이므로 @Component 일 필요가 없다는 점은 다시 유의합시다. (무지성으로 붙여도 아마도 메모리를 더 쓰는 정도로? 별 탈은 없겠지만요)
class UserHistoryListener {
@PrePersist
@PreUpdate
fun preUserPersistAndUpdate(o: Any) {
val repo = BeanUtils.getBean(UserHistoryRepo::class.java)
(o as? User)?.let {
repo.saveAndFlush(UserHistory().from(it))
}
}
}
UserHistory 도 변경날짜를 기록하자
UserHistory 엔티티도 마찬가지로 만든 날짜, 수정한 날짜를 기록해야 합니다. 수정한 날짜는 필요없겠지만... 이전 강의에서 만드는 걸 재활용하는 셈 치고 적용해둡시다. 다음 섹션에서 이것도 같이 내장 기능으로 바꿀거거든요.
생략
@EntityListeners(value = {AuditListener.class})
public class UserHistory implements IAuditable {
생략
@Column(updatable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
AuditingEntityListener: Spring 내장 Auditing Listener
만든 날짜, 수정한 날짜를 별도 칼럼에 기록하는 게 정말 많이들 하는 짓이다보니 Spring 의 내장으로 이미 구현되어있는 리스너인 AuditingEntityListener 가 있습니다.
(위에서 했던 History 를 남기는 작업이 아니라, 엔티티의 수정일 / 갱신일을 기록하는 작업을 말합니다. 강의 흐름이 바로 이전에서 이어져서 헷갈렸네요.)
내용은 까보면 이전 강의에서 만들었던 AuditListener랑 비슷하게 생겼습니다. 실제 호출을 다른데로 연결해준다는 걸 빼면 말이죠.
써봅시다.
먼저 설정을 해줍니다. Application 에 @EnableJpaAuditing 도 달아줍시다.
@SpringBootApplication
@EnableJpaAuditing
public class JpaSandboxApplication {
생략
다음으로 만든 날짜 / 수정한 날짜를 기록할 엔티티들에 AuditingEntityListener 와, 날짜 필드에 @CreatedDate, @LastUpdatedDate 를 달아줍니다. 이제 이전 강의에서 만들었던 AuditListener와 IAuditable은 필요없으니 상속도 지워버리구요.
앞서 만들었던 User 클래스의 히스토리를 관리하기 위한 UserHistoryListener 를 지울 필요는 없습니다.
생략
@EntityListeners(value = {AuditingEntityListener.class, UserHistoryListener.class})
public class User {
생략
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
같은 짓을 한 번 더
생략
@EntityListeners(value = {AuditingEntityListener.class})
public class UserHistory implements IAuditable {
생략
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
생략
}
이 외에도 @CreatedBy / @LastModifiedBy 도 있는데, 스프링 시큐리티와 연계되어 있다고 합니다. 시큐리티가 나올 때 다시 보게 되겠네요.
실전 스타일 Auditing - 부모 클래스를 만들자
매 엔티티마다 이 짓을 반복하고 있을 수는 없습니다. "실전"에서는 그래서 베이스 클래스를 별도로 만들어서 Audit 기능을 추가한 뒤, mixin 처럼 활용한다고 합니다. (Java에 mixin은 없지만 ㅎ)
베이스 클래스 (강의에서는 abstract 가 아니었는데... 뭐 중요한 건 아니니까요.)
@Data
@MappedSuperclass // <<<<<<< 이 클래스를 상속받는 클래스들도 부모클래스의 필드를 칼럼으로 사용
@EntityListeners(value = AuditingEntityListener.class)
public abstract class BaseAuditingEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
베이스 클래스에는 기존과 똑같지만 @MappedSuperclass 가 들어가는 게 차이점입니다.
Designates a class whose mapping information is applied to the entities that inherit from it. A mapped superclass has no separate table defined for it.
이걸 상속받는 엔티티에서는 이제 createdAt과 updatedAt 을 지워도 됩니다.
@Data
@NoArgsConstructor
@Entity
@EqualsAndHashCode(callSuper = true) // <<<<<
@ToString(callSuper = true) // <<<<<
@EntityListeners(value = {/* 삭제됨, */ UserHistoryListener.class})
public class User extends BaseAuditingEntity {
// ^^^^^^^^^^^^^^^^^^
// 추가
@Id
@GeneratedValue
private Long id;
@NonNull
private String name;
@NonNull
private String email;
// createdAt, updatedAt 삭제됨
}
부모 클래스에 필드가 생겼기 때문에, 상속만 하면 @Data 에 다음과 같은 에러가 나타납니다. 그래서 추가로 @EqualsAndHashcode(callSuper = true) 와 @ToString(callSuper = true) (이건 그냥 보고싶어서 넣으신듯) 이 추가로 들어가게 되었습니다. lombok 이 상속을 받는데 자식 필드만으로 hashcode 를 만드는 것도 말이 안 되죠?
Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '(callSuper=false)' to your type.
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
동영상 하나로 이렇게 오랜 시간을 쓴다고???
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04 JPA' 카테고리의 다른 글
JPA Ch 6 릴레이션 1:N, N:1 - 패스트캠퍼스 챌린지 27일차 (0) | 2022.02.19 |
---|---|
JPA Ch 6 릴레이션, ERD, 1:1 - 패스트캠퍼스 챌린지 26일차 (0) | 2022.02.18 |
JPA Ch 5 Entity Listener - 패스트캠퍼스 챌린지 24일차 (0) | 2022.02.16 |
JPA Ch 4 Entity 기본 어노테이션들 - 패스트캠퍼스 챌린지 23일차 (0) | 2022.02.15 |
JPA Ch 3 Query Method 소팅 / 페이징 - 패스트캠퍼스 챌린지 22일차 (0) | 2022.02.14 |