항해99 3기

[TIL] 2021.11.04 최종 프로젝트 진행중

na_o 2021. 11. 4. 15:38
728x90

현재 MultiPartFile을 이용한 게시물 수정 시 이미지 변경 로직을 짜고 있다

S3에 업로드하는 메소드에서 'MultipartFile -> File convert fail' 이라는 것이 떴다

이 부분에서 에러가 난다

요청은 아래 사진처럼 했다

수정하는 이미지의 위치를 맞춰주기 위해 imageUrl과 image에서 아무것도 입력 안한 것이 있다

게시물 작성 때 사진을 2개 올리고, 두번째 위치에 있는 사진을 수정하려고 한다

그래서 두번째 위치에 있던 기존 이미지 URL은 빈칸으로 두고,

image의 두번째 위치에 새로 올린 이미지파일을 첨부해줬다(춘식3.png)

첫번째 사진은 수정을 안할거기 때문에 첫번째 imageUrl에 기존 이미지 링크를 적어뒀고,

첫번째 image는 빈칸으로 두었다

 

디버그를 찍어보니 빈칸으로 request한 것들은 null이 아니었다

imageUrl은 null이 아닌 "",

image는 null이 아니고, filename에서 "" 로 들어가있었다

참고로 requestDto는 이렇게 생김

imageUrl이나 image가 null일 때 로직을 실행하도록 만들어놨기 때문에 위와 같은 에러가 났던 것이다


슬랙에 질문을 남김

 

  • 현재 하려는것: 업로드한 이미지들을 수정하기 위해, 먼저 기존 RECIPE_IMAGE테이블에서 특정 Recipe에 해당하는 이미지들을 모두 삭제를 하고 새로운 사진을 INSERT하고자 합니다.
  • 구체적인 문제상황:
  • RECIPE_IMAGE테이블에 RECIPE_ID가 1에 해당하는 row들이 5개가 있음(총 5장의 사진이 등록된 상황).
  • RECIPE_ID가 1인 게시물에 새 사진 2장으로 수정하려고 함
  • 실행해보면 기존 사진이 삭제가 되지 않고(delete쿼리가 날라가지 않음), insert만 되어서 사진이 총 7장이 쌓임.
  • 이미지수정메서드 코드
@Transactional 
public Recipe updateRecipe(Long recipeId,PostRecipeRequestDto requestDto, UserDetailsImpl userDetails) throws IOException { 
    //게시글 존재여부확인 
    Recipe recipe = recipeRepository.findById(recipeId).orElseThrow(()->new CustomErrorException("해당 게시물을 찾을 수 없습니다")); 
    //S3에 있는 사진 삭제 
    for(int i=0; i<recipe.getRecipeImagesList().size();i++){ 
    	RecipeImage recipeImage = recipe.getRecipeImagesList().get(i); 
        if(recipeImage!= null) deleteS3(recipeImage.getImage()); 
    } 
    //S3에 이미지 업로드 
    List<String> imageUrlList= uploadManyImagesToS3(requestDto, "recipeImage"); 
    //DB의 recipe_image 기존 row들 삭제(그냥 update하면 더 작은 개수로 image업뎃할때 outOfInedex에러남) 
    recipeImageRepository.deleteAllByRecipe(recipe); 
    List<RecipeImage> recipeImageList = new ArrayList<>(); 
    imageUrlList.forEach((image)->recipeImageList.add(new RecipeImage(image,recipe))); 
    recipeImageRepository.saveAll(recipeImageList); return recipe; 
}
  • 해결:
    • 기존 Recipe엔티티:
      • 케스케이드 옵션을 가지고 있음. (참조무결성오류 방지)
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL)
@JsonBackReference 
private List<RecipeImage> recipeImagesList;

 

  •  기존 RecipeImage엔티티:

 

@Getter 
@NoArgsConstructor 
@Entity 
public class RecipeImage extends BaseEntity { 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    @Column(name = "image_id") 
    private Long id; 
    
    private String image; 
    
