상품등록 서비스를 만들 때 이미지 업로드 기능이 필요하다.
상품 등록에 대한 요구사항은 아래와 같다.
요구사항
- 상품 정보 입력 (상품명, 가격, 수량, 상품상태, 상품 카테고리 등등)
- 이미지 파일 여러 개를 등록할 수 있다.
- 업로드한 이미지를 웹 브라우저에서 확인할 수 있다.
스프링은 MultipartFile 이라는 인터페이스로 멀티파트 파일을 매우 편리하게 지원한다.
스프링 MVC를 활용하여 구현해보자.
application.properties 작성
file.dir=C:/newThing/chimm/
file.dir
( file.dir ) 이 부분을 원하는 이름으로 적고 원하는 파일저장경로를 설정해주면 된다.
후에 @Value("${file.dir}")로 불러다가 사용할 예정이다.
상품 등록 폼화면 작성
<form role="form" method="post" enctype="multipart/form-data" th:object="${itemFormDto}">
- form 태그의 enctype="multipart/form-data"로 설정을 해주어야 한다.
- input 태그의 multiple="multiple" 은 원래 type="file"의 input은 기본적으로 하나의 파일만 선택 가능한데 위의 속성을 적용해주면 여러 개의 파일을 선택할 수 있도록 해준다.
이미지 파일 정보를 담을 Entity를 작성
- imgName :사용자가 업로드한 파일명
- originImgName :서버 내부에서 관리하는 파일명
- savePath :서버 내부에서 관리하는 파일 경로
- isMainImg :상품 대표 이미지를 설정한다.
ItemImg 엔티티와 Item는 연관관계다.
ItemImg와 Imtem을 join()해서 하나의 상풍 객체로 만들어 사용할 예정이다.
ItemImg와 Item을 join()해서 페이징 구현할 것이다.
파일 정보를 DB에 저장할 Repository 작성
Spring Data JPA를 이용한다.
public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
List<ItemImg> findAllByItem_id(Long id);
ItemImg findByImgName(String imgName);
}
업로드 파일 정보 보관할 DTO 작성
@Data
public class FileInfoDto {
private String imgName;
private String originImgName;
private String savePath;
public void updateItemImg(String originImgName, String imgName, String savePath) {
this.originImgName = originImgName;
this.imgName = imgName;
this.savePath = savePath;
}
}
- imgName :사용자가 업로드한 파일명
- originImgName :서버 내부에서 관리하는 파일명
- savePath :서버 내부에서 관리하는 파일 경로
사용자가 업로드한 파일명으로 서버 내부에 파일을 저장하면 안된다.
왜냐하면 서로 다른 사용자가 같은 파일이름을 업로드 하는 경우 기존 파일 이름과 충돌이 날 수 있다.
서버에서는 저장할 파일명이 겹치지 않도록 내부에서 관리하는 별도의 파일명이 필요하다.
Multipart로 넘어온 파일을 처리해 줄 Service 작성
파일을 서버에 내부에서 관리하기 위해, 별도의 파일명을 생성해준다.
@Service
@Slf4j
public class FileService {
@Value("${file.dir}")
private String fileDir;
public String getFullPath(String filename) {
return fileDir + filename;
}
public FileInfoDto storeFile(MultipartFile multipartFile) throws IOException {
if (multipartFile.isEmpty()) {
return null;
}
// 원래 파일명 추출
String originalFilename = multipartFile.getOriginalFilename();
// uuid 저장파일명 생성
String savedFileName = createStoreFileName(originalFilename);
// 파일을 불러올 때 사용할 파일 경로 (예: /file:/users/.../nameh8787bghh33.png)
String savedFilePath = fileDir + savedFileName;
FileInfoDto fileInfo = new FileInfoDto();
fileInfo.updateItemImg(originalFilename,savedFileName,savedFilePath);
// 실제로 로컬에 uuid 파일명으로 저장하기
multipartFile.transferTo(new File(savedFilePath));
log.info("fileInfo={}", fileInfo.getSavePath());
return fileInfo;
}
// uuid 파일명 생성 메서드
private String createStoreFileName(String originalFilename) {
String fileExtension = getFileExtension(originalFilename);
String uuid = UUID.randomUUID().toString();
//uuid.확장자 로 만들기
String savedFileName = uuid + "." + fileExtension;
return savedFileName;
}
// 확장자 추출 메서드
private String getFileExtension(String originalFilename) {
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
return extension;
}
}
storeFile()
:multipartFile를 받아, fileInfo(상품 이미지 정보 DTO)로 반환해준다.createStoreFileName()
:서버 내부에서 관리하는 파일명은 유일한 이름을 생성하는 UUID 를 사용해서 충돌하지 않도록 한다.getFileExtension()
:확장자를 별도로 추출해서 서버 내부에서 관리하는 파일명에도 붙여준다.
예를 들어서 사용자가 a.png 라는 이름으로 업로드 하면 51041c62-86e4-4274-801d-614a7d994edb.png 와 같이 저장한다.
상품 등록해주는 Service
이제 fileService에서 만든 별도의 파일명과 경로 정보를 가지고 ItemImgService에서 ItemImg 엔티티를 만들어야 한다.
Item 엔티티는 ItemFormDto로 받은 정보를 가지고 만들어야 한다.
ItemService의 saveItem() 로직은 아래와 같다.
- itemFormDto.toEntity() :ItemFormDto로 Item 엔티티를 만든다.
- item.setUpUser(user) :어떤 사용자가 상품을 등록했는지 적어둔다.
- itemRepository.save(item) :Item 엔티티를 저장한다.
- multipartFileList의 첫번째 파일을 ItemImg 엔티티의 대표 이미지로 설정한다.
- ItemInfoDto에 item 엔티티 정보와 대표 이미지 구별정보를 넣어, for()문을 통해 ItemImg 엔티티를 만들어 저장한다.
ItemImgService의 saveItemImg()코드는 아래와 같다,
대표 이미지 구별정보, item 엔티티 정보, 저장 파일명과 경로를 넣어 ItemImg 엔티티를 만들어 저장한다.
Controller를 작성
ItemController 코드
- @GetMapping("/item/new") :상품 등록폼을 보여준다.
- @PostMapping("/item/new") :폼의 데이터를 저장하고, 보여주는 화면으로 리다이렉트 한다.
- @GetMapping("/item/{itemId}") :상품을 보여준다.
이미지를 다중 업로드 하기 위해 MultipartFile를 사용했다. 상품 정보와 이미지를 저장하는 코드는 아래와 같다.
@Validated @ModelAttribute ItemFormDto itemFormDto
@ModelAttribute를 사용하여 ItemFormDto를 받는다. itemFormDto 객체로 매핑받은 정보로 Item 엔티티를 만든다.@RequestParam("itemImgFile") List<MultipartFile> itemImgFileList
이미지 파일을 받는다.
@RequestParam과 @ModelAttribute의 눈에 띄는 차이점은, 파라미터의 타입으로 "1개를 얻는지?" "객체의 타입을 받는지?"로 나뉜다. ModelAttribute를 사용하면, 폼 형태(form)의 HTTP Body와 요청 파라미터들을 객체에 바인딩시키는 장점이 있다.
@GetMapping("/images/{filename}") :HomeController에 작성했었다.
태그로 이미지를 조회할 때 사용한다.
UrlResource로 이미지 파일을 읽어서 @ResponseBody 로 이미지 바이너리를 반환한다.
View 작성
첨부 파일은 링크로 걸어두고, 이미지는 태그를 반복해서 출력한다.
테스트 코드
파일 업로드 같은 경우에는 스프링의 MockMVC를 사용해야 한다. 테스트 해보자
MockMultipartFile 생성자를 사용하여 "image/jpg" 파일을 만든 다음, List<>로 묶는다.
MockMultipartFile로 만든 파일로 ItemService가 잘 동작되는지 확인해보았다.
실행화면
실행해보면 여러 이미지 파일을 한번에 업로드 할 수 있다.
사용자가 상품입력하는 과정
상품 등록 버튼을 눌렀을 때
후기
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 (인프런 강의)를 많이 참고했었다.
Item과 ItemImg 엔티티 설계를 많이 고민했었는데 Img 정보를 Item 엔티티에 몰아 넣기에는 복잡한 느낌이 들어서,
Item과 Img 엔티티를 나누어 설계했었다.
그리고 MultipartFile 테스트 코드 부분을 더 추가해보면 좋겠다고 느꼈다.
Testing a Spring Multipart POST Request 글을 참고해보자.
'프로젝트 > 개인 프로젝트 V2' 카테고리의 다른 글
8) 개선사항과 만났던 오류들 (0) | 2023.06.14 |
---|---|
7) 오류처리와 예외처리 (0) | 2023.06.14 |
5) Paging Query (1) | 2023.06.13 |
4) Enum 활용 (0) | 2023.06.13 |
2) 도메인 모델과 테이블 설계 (0) | 2023.06.13 |
댓글