실전 프로젝트(반려동물 용품 중고거래 서비스)를 진행하며 금주 내가 담당했던 부분은 아래와 같았다.
- Item CRUD 구현( + 이미지 다중업로드)
- 카테고리에 따른 게시글 조회 기능(단일 카테고리 / 이중 카테고리)
- 검색 기능(query를 바탕으로)
- 찜 기능(좋아요) / 내가 찜한 게시글 조회 기능
우선 기능적으로 구현을 완료해둔 다음, Spring Security를 작업하신 팀장님의 코드와 합치며 로직들을 수정(토큰을 통해 Member Class의 정보를 받아오게끔)하고 있었는데 Item 수정부분에서 문제가 발생하였고, 이를 정리해두려한다.
Item 수정(PUT METHOD) 버그 해결 트러블 슈팅
우선 해당 파트에서 발생하는 오류에 대해 설명하기 로직적으로 설명하자면,
Item(상품)을 등록할 때 이미지를 함께 등록할 수 있게끔 설계하였고, 이때의 이미지는 없을 수도 혹은 1장이거나 여러장일 수도 있게 구현해두었다. 하지만 이러한 이미지들은 추후 수정 및 삭제가 간편하도록 Item table과 Image table을 분리하여 저장하게끔 설계하여 Item table만을 조회했을 때는 해당 게시글에 등록된 이미지를 볼 수 없지만, 로직적으로 Response할 때 Item과 더불어 해당 Item의 Id 값을 FK값으로 가지고 있는 Image들도 모두 List로 불러오게끔 만들어두었다.
ItemService - 상품등록 파트
/*
ItemService 중
*/
/* 상품 등록 */
public ItemResponseDto createItem(UserDetails userDetails, ItemRequestDto itemRequestDto) throws IOException {
Item item = Item.builder()
.title(itemRequestDto.getTitle())
.content(itemRequestDto.getContent())
.nickname(((UserDetailsImpl) userDetails).getMember().getNickname())
.petCategory(itemRequestDto.getPetCategory())
.itemCategory(itemRequestDto.getItemCategory())
.location(itemRequestDto.getLocation())
.purchasePrice(itemRequestDto.getPurchasePrice())
.sellingPrice(itemRequestDto.getSellingPrice())
.build();
itemRepository.save(item);
/* itemRequestDto에서 받아온 값 중 item과 multipartFile을 따로 저장 */
List<MultipartFile> multipartFileList = itemRequestDto.getMultipartFileList();
if (multipartFileList != null) {
for (MultipartFile multipartFile : multipartFileList) {
String url = s3uploader.Uploader(multipartFile);
Image image = Image.builder()
.imgUrl(url)
.item(item)
.build();
imageRepository.save(image);
}
}
return buildItemResponseDto(userDetails, item); /* 공통작업 - ItemResponseDto build */
}
문제 상황
문제가 발생한 것은 바로 등록된 Item의 수정 부분이었다.
앞서 잠깐 언급했던 것과 같이 Item에 연결된 Image들도 쉽게 수정, 삭제가 가능하게끔 Item 수정 시, 해당 Item에 할당되어있는 이미지들을 모두 삭제되게끔 설계를 해두었었다. 그래서 게시글을 수정할 때, 다음과 같은 오류가 발생하는 것이 확인되었다.
- 이미지를 변경하여도 기존 등록하였던 이미지가 삭제되지 않고 기존 등록한 이미지 + 변경하려 추가한 이미지 모두 저장되었다.
- 이미지를 없애도록 게시글을 수정하면 이미지뿐만 아니라 해당 게시글까지도 삭제되어버렸다.
ItemService - 상품 수정 부분
/* 상품 수정 - detail */
@Transactional
public ItemResponseDto updateItem(UserDetails userDetails, Long itemId, ItemRequestDto itemRequestDto) throws IOException {
Item item = itemRepository.findById(itemId).orElseThrow(
() -> new IllegalArgumentException("조회하시려는 상품이 존재하지 않습니다.")
);
if (!item.getNickname().equals(((UserDetailsImpl) userDetails).getMember().getNickname())) {
throw new IllegalArgumentException("작성자가 일치하지 않습니다.");
}
item.update(itemRequestDto);
imageRepository.deleteAllByItemId(itemId);
List<MultipartFile> multipartFileList = itemRequestDto.getMultipartFileList();
if (multipartFileList != null) {
for (MultipartFile multipartFile : multipartFileList) {
String url = s3uploader.Uploader(multipartFile);
Image image = Image.builder()
.imgUrl(url)
.item(item)
.build();
imageRepository.save(image);
}
}
return buildItemResponseDto(userDetails, item);
}
ItemRepository
public interface ImageRepository extends JpaRepository<Image, Long> {
List<Image> findAllByItemId(Long itemId);
void deleteAllByItemId(Long itemId);
}
Item
/* Item Class - Image Class와의 연관관계 부분 */
@OneToMany(fetch = FetchType.LAZY, mappedBy="item", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Image> image;
Image
/* Image Class - Item Class와의 연관관계 부분 */
@JoinColumn(name = "item_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Item item;
문제원인
처음에는 ItemService 단에 작성한 로직이나 Item Entity와 Image Entity 간의 연관관계 문제인가 하여 순서들을 조정해 보았었다. 수정하려는 Item의 Id 값을 갖고 있는 Image가 삭제되는 시점을 다르게 설정을 해본다거나 Entity Class들을 확인해보며 MappedBy등 연관관계가 잘못 잡혀있지는 않은가 확인해보았지만 이유를 찾을 수 없었다.
그러던 중 1차원적으로 deleteAll이라 작성한 Reposiotry에서의 JPQL 부분때문에 deleteAll(Item과 Image)되었나는 생각에 native Query로 수정해보았다.
(원인을 파악한 지금 생각해보니 굉장히 부끄러운 추론과정이었다는 생각이 든다)
ImageRepository(nativeQuery로 수정)
@Modifying
@Query(nativeQuery = true, value =
"delete i from image i " +
"where i.item_id = :itemId")
void deleteAllByItemId(Long itemId);
이렇게 변경하니 당장 문제는 해결되었었다.
하지만 근본적인 원인은 알 수 없었다. 그렇담 왜 native query로 변경하기 전, JPQL일 때는 이미지를 추가하면 deleteAll이 작동하지 않았고 이미지를 추가하면 Item Class까지 삭제되었을까?
해답은 이미 위에 있었다.
문제해결
Image
/* Image Class - Item Class와의 연관관계 부분 */
@JoinColumn(name = "item_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Item item;
Item의 하위 Entity인 Image에 까지 CascadeType.ALL을 넣어주었던 것이 문제였다.
종속관계를 설정하는 CascadeType.ALL을 상위클래스인 Item에만 적는 것이 아닌 하위 클래스인 Image에까지 생각없이 습관적으로 넣어준 것이 문제를 발생시켰었던 것이다.
CascadeType.ALL에는 Remove도 포함되어있기에 Image Table이 삭제되니 Item도 하위클래스로 오인하여 삭제시켜 버렸던 것이다.
처음 고민했던, 연관관계 문제가 발생했던 것이다. 이 부분을 삭제시키고 다시금 JPQL로 원복시키니 에러없이 정상 작동 되었다,
Image(수정)
/* Image Entity - Item Entity와 연관관계 부분(수정) */
@JoinColumn(name = "item_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Item item;
결과
ImageRepository(원복)
public interface ImageRepository extends JpaRepository<Image, Long> {
List<Image> findAllByItemId(Long itemId);
void deleteAllByItemId(Long itemId);
}
ImageRepository 부분도 이전과 동일하게 원복시켜도 문제가 발생되지 않았다.
결국에는 가장 기본적인 테이블관의 연관관계를 제대로 가져가지 못해서 발생했던 문제였던 것이다. 너무 기초적인 실수였기에 뼈아프지만 이제라도 다시 기본적인 부분을 다져서 후에는 이런 실수를 반복하지 않을 수 있을 것이다.
'✍️개발로그' 카테고리의 다른 글
20220909_실전 프로젝트 14일차(최근 조회한 상품 목록) (0) | 2022.09.09 |
---|---|
20220909_실전 프로젝트 14일차(Swagger 도입) (0) | 2022.09.09 |
220829_실전프로젝트 4일차 (0) | 2022.08.29 |
20220823_클론코딩(항해인사이드) 5일차 (0) | 2022.08.23 |
20220820_클론코딩(항해인사이드) 2일차 (0) | 2022.08.20 |