    @ManyToOne(fetch = FetchType.LAZY) 
    @JsonManagedReference 
    @JoinColumn(name = "recipe_id") 
    private Recipe recipe;
  • 수정한 엔티티:
    • 다음처럼(cascade = CascadeType.REMOVE) 수정하니 해결이 되었습니다.(사실은 cascade옵션 아무것도 안 써도 해당문제는 해결은 되나, 참조무결성때문에 넣었음)
@OneToMany(mappedBy = "recipe", cascade = CascadeType.REMOVE) 
@JsonBackReference 
private List<RecipeImage> recipeImagesList;

 

 

  • 궁금한것:
    • 블로그들을 짜집해서 이해한결과,, CascadeType.ALL로 설정하면 트랜잭션 끝날때(flush하는거 맞죠?) recipeImageList에 있는 recipeImage에 대해서  persist연산을 수행하기 때문에 delete쿼리가 작동하지 않는 것 같긴 한데요, 이해가 잘 되지 않습니다....

일단 CasacadeType에 대한 정확한 이해가 없이는 사용하는 것을 저는 비추천 합니다!! (사이드 이펙트가 많이 발생할거에요!!) 해당 문제도 마찬가지이구요!!

 

일단 저의 뇌피셜은 하나의 transaction에서 remove와 insert가 같이 진행되는 것으로 보입니다!! 그러면 영속성 컨텍스트에서 이미지들이 지워지지 않았기 때문에 여전히 남아있습니다!!  그리고 다시 신규 이미지를 두 개 넣어버리면 7개가 들어갑니다!!!

 

즉, 하나의 트랜잭션에서 관리가 2가지 작업이 일어나고 있기 때문에 이러한 현상이 발생한 것인데요!! 영속성 컨텍스트에서 이미지들을 없애야 이런 장애가 발생하지 않을 것입니다. 그래서 remove를 하거나 아무것도 적용하지 않았을 때 정상작동하는 것이구요!!

 

그래서 저는 이럴 때는 orphanremoval를 사용하는 것을 보다 추천드립니다!!  + 양방향 관계를 하지 않아도 이런 문제가 없을 것으로 보입니다!!

 

아래의 글을 읽어보시는 것을  추천드립니다!!

https://joont92.github.io/jpa/CascadeType-PERSIST%EB%A5%BC-%ED%95%A8%EB%B6%80%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%95%88%EB%90%98%EB%8A%94-%EC%9D%B4%EC%9C%A0/

 

[jpa] CascadeType.PERSIST를 함부로 사용하면 안되는 이유

엔티티의 자식에 CascadeType.PERSIST를 지정할 경우 JPA에서 추가적으로 수행하는 동작이 있고, 이 때문에 예상치 못한 사이드 이펙트가 발생할 수 있으므로 이를 남겨두고자 한다. 일단 기본적으로 c

joont92.github.io

 


 

나도 위의 내용에 해당된다

어차피 질문한 사람이 같은 팀원이기도 하고, 나와 저 팀원분은 결국 같은 게시판 기능을 만들고 있기 때문이다

사용자가 게시물 작성 시 여러 장의 이미지를 올리고, 게시물 수정 시 여러장 이미지 중 하나만 삭제하고 수정 완료를 한다면, DB에서 한 행의 데이터를 삭제해야 한다

그런데, 나도 위의 내용처럼 CascadeType.ALL이 걸려있다

@Getter
@NoArgsConstructor
@Entity
public class BoardImage extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "image_id")
    private Long id;

    private int location;

    private String image;

    @ManyToOne(fetch = FetchType.LAZY)
    @JsonManagedReference
    @JoinColumn(name = "board_id")
    private Board board;
//Board.java
    @OneToMany(mappedBy = "board", cascade = CascadeType.ALL)
    @JsonBackReference
    private List<BoardImage> boardImageList;

현재 CascadeType.ALL 때문에 한 행이 삭제가 되지 않은 채 "수정완료"라는 response를 받았다

 


추가로 질문이 있는데요..! 그럼 서로 참조를 해야만 양방향 관계를 설정해야 하는가에 대한 질문입니다.
서로 참조를 하지 않기에 단방향으로만 했을 경우, 예를들어 레시피-댓글 관계가 있다 치면요..!


