일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- Test code
- MySQL
- Service 계층 테스트
- Test
- Java
- enumSet
- JPA
- JPQL
- Spring
- 공룡책
- Test Doulbe
- mapstruct
- 소프티어
- ExceptionResolver
- Junit 5
- Server
- proxyFactory
- 테크쇼
- ObjectMapper
- softeer
- 자바
- backend
- 인프콘2023
- RequestBody
- db
- modelmapper
- OS
- FCM
- 일상
- Coputer Science
- Today
- Total
공부내용공유
Mongo @CreateDate, @LastModifiedDate 오류 (feat: Spring Data Auditing) 본문
Mongo @CreateDate, @LastModifiedDate 오류 (feat: Spring Data Auditing)
forfun 2024. 11. 8. 16:39서론
@CreateDate와 @LastModifiedDate는 Spring Data JPA를 사용했다면 한번쯤은 사용해봤거나 들어봤을 것이다.
나도 현재 진행중인 프로젝트에서 Spring Data Mongo와 함께 사용중인데 @LastModifiedDate 가 붙은 필드가 업데이트가 안되는 경우가 발생하여서 정확히 언제 어떻게 업데이트를 쳐주는지 알아야 할 것 같아서 슥 조사하고 정리해봤다.
본론
이 글의 목차는
- @CreateDatet와 @LastModifiedDate
- 업데이트가 왜 안되었는가
- 언제 어떻게 업데이트 되는건가
로 구성될 예정이다. 시간이 없으면 결론만 확인하면 된다.
@CreateDate와 @LastModifiedDate
어플리케이션에서 기능을 수행하든 어플리케이션에서 다루고 있는 데이터에 대한 통계, 분석을 위해서는 해당 데이터가 생성된 날짜, 수정된 날짜가 필요하다. 그렇기에 각 테이블들은 공통적으로 createdAt, updatedAt과 같은 필드를 date time 형태로 가지고 있다.
createdAt까지는 그렇다쳐도 updatedAt은 데이터의 필드 하나라도 변경되면 같이 변경되어야 하기에 신경쓰기 여간 귀찮은 것이 아니다.
Spring Data는 이러한 수고로움을 덜하고자 @CreatedDate, @LastModifiedDate 어노테이션을 지원하여 데이터가 처음 생성되면 생성된 시점에 값을 자동으로 넣어주고 업데이트가 되는 경우 그 시점으로 @LastModifiedDate 가 붙은 필드를 업데이트 해준다.
나는 Spring Data Mongo와 같이 썼는데
public class BaseDocument {
@CreatedAt
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
@EnableMongoAuditing
@Configuration
public class MongoConfig {
...
}
이런식으로 main application 코드나 config 코드에 @EnableMongoAuditing 을 달아주고 사용하고 싶은 필드에 @CreatedAt, @LastModifiedDate 를 달아주면 된다. (@EnableMongoAuditing 이거 빼먹어서 안되요 ㅠㅠ 하는 질문들 많던데 주의하자)
내용과는 별개지만 위에서 언급한 것처럼 거의 모든 데이터의 공통 필드이기에 저런식으로 클래스를 하나 만들고 각 도큐먼트 클래스들이 상속하여 사용하게 하였다.
업데이트가 왜 안되었는가
해당 어노테이션을 사용하고 나는 무적이다 ㅋㅋ 하면서 열심히 기능 개발을 하였다. 내가 진행한 프로젝트는 쿼리는 모두 mongoTemplate을 사용하고 있었는데 Spring Mongo Jpa를 사용하지 않음에도 updatedAt이 잘 변경되어 안심하고 있었다.
그런데 분명 업데이트를 한번 한 데이터인데 createdAt과 updatedAt이 동일한걸 발견하였다. (다행히 프로덕션까지는 안올라가 있었다.)
업데이트 api의 쿼리 메서드를 보니 Update 라는 클래스를 사용하고 있었다. 별로 안궁금할 수도 있지만 Update 예시 코드를 보여주자면
Query query = new Query();
query.addCriteria(Criteria.where("userNo").is(userNo));
Update update = new Update()
.set("userName", name);
mongoTempalte.updateFirest(query, update, UserDocument.class);
요런 느낌으로 썼는데 이렇게 쓰니까 업데이트가 안된다. 단순히 mongoTemplate 때문이라기에는
//얘나
Query query = new Query();
query.addCriteria(Criteria.where("userNo").is(userNo));
FindAndReplaceOptions findAndReplaceOptions = FindAndReplaceOptions.options().returnNew();
mongoTempalte.findAndReplace(query, updatedUser, findAndReplaceOptions);
//얘도
UserDocument updatedUser = dto.getUpdatedUserDocument();
mongoTemplate.save(updatedUser);
업데이트가 다 잘된다..
그래서 일단 Update 를 사용하는 쿼리의 경우
Query query = new Query();
query.addCriteria(Criteria.where("userNo").is(userNo));
Update update = new Update()
.set("userName", name)
.set("updatedAt", LocalDateTime);
mongoTempalte.updateFirest(query, update, UserDocument.class);
이렇게 변경하였다.. 자 그러면 어디서 업데이트를 담당하기에 Update만 @LastModifiedDate 가 적용이 안될까?
언제 어떻게 업데이트 되는건가
자 mongoTemplate 부터 천천히 내려가보자 일단 시작은 mongoTemplate 부터이다. 위에서는 save, findAndReplace 를 언급했지만
일단 save를 예시로 어디서 업데이트를 치는지 살펴보자.
mongoTemplate의 save를 따라가다 보면 최종적으로 doSave
와 doSaveVersioned
에 도달하게 된다. 여기서 나오는 version의 개념은 저장, 업데이트 될 때마다 @Version이 사용된 필드를 조회, 업데이트 하는 로직을 추가하여 동시에 업데이트가 되는 상황을 방지해준다.
여기서는 크게 다룰 내용은 아니고 doSave
와 doSaveVersioned
공통적으로 들어가있는 로직에서 @LastModifiedDate 가 붙은 필드를 업데이트 해준다.
protected <T> T doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
objectToSave = ((BeforeConvertEvent)this.maybeEmitEvent(new BeforeConvertEvent(objectToSave, collectionName))).getSource();
objectToSave = this.maybeCallBeforeConvert(objectToSave, collectionName);
EntityOperations.AdaptibleEntity<T> entity = this.operations.forEntity(objectToSave, this.mongoConverter.getConversionService());
entity.assertUpdateableIdIfNotSet();
MappedDocument mapped = entity.toMappedDocument(writer);
Document dbDoc = mapped.getDocument();
this.maybeEmitEvent(new BeforeSaveEvent(objectToSave, dbDoc, collectionName));
objectToSave = this.maybeCallBeforeSave(objectToSave, dbDoc, collectionName);
Object id = this.saveDocument(collectionName, dbDoc, objectToSave.getClass());
T saved = this.populateIdIfNecessary(objectToSave, id);
this.maybeEmitEvent(new AfterSaveEvent(saved, dbDoc, collectionName));
return this.maybeCallAfterSave(saved, dbDoc, collectionName);
}
mongoTemplate의 doSave 메서드인데 이 중
objectToSave = ((BeforeConvertEvent)this.maybeEmitEvent(new BeforeConvertEvent(objectToSave, collectionName))).getSource();
objectToSave = this.maybeCallBeforeConvert(objectToSave, collectionName);
이 두가지 메서드가 수상해서 따라가본 결과.
objectToSave = this.ç(objectToSave, collectionName);
해당 메서드에서 @CreateData, @LastModifiedDate를 변경해준다.
BeforeConvertEvent는 mongoDB 라이브러리 디렉토리에 EventHandler가 있긴한데 Deprecated 되었다고 한다. 그리고 디버깅 해보니 해당 메서드 실행되어도 date 관련은 변경되지 않는다.
maybeCallBeforeConvert
메서드는
public class AuditingEntityCallback implements BeforeConvertCallback<Object>, Ordered {
private final ObjectFactory<IsNewAwareAuditingHandler> auditingHandlerFactory;
public AuditingEntityCallback(ObjectFactory<IsNewAwareAuditingHandler> auditingHandlerFactory) {
Assert.notNull(auditingHandlerFactory, "IsNewAwareAuditingHandler must not be null!");
this.auditingHandlerFactory = auditingHandlerFactory;
}
public Object onBeforeConvert(Object entity, String collection) {
return ((IsNewAwareAuditingHandler)this.auditingHandlerFactory.getObject()).markAudited(entity);
}
public int getOrder() {
return 100;
}
}
요런 클래스를 호출해 반가운 auditing 어쩌고를 시전한다. 저 markAudited 따라 들어가면 spring data에서 지원하는 auditing 클래스를 만나고 해당 클래스에서 @CreateDate 나 @LastModifiedDate를 넣어준다.
아래 클래스는 AuditingHandlerSupport의 touch 메서드이다.
private <T> T touch(Auditor auditor, T target, boolean isNew) {
Optional<AuditableBeanWrapper<T>> wrapper = this.factory.getBeanWrapperFor(target);
return wrapper.map((it) -> {
this.touchAuditor(auditor, it, isNew);
Optional<TemporalAccessor> now = this.dateTimeForNow ? this.touchDate(it, isNew) : Optional.empty();
if (logger.isDebugEnabled()) {
Object defaultedNow = now.map(Object::toString).orElse("not set");
Object defaultedAuditor = auditor.isPresent() ? auditor.toString() : "unknown";
logger.debug(LogMessage.format("Touched %s - Last modification at %s by %s", target, defaultedNow, defaultedAuditor));
}
return it.getBean();
}).orElse(target);
}
결론
그래서 결론은 mongoTemplate에서 objectToSave = this.maybeCallBeforeConvert(objectToSave, collectionName);
부분이 @CreateDate, @LastModifiedDate 를 처리해주니 해당 로직이 들어가 있는지 확인하고 메서드를 사용하자 이다.
'Spring > Spring' 카테고리의 다른 글
spring validation (service, presentation 어디서 검증할까?) (2) | 2024.02.18 |
---|---|
getFile 사용시 주의점 (feat: FileNotFoundException) (0) | 2023.10.06 |
FCM 기능 구현 및 리팩토링을 하면서의 고민들 (0) | 2023.09.08 |
Spring Proxy Factory 뜯어보기 (0) | 2023.09.05 |
Test Code H2 DB 사용시 주의사항 (0) | 2023.07.28 |