-
6월 30일 금요일 TIL 회고록카테고리 없음 2023. 7. 1. 01:00
어제 multipartFile을 사용했었는데, multipartFile이 뭔지 적으면 좋을 것 같아 적어보려고 한다.
Multipart란?
- 웹 클라이언트가 요청을 보낼 때, HTTP 프로토콜의 바디 부분에 데이터를 여러 부분으로 나눠서 보내는 것이다.
- 웹 클라이언트가 서버에게 파일을 업로드할 때, HTTP 프로토콜의 바디 부분에 파일 정보를 담아서 전송하는데, 파일을 한번에 여러개 전송을 하면 Body 부분에 파일이 여러개의 부분으로 연결되어 전송된다. 이렇게 여러부분으로 나뉘어서 전송되는 것을 Multipart Data라고 한다.
- 보통 파일을 전송할 때 사용한다.
Multipart/form-data란?
- 일반적으로 폼 데이터를 전송하면 application/x-www-form-urlencoded 의 형식으로 전송된다.
- HTTP body에 바로 전송하고자 하는 데이터가 들어가는 형태이다.
- 예시로 name=bae&age=25 와 같은 key-value쌍이 body에 들어가는 것이다.
- 이렇게 동일한 타입의 문자 데이터를 전송하는 것은 전혀 무리가 가지 않는다.
- Key-Value 형태의 문자 데이터와 바이너리 형태의 파일 데이터가 함께 전송되는 것은 다르다.
- application/x-www-form-urlencoded 타입으로는 전송이 어렵다.
- 여기서 multipart/form-data로 지정되고 정해진 형식에 따라 메시지를 인코딩해 전송한다.
- 이를 처리하기 위해 서버는 멀티파트 메시지에 대해서 각 파트별로 분리하여 개별 파일의 정보를 얻게 된다.
- 이미지 파일로 문자로 이루어져 있어 HTTP Request Body에 담아 서버로 전송한다.
MultipartResolver
- MultipartResolver의 경우 사용자의 파일 업로드 요청에 대한 처리를 하는 인터페이스이다.
- MultipartResolver의 경우에는 개발자가 별도의 Bean을 등록하지 않는다고 해도 별도로 Spring에서 등록해 주지 않는다. 하지만 Spring Boot를 사용한다면 기본 구현체가 등록이 된다.
- Spring MVC에서는 파일 업로드 처리 시 DispatcherServlet에서 사용할 MultipartResolver의 Bean을 등록을 해주어야 한다.
MultipartFile 이란?
사용자가 업로드한 File을 핸들러에서 손쉽게 다룰 수 있게 도와주는 매개변수 중 하나이다.
매개변수를 사용하기 위해서는 MultipartResolver Bean이 등록되어 있어야 한다.
이는 SpringBoot에서는 자동 등록을 지원하지만, Spring MVC에서 기본으로 등록해주지 않으므로 꼭 확인해야 한다.
- MultipartFile 인터페이스는 스프링에서 업로드 한 파일을 표현할 때 사용되는 인터페이스이다.
- MultipartFile 인터페이스를 이용해서 업로드한 파일의 이름, 실제 데이터, 파일 크기 등을 구할 수 있다.
메소드 설명 String getName() 파라미터 이름을 구한다. String getOriginalFilename() 업로드한 파일의 이름을 구한다. String isEmpty() 업로드한 파일이 존재하지 않는 경우 True를 리턴한다. long getSize() 업로드한 파일 크기를 구한다. byte[] getBytes() throws IOException 업로드한 파일 데이터를 구한다. inputStream getInputStream() throws IOException 업로드한 파일 데이터를 읽어오는 InputStream을 구한다.
InputStream의 사용이 끝나면 알맞게 종료해주어야 한다.void transfer To(File dest) throws IOException 업로드한 파일 데이터를 지정한 파일에 저장한다. File 클래스란?
- 기본적이면서도 가장 많이 사용되는 입출력 대상이기 때문에 중요하다.
- 자바에서는 File 클래스를 통해서 파일과 디렉토리를 다룰 수 있도록 하고있다.
- 그래서 File 인스턴스는 파일 일 수도 있고 디렉토리 일 수도 있다.
415 에러 해결
1. 에러 메시지
Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/octet-stream' not supported]
2. 원인
게시글 작성 api가 올바르게 실행되는지 포스트맨으로 테스트를 하려고 했더니 415 에러가 출력되며 게시글 작성이 되지 않았다.
3. 해결
클라이언트에서 header에 content-type으로 'application/json'을 추가해서 보내지 않아서 생긴 오류였다. 그래서 위의 이미지 같이 별도로 content-type에 'application/json' 추가해서 테스트를 진행하면 된다.
오늘 추가 및 수정한 api
1. upload 메서드 코드 리팩토링, 게시글 작성 메서드 코드 리팩토링
원래 게시글을 작성할 때 이미지가 있으면 for문을 사용해 이미지를 가져와 BoardImageRepository에 save 메서드에 저장시켰는데,
굳이 게시글 api에서 할 필요 없이 upload 메서드에서 저장시키면 좋을 것 같아 코드를 수정했다.
upload 메서드
// BoardImage 엔티티 생성 BoardImage boardImage = BoardImage.builder() .originalName(multipartFile.getOriginalFilename()) .imagePath(path + File.separator + newFileName) .fileSize(multipartFile.getSize()) .board(basicBoard) .build(); // 생성 후 리스트에 추가 uploadImagePaths.add(boardImage); // 업로드 한 파일 데이터를 지정한 파일에 저장 file = new File(absolutePath + path + File.separator + newFileName); multipartFile.transferTo(file); // 반복문을 사용해 이미지를 가져온 후 boardImageRepository에 저장 for (BoardImage boardImage1 : uploadImagePaths) { boardImageRepository.save(boardImage1); }
BoardImage 엔티티를 생성할 때 board도 받게한 후 반복문을 사용해 boardImagaRepository에 저장했다.
게시글 작성 메서드
// List<BoardImage> boardImageList = upload(multipartFiles, basicBoard); // // // 파일이 존재할 때에만 처리 // if (!boardImageList.isEmpty()) { // for (BoardImage boardImage : boardImageList) { // // 파일을 DB에 저장 // basicBoard.addBoardImage(boardImageRepository.save(boardImage)); // } // } // 이미지가 존재하면 이미지 업로드 if (multipartFiles != null) { upload(multipartFiles, basicBoard); }
upload 메서드에서 이미지를 저장시켜주니 게시글 작성 메서드에는 if문으로 이미지가 존재하면 upload 메서드를 호출시키기만 하면 알아서 이미지를 저장시켜준다. (코드가 깔끔해졌다.)
2. 게시글 수정 및 삭제 시 이미지 삭제 or 추가 되도록 코드 리팩토링
게시글 수정을 할 때 이미 이미지가 있으면 그 이미지를 삭제하고 수정을 할 때 이미지도 같이 추가를 하면 업로드가 되도록 만들었다.
deleteBoardImages 메서드
// 보드 이미지 삭제 public void deleteBoardImages(Long boardId) { // boardImageRepository.findAllByBoardId()를 사용해 이미지를 가져온다. List<BoardImage> boardImages = boardImageRepository.findAllByBoardId(boardId); // 이미지를 담을 imagePaths 리스트를 만들었다. List<String> imagePaths = new ArrayList<>(); // 반복문을 사용해 imagePaths 리스트에 boardImage.getImagePath()를 추가한다. for (BoardImage boardImage : boardImages) { imagePaths.add(boardImage.getImagePath()); } // 반복문을 사용, boardImageRepository.deleteByImagePath()를 사용해 이미지를 삭제한다. for (String imagePath : imagePaths) { boardImageRepository.deleteByImagePath(imagePath); } boardImageRepository.deleteAllByBoardId(boardId); }
boardImageRepository
public interface BoardImageRepository extends JpaRepository<BoardImage, Long> { List<BoardImage> findAllByBoardId(Long boardId); @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("delete from BoardImage b where b.imagePath in :imagePath") void deleteByImagePath(@Param("imagePath") String imagePath); @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("delete from BoardImage b where b.board.id in :boardId") void deleteAllByBoardId(@Param("boardId") Long boardId); }
@Query 어노테이션을 사용해서 쿼리문을 작성했다.
게시물 수정, 삭제 메서드에 deleteBoardImages 메서드를 추가했다.
게시글 수정 메서드
if (multipartFiles != null) { deleteBoardImages(boardId); upload(multipartFiles, basicBoard); }
multipartFiles가 null이 아니면 원래 있었던 이미지를 삭제한 후 새로 추가한 이미지를 업로드한다.
게시글 삭제 메서드
deleteBoardImages(boardId);
게시글을 삭제하는 기능이므로 따로 if문으로 검증이 필요하지 않을 것 같아서 if문은 넣지 않았다.
실행 결과
1. 게시글 작성
포스트맨
h2 console
게시글 작성 및 이미지 업로드가 정상적으로 잘 되었다.
2. 게시글 수정
포스트맨
h2 console
게시글 수정도 잘 되었고, 원래 있던 이미지 파일들이 삭제되고 새로운 이미지 파일이 업로드되었다.
3. 게시글 삭제
포스트맨
삭'재'뭐야.. ㅠㅠㅠ h2 console
게시글과 이미지 전부 삭제되었다.
다음 목표
프론트엔드에 백엔드 api 연동(?) 시키기..