
실습 진행 간 발생한 오류 정리 ✍
오늘부터 내일까지는 강사님이 제공해주시는 Swagger UI 문서에 따라 기능들을 구현하는 시간을 갖는다. 실습 진행 간 발생한 에러를 기록해두기 위해 이 글을 적는다.
에러는 상품을 생성할 때 발생했다. 상품을 생성할 때 이지지 파일을 여러개 등록하여 생성이 가능토록 구현하는 거였는데, 로컬 저장소에 구현했을때는 정상적으로 작동되었다.
하지만, 상품 이미지를 불러오기 위해 수업때 배운 AWS 클라우드를 활용하여, 클라우드 저장소에 이미지를 업로드 하고 클라우드 저장소에서 이미지를 불러오도록 구현하면 되는 거였다.
이미 지난 수업시간때 코드를 다 짜고, 작동 테스트까지 해봤기 때문에 손쉽게 구현할 줄 알았지만, 여기서 UncheckedIOException 에러가 내 발목을 잡았다. 분명 예전에는 정상적으로 동작한 코드를 그대로 사용하였는데 왜 에러가 발생하는지 이유를 모르겠었다.
먼저 에러가 발생한 코드는 상품서비스 클래스에서 AWS 클라우드 저장소에 이미지를 업로드 하는 부분에서 발생하였다. 코드는 아래와 같다.
💻 Product 엔티티
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer idx;
@OneToMany(mappedBy = "product")
private List<ProductImage> productImageList = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_idx")
private Member member;
private String name;
private Integer categoryIdx;
private Integer price;
private Integer salePrice;
private String deliveryType;
private String isTodayDeal;
private String contents;
}
💻 ProductImage 엔티티
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ProductImage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer idx;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_idx")
private Product product;
private String image;
}
💻 ProductController 클래스
@RestController
@RequiredArgsConstructor
@RequestMapping("/product")
@CrossOrigin("*")
public class ProductController {
private final ProductService productService;
@RequestMapping(method = RequestMethod.POST, value = "/create")
public ResponseEntity createProduct(@RequestPart PostProductReq postProductReq, @RequestPart MultipartFile[] uploadFiles) throws IOException {
ProductBaseRes productBaseRes = productService.createProduct(postProductReq, uploadFiles);
return ResponseEntity.ok().body(productBaseRes);
}
}
💻 ProductService 클래스
@Service
public class ProductService {
@Value("${project.upload.path}")
private String uploadPath;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private AmazonS3 s3;
private final ProductRepository productRepository;
private final ProductImageRepository productImageRepository;
public ProductService(AmazonS3 s3, ProductRepository productRepository, ProductImageRepository productImageRepository) {
this.s3 = s3;
this.productRepository = productRepository;
this.productImageRepository = productImageRepository;
}
public String makeFolder(){
String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String folderPath = str.replace("/", File.separator);
File uploadPathFolder = new File(uploadPath, folderPath);
if(uploadPathFolder.exists() == false) {
uploadPathFolder.mkdirs();
}
return folderPath;
}
public String saveFile(MultipartFile file) {
String originalName = file.getOriginalFilename();
String folderPath = makeFolder();
String uuid = UUID.randomUUID().toString();
String saveFileName = folderPath+ File.separator + uuid + "_" + originalName;
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
// 🔥에러가 발생한 부분🔥
s3.putObject(bucket, saveFileName.replace(File.separator, "/"), file.getInputStream(), metadata);
} catch (IOException e) {
throw new RuntimeException(e);
}
return s3.getUrl(bucket, saveFileName.replace(File.separator, "/")).toString();
}
public ProductBaseRes createProduct(PostProductReq postProductReq, MultipartFile[] uploadFiles) {
Member member = (Member) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Product product = Product.builder()
.member(member)
.name(postProductReq.getName())
.categoryIdx(postProductReq.getCategoryIdx())
.price(postProductReq.getPrice())
.salePrice(postProductReq.getSalePrice())
.deliveryType(postProductReq.getDeliveryType())
.isTodayDeal(postProductReq.getIsTodayDeal())
.contents(postProductReq.getContents())
.build();
// 상품 테이블에 저장
Product result = productRepository.save(product);
// 상품 이미지 테이블에 저장
for(MultipartFile multipartFile : uploadFiles) {
String saveFileName = saveFile(multipartFile);
productImageRepository.save(ProductImage.builder()
.product(result)
.image(saveFileName.replace(File.separator, "/"))
.build());
}
ProductBaseRes productBaseRes = ProductBaseRes.builder()
.isSuccess(true)
.code(1000)
.message("요청 성공")
.result(PostProductRes.builder()
.idx(result.getIdx())
.build())
.success(true)
.build();
return productBaseRes;
}
디버깅을 했을때, ProductService 클래스에서 표시한 부분 다음으로 넘어갈때 아래와 같은 에러가 등장했다.
java.io.UncheckedIOException: Cannot delete C:\Users\????\AppData\Local\Temp\tomcat.8080.8028046931035505689\work\Tomcat\localhost\ROOT\upload_9862159e_c81a_4c5d_a095_fdf656acb30a_00000011.tmp
구글링을 해보고, 챗 gpt에도 물어보고 하는데 정확하게 나와있는 정보가 없었다. 에러를 읽어보면 임시파일을 삭제할 수가 없다고 나오는데 임시파일은 무엇이고, 임시파일을 왜 삭제하지❓ 라는 의문만 들었다.
분명 나는 삭제하라는 코드를 작성한 적이 없는데 도통 원인을 몰랐었다.
그러다가 간신히 알아낸 방법이 있었다. 우리가 클라우드 저장소에 이미지를 업로드 하게되면 사실 로컬 저장소에는 더이상 이미지를 저장할 필요가 없다.
따라서 이것을 강제로 삭제할 수 있도록 아래와 같이 ProductService 클래스를 수정해줬다. 수정하고 나서 테스트해보니 정상적으로 클라우드 저장소에 이미지가 업로드 되는 것을 확인 할 수 있었다.
✅ 수정 부분
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentLength(file.getSize());
metadata.setContentType(file.getContentType());
s3.putObject(bucket, saveFileName.replace(File.separator, "/"), file.getInputStream(), metadata);
} catch (IOException e) {
throw new RuntimeException(e);
// 🔥 여기 부터 수정부분 🔥
} finally {
// 로컬 저장소에 파일이 남아 있으면 삭제
File localFile = new File(saveFileName);
if (localFile.exists()) {
localFile.delete();
}
}
// 🔥 또 다른 방법 : system.gc() 를 try 부분에 넣어주기 ( 가비지 컬렉션 삭제 )