레시피-댓글 관계에서 단방향으로만 관계를 설정한 경우엔 1번 레시피를 삭제 한경우 1번 레시피에 대한 댓글들도 함께 삭제되지 않으면 DataIntegrityViolationException 이 발생하는데요..!


이런 경우를 위해서라도 양방향으로 설정하고 cascadetype 또는 orphanRemoval = true 를 설정해줘야 하지 않을까요?? 만약 그렇지 않으면 해당 댓글들을 삭제해주는 쿼리를 또 작성해줘야 할 것 같아서요..! 어떤 방법을 더 선호하시나요??

 


서로 참조를 해야만 양방향 관계를 설정해야하나?? 라는 질문에는 정답이 없습니다!! 어떤 방식을 사용하던 잘 사용하는 것이 중요하다고 생각합니다!!!그리고 해당 문제는 다음과 같은 옵션들이 있다고 생각합니다!!!

  1. 정책적으로 댓글이 달린  레시피는 사용자 마음대로 삭제할 수 없다 라는 정책을 가진다.
  2. soft delete를 한다.
  3. orphanRemoval를 사용한다.(고아 객체 제거)

이중에서 가장 많이 사용되는 방식이 soft delete를 하는 방법을 가장 많이 사용하는 것으로 알고 있습니다!!

(Soft Delete는 delete 쿼리를 날리지 않고 flag 값으로 처리하는 것을 의미)

댓글들 같은 경우 보통  여러가지 법적 이유도 있고해서 DB에서 완전히 삭제를 하지 않습니다.

(일정시간이 지난후 삭제)

 

* 고아객제(orphan): 연관관계가 끊어진 자식 엔티티 


MultipartFile을 Spring에서 다룰 때

request로 들어온 파일의 이름을 알아내고 싶었음 : getName()

파일의 크기 : getSize()

 

https://ggoreb.tistory.com/73

 

[Spring] 파일 업로드

[xxxx-servlet.xml] 정확하게 위와 같이 지정해 준다 [write.jsp] 제목 파일첨부 JSP 코딩을 할때 당연한 소리지만 항상 이름들은 꼭 대소문자를 구분해서 맞춰줘야 한다 뷰 페이지에서 id, file 이라고 지

ggoreb.tistory.com

 


[Page<Entity> -> Page<Dto>로 변환하는 방법]

 

https://velog.io/@junho918/Spring-Data-Jpa-JPA..%EA%B7%B8%EB%9E%98-%EC%95%8C%EA%B2%A0%EC%96%B4..-%EA%B7%B8%EB%9E%98%EC%84%9C-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%93%B0%EB%8A%94%EB%8D%B0

 

[Spring Data Jpa] JPA..그래 알겠어.. 그래서 스프링 데이터 JPA는 어떻게 쓰는데..

김영한님의 실전 Spring Data Jpa를 수강하고 정리한 문서 입니다. 강추! 최고최고. 인프런 김영한님 실전 스프링 데이터 JPA 링크딴거는 다 모르겠고, 기록할 부분만 기록해야겠다.\-h2 데이터베이스

velog.io

 

페이지를 유지하면서 엔티티를 DTO로 변환하기!

Page<Member> page = memberRepository.findByAge(10,pageRequest);

Page<MemberDto> dtoPage = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));

위의 코드를 보고 나는 이렇게 사용했다

Page<Board> boardList = boardRepository.findAll(pageable);

       Page<GetBoardResponseDto> responseDtoList = boardList.map(board -> new GetBoardResponseDto(
                board.getId(), board.getUser().getNickname(), board.getTitle(), board.getContent(),
                board.getBoardImageList().get(0).getImage(), board.getRegDate(), board.getBoardCommentList().size(),
                board.getBoardLikesList().size(), board.getBoardLikesList().contains(currentLoginUser)
       ));
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class GetBoardResponseDto {
    private Long boardId;
    private String nickname;
    private String title;
    private String content;
    private String image;
    private LocalDateTime regDate;
    private int commentCount;
    private int likeCount;
    private boolean likeStatus;
}

 

response가 내가 원하는대로 왔다..!(작성한 날짜 최신순 정렬 && 한 페이지에 10개씩 출력 && 1 페이지)