쇼핑몰의 게시글을 올릴때 여러장의 사진을 한번에 올리려면 어떤걸로 받아야할까?
저번 프로젝트에는 하나의 이미지만 업로드가 가능했는데 이번에는 여러 이미지를 업로드가 가능하게끔 할 생각이라 엔티티에서는 List
를 사용할 생각이었다. 그럼 여러 이미지를 어떻게 받나? 라는건 MultipartFile
을 사용할 생각이다.
컨트롤러에 매핑하고 db에 저장하는 과정을 생각해보자
public class Product {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE, fetch = FetchType.LAZY)
@OrderBy("id asc")
private List<UploadFile> uploadFiles = new ArrayList<>();
...
}
public class UploadFile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="product_id")
private Product product;
...
}
@Service
@RequiredArgsConstructor
public class UploadFileServiceImpl implements UploadFileService {
final private UploadFileRepository uploadFileRepository;
@Value("${image.upload.directory}")
String imageUploadDirectory;
@Override
public UploadFile saveUploadFile(String imageUrl, Product product) {
UploadFile uploadFile = UploadFile.builder()
.imageUrl(imageUrl)
.product(product)
.build();
return uploadFileRepository.save(uploadFile);
}
@Override
public UploadFileDTO saveImageFile(MultipartFile file, Product product) {
// UUID 생성
String uuid = UUID.randomUUID().toString();
// 파일 확장자 추출
String originalFilename = file.getOriginalFilename();
// UUID를 파일 이름에 추가하여 저장
String savedFilename = uuid + "_" + originalFilename;
try {
// 이미지 파일 저장
Path uploadPath = Paths.get(imageUploadDirectory, savedFilename);
Files.copy(file.getInputStream(), uploadPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("이미지 파일을 저장하는 중에 오류가 발생했습니다.", e);
}
// 저장된 이미지 파일의 경로를 imageUrl로 설정하여 업로드 파일 저장
String imageUrl = imageUploadDirectory + "/" + savedFilename;
// UploadFile 엔티티를 생성하고 저장
UploadFile uploadFile = saveUploadFile(imageUrl, product);
return new UploadFileDTO(uploadFile.getImageUrl());
}
}
처음엔 db에서 파일 관리가 편하게 Product
라는 엔티티와 UploadFile
이라는 엔티티를 나눠서 받게끔했는데 다시 게시글을 조회하기위해서 UploadFile
의 imageUrl
을 불러와야하지만 UploadFile
은 Product
에 종속된 관계(1:N)이기때문에 원칙을 준수하면서 서비스로직을 구현하기가 까다로웠다.
설명하자면, 서비스상단에서는 Product
에 대한 정보를 UploadFile
의 서비스로직이 알아야하기때문에 컨트롤러부분에서 로직의 매개변수를 얻어야한다. 예를들어 saveUploadFile()
같은 메서드를 정의해 Product
의 식별 키와 이미지 경로에 대한 정보를 받아서 UploadFile
에 넣어줘야한다. 이럴거면 굳이 나눠야할까? 라는생각에 그냥 UplaodFile
엔티티를 과감히 삭제하기로 했다.
만약 테이블의 규모가 크다면 고민해볼 문제지만 현재로서는 과하게 역할을 나눈 느낌이 들었기때문이다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
// 여러 이미지 URL을 저장하는 리스트
@ElementCollection
private List<String> imageUrls = new ArrayList<>();
@Builder
public Product(String productName, String category, String writerName, int price, String description) {
this.productName = productName;
this.category = category;
this.writerName = writerName;
this.price = price;
this.description = description;
}
// 게시글에 이미지 URL 추가
public void addImageUrl(String imageUrl) {
this.imageUrls.add(imageUrl);
}
}
@Getter @Setter
@NoArgsConstructor
public class ProductDTO {
private String productName;
private String category;
private String writerName;
private int price;
private String description;
private List<String> imageUrls;
}
URL 정보를 리스트로 둔다음 Builder 패턴과 addImageUrl
을 통해 엔티티의 등록이 이루어진다.
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService{
final private ProductRepository productRepository;
@Value("${image.upload.directory}")
String imageUploadDirectory;
@Override
@Transactional
public void uploadProduct(ProductDTO productDTO, MultipartFile[] imageFiles) {
if (productDTO == null) {
throw new IllegalArgumentException("상품정보가 유효하지 않습니다.");
}
// ProductDTO로부터 상품 정보를 추출하여 Product 엔티티를 생성
Product product = Product.builder()
.productName(productDTO.getProductName())
.category(productDTO.getCategory())
.writerName(productDTO.getWriterName())
.price(productDTO.getPrice())
.description(productDTO.getDescription())
.build();
// 이미지 파일 처리
if (imageFiles != null && imageFiles.length > 0) {
for (MultipartFile imageFile : imageFiles) {
// 각 이미지 파일을 저장하고 저장된 파일의 URL을 추출하여 Product 엔티티에 추가
String savedImageUrl = saveImageFile(imageFile);
product.addImageUrl(savedImageUrl);
}
}
// 상품을 저장
productRepository.save(product);
}
private String saveImageFile(MultipartFile imageFile) {
// UUID를 사용하여 파일 이름 생성
String uuid = UUID.randomUUID().toString();
String fileExtension = imageFile.getOriginalFilename().substring(imageFile.getOriginalFilename().lastIndexOf("."));
String savedFilename = uuid + fileExtension;
// 이미지 파일을 저장할 경로 설정
Path uploadPath = Paths.get(imageUploadDirectory, savedFilename);
// 이미지 파일 저장
try {
Files.copy(imageFile.getInputStream(), uploadPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("이미지 파일을 저장하는 중에 오류가 발생했습니다.", e);
}
// 저장된 이미지 파일의 경로를 반환
return savedFilename;
}
}
@PostMapping("/upload")
public String uploadProduct(@Valid @ModelAttribute("productDTO") ProductDTO productDTO,
@RequestParam("imageFiles") MultipartFile[] imageFiles,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "product/uploadForm";
}
// 이미지 파일들과 함께 상품 정보를 서비스로 전달하여 상품을 업로드
productService.uploadProduct(productDTO, imageFiles);
return "redirect:/";
}
대략적으로 구조를 설명해보자면
Entity, DTO : 게시글, 이미지정보 담당
Service : uuid로 생성된 이미지를 로컬에 저장하고 db에 url을 넣음
Controller : ProductDTO와 MultipartFile로 게시글과 파일정보를 받고 서비스로직으로 전달하여 처리
이미지를 두개 이상 선택하고 Post하면?
잘 들어가는 모습