관리자 페이지에서 판매할 상품을 등록한다. 엔티티와 함께 등록 로직도 다 구현했으니 이제 사진을 업로드 해야 한다. 우선 구조를 생각해보자.
사진은 서버에 저장되어야 하며, 경로를 DB에 저장해야 한다. -> MultipartFile 사용 : Spring의 파일 업로드 인터페이스
한 상품 객체에 여러 사진 경로가 저장될 수 있어야 한다. -> 상품 엔티티에 사진 경로 리스트 필드
리스트 필드는 OneToMany로 구현해야 한다. -> 썸네일 엔티티가 별개로 필요하다.
정리하면 이미지 업로드는
- 사진을 저장할 엔티티(테이블)을 만든다.
- MultipartFile 인터페이스로 구현한다.
- 상품과 썸네일은 일대다 (1:N) 관계
엔티티 설정
ProductThumbnail 엔티티 생성
사진을 저장할 엔티티를 생성한다. 이는 상품 엔티티를 참조한다.
// productThumbnail 엔티티
@Getter
@Setter
@Entity
@NoArgsConstructor // 디폴트 생성자
@Table(name = "product_thumbnails")
public class ProductThumbnail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "thumbnail_id")
private Long thumbnailId; // PK
@Column(name = "image_path", nullable = false)
private String imagePath; // 사진 경로
@ManyToOne
@JoinColumn(name = "product_id", nullable = false)
private Product product; // FK
@Column(name = "created_at", nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private LocalDateTime createdAt;
// 생성자
public ProductThumbnail(Product product, String imagePath) {
this.product = product;
this.imagePath = imagePath;
this.createdAt = LocalDateTime.now();
}
}
Product 엔티티에 매핑
// Product 엔티티에 ProductThumbnail 리스트 필드 추가
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProductThumbnail> productThumbnails = new ArrayList<>();
상품이 삭제되면 사진도 삭제되어야 하므로 영속성 전이를 설정해주고 ->CascadeType.ALL
고아 객체를 관리하기 위해 orphanRemoval
을 true로 설정해준다.
Repository 인터페이스 생성
package PU.pushop.productThumbnail.repository;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductThumbnailRepositoryV1 extends JpaRepository<ProductThumbnail, Long> {
}
JpaRepository를 상속받은 JPA 리포지토리 생성
사진 등록하기
Thumbnail Service 클래스
ThumbnailService
클래스를 생성하고 uploadThumbnail
메서드 작성
상품 생성에서 만든 Product 객체와 MultipartFile 타입 리스트를 파라미터로 받을 거다. 그리고 먼저 사진을 저장할 경로를 생성. 클라이언트에서 경로로 접근해 파일을 찾을 때 static 아래에서 찾으므로 해당 경로로 설정한다.
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class ProductThumbnailServiceV1 {
private final ProductThumbnailRepositoryV1 productThumbnailRepository;
/**
* 썸네일 등록
* @param productId
* @param images
*/
public void uploadThumbnail(Product product, List<MultipartFile> images) {
try {
// 이미지 파일 저장을 위한 경로 설정
String uploadsDir = "src/main/resources/static/uploads/thumbnails/";
}
이제 이미지 파일 리스트를 잘라서 이름을 생성하고 경로를 저장한다. 이는 saveImage
메서드에서 진행한다.
// 이미지 파일을 저장하는 메서드
private String saveImage(MultipartFile image, String uploadsDir) throws IOException {
// 파일 이름 생성
String fileName = UUID.randomUUID().toString().replace("-", "") + "_" + image.getOriginalFilename();
// 실제 파일이 저장될 경로
String filePath = uploadsDir + fileName;
// DB에 저장할 경로 문자열
String dbFilePath = "/uploads/thumbnails/" + fileName;
Path path = Paths.get(filePath); // Path 객체 생성
Files.createDirectories(path.getParent()); // 디렉토리 생성
Files.write(path, image.getBytes()); // 디렉토리에 파일 저장
return dbFilePath;
}
- 사진 파일 이름 생성 -
fileName
- 같은 이름의 파일을 업로드 하게 되면 덮어쓰게 된다.
- 따라서 랜덤 UUID 값을 생성해 기존 파일 이름과 합쳐 새 파일 이름을 생성했다.
- 파일 경로 설정 및 DB 저장
- 실제로 파일이 저장되는 경로 : 위에서 설정한 경로 + 파일 이름 -
filePath
- DB에 저장할 경로 : static 이후로 자른 경로 -
dbFilePath
- 실제로 파일이 저장되는 경로 : 위에서 설정한 경로 + 파일 이름 -
- 설정한
filePath
에 사진을 저장하고, DB 저장을 위해dbFilePath
반환
Path는 Java NIO 패키지에서 제공하는 클래스로, 말 그대로 파일 경로에 쓴다.Paths.get()
메서드로 Path 객체를 생성하고, 주어진 문자열 경로를 Path로 변환한다.Files.createDirectories
로 저장할 디렉토리를 생성하고,Files.write()
메서드로 해당 디렉토리에 저장한다.
그리고 다시 uploadThumbnail
서비스 메서드로 돌아와서 위에서 받아온 상품 객체, 경로(dbFilePath
)를 DB에 저장
// 이미지 파일 경로를 저장
String dbFilePath = saveImage(image, uploadsDir);
// ProductThumbnail 엔티티 생성 및 저장
ProductThumbnail thumbnail = new ProductThumbnail(product, dbFilePath);
productThumbnailRepository.save(thumbnail);
사진 업로드 전체 Service 코드
// import 생략
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class ProductThumbnailServiceV1 {
private final ProductThumbnailRepositoryV1 productThumbnailRepository;
private final ProductRepositoryV1 productRepository;
/**
* 썸네일 등록
* @param productId
* @param images
*/
public void uploadThumbnail(Product product, List<MultipartFile> images) {
try {
// 이미지 파일 저장을 위한 경로 설정
String uploadsDir = "src/main/resources/static/uploads/thumbnails/";
// 각 이미지 파일에 대해 업로드 및 DB 저장 수행
for (MultipartFile image : images) {
// 이미지 파일 경로를 저장
String dbFilePath = saveImage(image, uploadsDir);
// ProductThumbnail 엔티티 생성 및 저장
ProductThumbnail thumbnail = new ProductThumbnail(product, dbFilePath);
productThumbnailRepository.save(thumbnail);
}
} catch (IOException e) {
// 파일 저장 중 오류가 발생한 경우 처리
e.printStackTrace();
}
}
// 이미지 파일을 저장하는 메서드
private String saveImage(MultipartFile image, String uploadsDir) throws IOException {
// 파일 이름 생성
String fileName = UUID.randomUUID().toString().replace("-", "") + "_" + image.getOriginalFilename();
// 실제 파일이 저장될 경로
String filePath = uploadsDir + fileName;
// DB에 저장할 경로 문자열
String dbFilePath = "/uploads/thumbnails/" + fileName;
Path path = Paths.get(filePath); // Path 객체 생성
Files.createDirectories(path.getParent()); // 디렉토리 생성
Files.write(path, image.getBytes()); // 디렉토리에 파일 저장
return dbFilePath;
}
}
Product Service 클래스
상품 등록시 사진을 함께 업로드하기 위해 이 서비스 메서드는 상품을 등록하는 서비스 메서드에서 사용했다.
상품 등록 메서드 파라미터에 List<MultipartFile> 파라미터를 추가해주고, 상품 등록 후 생성된 상품 객체를 가져와서 이미지를 저장해야 한다.
// Product Service 클래스
/**
* 상품 등록
* @param requestDto
* @return productId
*/
public Long createProduct(ProductCreateDto requestDto, List<MultipartFile> images) {
if (requestDto.getPrice() < 0) {
throw new IllegalArgumentException("가격은 0 이상이어야 합니다.");
}
// DTO를 엔티티로 매핑
Product product = modelMapper.map(requestDto, Product.class);
productRepositoryV1.save(product);
// 추가 - 썸네일 저장 메서드 실행
productThumbnailService.uploadThumbnail(product, images);
return product.getProductId();
}
Product Controller 클래스
이제 상품 서비스 메서드를 호출할 컨트롤러 클래스의 메서드를 수정해주면 된다.
// ProductController
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Slf4j
public class ProductApiControllerV1 {
private final ProductServiceV1 productServiceV1;
/**
* 상품 등록
* @param requestDto 사용자에게 받아온 정보
* @return productId, productName, price
*/
@PostMapping("/products/new")
public ResponseEntity<String> createProduct(@Valid @RequestParam("images") List<MultipartFile> images, @ModelAttribute ProductCreateDto requestDto) {
Long productId = productServiceV1.createProduct(requestDto, images); // 저장한 상품의 pk
return ResponseEntity.status(HttpStatus.CREATED).body("상품 등록 완료. Id : " + productId);
}
createProduct 메서드 수정
- 파일을 받아와야 하므로 Request 형식을 Form Data로 변경
List<MultipartFile> images
파라미터 추가@RequestBody
->@ModelAttribute
- 상품 등록 서비스 메서드를 실행할 때 파라미터에 이미지 넣어 보냄
상품 리스트 Dto 수정
상품 리스트를 가져올 때 사진도 가져오기 위해 DTO에 썸네일 경로 리스트를 추가했다.
// ProductListDto
// import 생략
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductListDto {
private Long productId;
private ProductType productType;
private String productName;
private Integer price;
private LocalDateTime createdAt;
private Long wishListCount;
private Boolean isDiscount;
private Integer discountRate;
private Boolean isRecommend;
private List<String> productThumbnails; // 썸네일 경로 리스트 추가
public ProductListDto(Product product) {
this(
product.getProductId(),
product.getProductType(),
product.getProductName(),
product.getPrice(),
product.getCreatedAt(),
product.getWishListCount(),
product.getIsDiscount(),
product.getDiscountRate(),
product.getIsRecommend(),
product.getProductThumbnails().stream().map(ProductThumbnail::getImagePath).toList() // 경로만 가져오기
);
}
}
반환 확인
상품 생성하는 엔드포인트에 FormData 형식으로 사진과 필요한 필드를 보내면
등록 완료 반환값이 잘 나오고, DB에 들어가서 id 내림차순으로 보면
이렇게 상품이 생성되어 있고,
해당하는 product_id
에 내가 업로드한 사진이 들어간다.
그리고 상품 리스트를 조회해본다. 조건별 상품 리스트 조회 api를 사용해 시간 내림차순(최신순) 으로 보면,
가장 최근에 등록한 상품에 사진 경로 리스트까지 잘 저장되어있다.
GitHub 댓글