현재 JPA를 사용하며 간단한 사이드 프로젝트를 만들고 있던 중 리펙토링을 진행하고 있는데 update시 DynamicUpdate가 걸려있는 entity를 update 할 때 문제가 발생하였습니다.
🎈 Paper Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED) // new 클래스() 제한
@ToString(callSuper = true)
@EqualsAndHashCode
@Getter
@Entity
@DynamicInsert // null로 들어가야 하는 경우 대응(기본값)
@DynamicUpdate // 변경한 필드만 대응
@Table(name = "tbl_paper")
public class Paper {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long paperId;
private String paperTitle;
private String theme;
private String paperUrl;
private String deleteYn;
private String userId;
private Timestamp createdAt;
private Timestamp updatedAt;
private Timestamp dueDate;
@Builder
public Paper(Long paperId, String paperTitle, String theme, String paperUrl, String deleteYn, String userId, Timestamp dueDate) {
this.paperId = paperId;
this.paperTitle = paperTitle;
this.theme = theme;
this.paperUrl = paperUrl;
this.deleteYn = deleteYn;
this.userId = userId;
this.dueDate = dueDate;
}
public GetPaperRes toGetPaperRes() {
return new GetPaperRes.GetPaperResBuilder().
paperId(this.paperId).
paperTitle(this.paperTitle).
theme(this.theme).
paperUrl(this.paperUrl).
deleteYn(this.deleteYn).
userId(this.userId).
createdAt(this.createdAt).
updatedAt(this.updatedAt).
dueDate(this.dueDate).
build();
}
public void softRemovePaper() {
this.deleteYn = "Y";
}
}
🎈 Paper Service
public PutPaperRes updatePaper(PutPaperReq putPaperReq, String email) {
//생성한 사람이 아닌 사람이 수정을 시도하는 경우
log.info("putPaperReq ={}, email = {}", putPaperReq, email);
if(!paperDao.checkEmailAndPaperId(email, putPaperReq.getPaperId())) {
throw new CustomException(FAILED_TO_PAPER_UPDATE);
}
boolean exists = paperRepository.existsByPaperId(putPaperReq.getPaperId());
if(!exists) {
return null;
}
Paper savedPaper = paperRepository.save(putPaperReq.toEntity(email));
return new PutPaperRes(savedPaper.getPaperId());
}
현재 update시에는 데이터를 front server의 웹 페이지 화면에서 전송받아 스프링에서 메시지 컨버터를 통해 Json을 변환하여 특정 dto를 통해 업데이트 로직을 태우고 있습니다.
🎈 Paper Update Dto
@Getter
@ToString
public class PutPaperReq {
@NotNull
@Positive
private Long paperId;
@NotBlank
private String paperTitle;
@NotBlank
private String theme;
@NotNull
private Timestamp dueDate;
private String userId;
public Paper toEntity(String userId) {
return Paper.builder().
paperId(this.paperId).
paperTitle(this.paperTitle).
theme(this.theme).
dueDate(this.dueDate).
userId(userId).
build();
}
}
JPA에서는 update 메소드를 제공하지 않기 때문에 특정 데이터를 변경하려면 save()를 통해서 저장하게 됩니다.
하지만 DynamicUpdate 어노테이션이 없다면 dto를 entity로 변환하여 PaperDto는 없고 Paper Entity에만 존재하는 멤버 변수는(생성시간, 수정시간 등) null로 저장이 됩니다. 하지만 기존의 데이터는 건들지 않고 변경된 필드만 업데이트를 시키려면 DynamicUpdate를 추가해주는 방법이 있습니다.
하지만 위의 Entity에는 DynamicUpdate 어노테이션이 있지만 실제 로직을 실행해보면
위와 같이 Paper Entity에 존재하지 않는 데이터는 null로 들어가게 되는 것을 확인할 수 있습니다.
🎈 왜 이럴까?
이 문제를 해결하려면 먼저 영속성 컨텍스트에 대해 알아야 합니다. JPA에서는 변경 감지라는 기능을 제공하는데 entity에 값을 변경하게 되면 트랜잭션이 커밋되는 시점에 entity의 최종 상태를 snapsho처럼 데이터 베이스에 자동으로 비교하여 반영해줍니다.
public PutPaperRes updatePaper(PutPaperReq putPaperReq, String email) {
//생성한 사람이 아닌 사람이 수정을 시도하는 경우
log.info("putPaperReq ={}, email = {}", putPaperReq, email);
if(!paperDao.checkEmailAndPaperId(email, putPaperReq.getPaperId())) {
throw new CustomException(FAILED_TO_PAPER_UPDATE);
}
//영속성 컨텍스트 1차 캐시에 엔티티 저장
boolean exists = paperRepository.existsByPaperId(putPaperReq.getPaperId());
if(!exists) {
return null;
}
//문제가 생긴 부분
Paper savedPaper = paperRepository.save(putPaperReq.toEntity(email));
return new PutPaperRes(savedPaper.getPaperId());
}
현재 위의 로직에서 exsistByPaperId를 할 때 영속성 컨텍스트 1차 캐시에 엔티티가 저장되어 save 하면서 flush 될 때 해당 엔티티의 상태가 업데이트된 걸로 판단하여 null이 db에 저장된 것입니다.
1차 캐시에 저장된 데이터에서 dto에서 받아온 created_at, delete_yn, paper_url, updated_at가 없는 entity를 업데이트하여 발생한 문제였습니다.
🎈 해결 로직
public PutPaperRes updatePaper(PutPaperReq putPaperReq, String email) {
//생성한 사람이 아닌 사람이 수정을 시도하는 경우
log.info("putPaperReq ={}, email = {}", putPaperReq, email);
if(!paperDao.checkEmailAndPaperId(email, putPaperReq.getPaperId())) {
throw new CustomException(FAILED_TO_PAPER_UPDATE);
}
Optional<Paper> optionalPaper = paperRepository.findByPaperId(putPaperReq.getPaperId());
if(optionalPaper.isEmpty()) {
return null;
}
Paper paper = putPaperReq.toEntity(optionalPaper.get());
Paper savedPaper = paperRepository.save(paper);
return savedPaper.toPutPaperRes();
}
문제가 생겼던 부분은 exist 대신 find로 해당 entity를 찾아와서 PaperDto를 entity로 변환할 때 optionalPaper를 같이 하나의 entity로 변환하여 문제를 해결하였습니다. 물론 기존 코드에서 1차 캐시를 비우거나 영속성 컨텍스트를 초기화하는 방법도 있겠지만, 기존 로직들의 일관성을 최대한 맞추고자 해당 방법으로 진행하였습니다.
🎈 Paper Entity, Dto 변화
//Paper Dto Builder 패턴에 find로 찾은 Paper Entity를 받아 Update시 추가
@Getter
@ToString
public class PutPaperReq {
@NotNull
@Positive
private Long paperId;
@NotBlank
private String paperTitle;
@NotBlank
private String theme;
@NotNull
private Timestamp dueDate;
private String userId;
public Paper toEntity(Paper paper) {
return Paper.builder().
paperId(this.paperId).
paperTitle(this.paperTitle).
theme(this.theme).
dueDate(this.dueDate).
userId(paper.getUserId()).
paperUrl(paper.getPaperUrl()).
deleteYn(paper.getDeleteYn()).
createdAt(paper.getCreatedAt()).
updatedAt(paper.getUpdatedAt()).
build();
}
}
//Paper Entity에 Builder 타입 변환을 위한 메소드 추가
public class Paper {
... 생략
public PutPaperRes toPutPaperRes() {
return new PutPaperRes.PutPaperResBuilder().
paperId(this.paperId).
paperTitle(this.paperTitle).
theme(this.theme).
paperUrl(this.paperUrl).
deleteYn(this.deleteYn).
userId(this.userId).
createdAt(this.createdAt).
updatedAt(this.updatedAt).
dueDate(this.dueDate).
build();
}
}
부족한 부분이나 잘못된 부분이 있다면 언제든지 댓글로 피드백 주시면 감사하겠습니다.
'Languege > Java & Spring' 카테고리의 다른 글
스프링부트 어노테이션이란? 커스텀 어노테이션 추가하는 방법 (0) | 2023.04.19 |
---|---|
[JAVA] 리스트 null로 초기화 (0) | 2022.11.18 |
[Spring Framework OPEN API서비스 교육] OAuth 2.0 (0) | 2022.10.18 |
[Spring Framework OPEN API서비스 교육] OAuth 1.0 (0) | 2022.10.07 |