티스토리 뷰
JPA 양방향 관계 Entity 저장하기
인간은 습관의 동물이다.
습관에는 대부분 좋은 습관이 많지만 그중에 나쁜 습관도 있다.
그것은 바로.. 원리를 모르고 개발하는 습관이다. 😯
.
'요로케할 때 이렇게 했었으니까, 이렇게 해야지~!' 는 더 이상 통하지 않는다..
사이드 프로젝트를 하며 깨닫는 부분이 많다.
.
습관처럼 몸에 익어버린 방법들이 갑자기 뇌 정지가 오며 멈춰버렸을 때, 머릿속이 하얘지고 말았다.
적어도 왜 이렇게 개발했었는지 원리를 알았더라면, 이럴 일은 없었을텐데..
정말 기본적인 것들 앞에서 갑자기 띠용🙄 할 때가 있다.
(최근에 습관성 타이핑으로 노트북 비밀번호 까먹은 거는 진짜 비밀..🤫)
.
본론으로, JPA 양방향 관계 Entity를 저장해보자.
Situation
상품 등록 시 여러 이미지를 첨부할 수 있다.
상품은 첨부파일과 1:N 관계(@OneToMany
)를 가지고 있다.
@Getter
@NoArgsConstructor
@Entity
public class Product extends BaseTimeEntity {
//...
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
private Set<ProductUploadFile> productUploadFiles = new LinkedHashSet<>();
//...
}
반대로 첨부파일은 상품과 N:1 관계(@ManyToOne
)를 가지고 있다.
@Getter
@NoArgsConstructor
@Entity
public class ProductUploadFile extends BaseTimeEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
//...
}
Problem
상품 저장 시 첨부파일도 함께 저장되어야 한다.
그런데.. 아래 코드는 뭔가 비효율적이게 보인다.
어떤 부분이 비효율적일까?
.
참고로, 요청에는 DTO 가 사용되었다.
@Transactional
public Long save(ProductDto.Request form, SessionUser user) throws IOException {
ProductCategory category = categoryRepository.findById(form.getProductCategory()).get();
/**
* 1. 상품 엔티티 생성 및 저장
*/
Product product = Product.builder()
.productCategory(category)
.name(form.getName())
.contents(form.getContents())
.hits(0L)
.deleteYn(BooleanFormatType.N)
.userId(user.getId())
.build();
Product entityProudct = productRepository.save(product);
/**
* 2. 썸네일 첨부파일 엔티티 생성 및 저장
*/
MultipartFile formThumbnailFile = form.getThumbnailFile();
UploadFile uploadFile = fileUtilities.storeFile(formThumbnailFile, PathConst.PRODUCT);
ProductUploadFile productThumbnailFile = ProductUploadFile.builder()
.product(entityProudct)
.uploadFileName(uploadFile.getUploadFileName())
.storeFileName(uploadFile.getStoreFileName())
.thumbnailYn(BooleanFormatType.Y)
.build();
uploadFileRepository.save(productThumbnailFile);
/**
* 3. 기타 첨부파일 엔티티 생성 및 저장
*/
List<MultipartFile> formUploadFiles = form.getProductUploadFiles();
if (formUploadFiles != null && !formUploadFiles.isEmpty()) {
List<UploadFile> uploadFiles = fileUtilities.storeFiles(formUploadFiles, PathConst.PRODUCT);
uploadFiles.stream()
.map(up -> {
ProductUploadFile file = ProductUploadFile.builder()
.product(entityProudct)
.uploadFileName(up.getUploadFileName())
.storeFileName(up.getStoreFileName())
.thumbnailYn(BooleanFormatType.N)
.build();
uploadFileRepository.save(file);
return null;
});
}
return entityProudct.getId();
}
결과를 보니 상품과 첨부 파일이 문제없이 저장된다.
그럼 무엇이 문제란 것인가!?
.
문제는 없다.
그럼 뭐냐!??!🤯
.
무언가.. JPA를 비효율적으로 사용하는 것 같다.
Repository의 save() 호출이 너무 많다.
아마 첨부파일의 개수만큼 호출될 것이다...
여기서, "상품만 저장하면 나머지 연관된 엔티티들이 자동으로 저장되게 할 순 없을까?"가 핵심이다.
Solution
Product.java
- CascadeType 은 ALL(
PERSIST, REMOVE, MERGE, REFRESH, DETACH
)로 설정
public class Product extends BaseTimeEntity {
//..
@Builder.Default
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
private Set<ProductUploadFile> productUploadFiles = new LinkedHashSet<>();
public void addProductUploadFiles(ProductUploadFile productUploadFile) {
productUploadFiles.add(productUploadFile);
productUploadFile.setProduct(this);
}
}
ProductService.java
@Transactional
public Long save(ProductDto.SaveRequest form, SessionUser user) throws IOException {
ProductCategory category = categoryRepository.findById(form.getProductCategory()).get();
/**
* 1. 상품 엔티티 생성
*/
Product product = Product.builder()
.productCategory(category)
.name(form.getName())
.contents(form.getContents())
.hits(0L)
.deleteYn(BooleanFormatType.N)
.userId(user.getId())
.build();
/**
* 2. 썸네일 첨부파일 엔티티 생성
*/
MultipartFile formThumbnailFile = form.getThumbnailFile();
UploadFile uploadFile = fileUtilities.storeFile(formThumbnailFile, PathConst.PRODUCT);
ProductUploadFile productThumbnailFile = ProductUploadFile.builder()
.uploadFileName(uploadFile.getUploadFileName())
.storeFileName(uploadFile.getStoreFileName())
.thumbnailYn(BooleanFormatType.Y)
.build();
product.addProductUploadFiles(productThumbnailFile);
/**
* 3. 기타 첨부파일 엔티티 생성
*/
List<MultipartFile> formUploadFiles = form.getProductUploadFiles();
if (formUploadFiles != null && !formUploadFiles.isEmpty()) {
List<UploadFile> uploadFiles = fileUtilities.storeFiles(formUploadFiles, PathConst.PRODUCT);
uploadFiles.stream()
.map(up -> {
ProductUploadFile puf = ProductUploadFile.builder()
.uploadFileName(up.getUploadFileName())
.storeFileName(up.getStoreFileName())
.thumbnailYn(BooleanFormatType.N)
.build();
product.addProductUploadFiles(productThumbnailFile);
return null;
});
}
/**
* 4. 상품 엔티티 저장
*/
return productRepository.save(product).getId();
}
기존 방법과 차이점이 느껴진다면 👏🏻👏🏻👏🏻~!!
기존에는 첨부파일 엔티티를 생성하자마자 바로 save() 호출을 했었다.
하지만, 우리는 JPA를 사용 중이지 않은가!!
Repository의 save() 호출이 많았었지만 이제 단 한 번만 호출하면 된다.
.
(0) @OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
- OneToMany 필드(productUploadFiles)의 cascade 타입을
PERSIST
상태로 설정 - 영속성 전이를 통해 연관된 엔티티도 함께 영속 상태로 만들기
(1) 상품 엔티티
생성
(2, 3) 첨부파일 엔티티
생성
- 연관관계 편의 메서드 addProductUploadFiles()를 통해 첨부파일, 상품 엔티티 양쪽에 모두 연관 엔티티 세팅
(4) 상품 엔티티만 레파지토리에 저장해주면 외래키로 매핑된 첨부파일 엔티티는 자동으로 저장
.
쿼리는 이전과 동일하게 동작한다.
Result
저장되는 쿼리는 동일하지만 조금 더 JPA 답게 코드를 작성할 수 있게 되었다.
어쩌면 정말 기본적인 부분일 수 있지만, 순간 머릿속이 하얘지면서 비효율적인 방향으로 코드를 작성하게 되었었다.
나의 작은 머리에 과부하가 온 것인가😢
.
다시 정신을 차리고 머리에 노크를 하면서 원리를 찾다 보니 다시 방법이 떠올라서 작성해 보았다.
노트북 비밀번호든.. 뭐든.. 잘 적어놓자..🤣
'Web > Spring' 카테고리의 다른 글
[Spring Batch] Spring Batch 가이드 보고 따라하기 (0) | 2022.08.17 |
---|---|
[Spring] @Transactional 잘 사용해보기 (0) | 2022.05.07 |
Spring Boot, Gradle 환경에 Querydsl 설정 방법 (2) | 2022.03.09 |
[JPA] JSON 직렬화 순환 참조 해결하기 (0) | 2022.03.09 |
Spring Boot 2.4 Config file processing (0) | 2022.02.11 |