지금까지 Book 이라는 table 만 가지고, GET/POST/PUT/DELETE 를 해볼 수 있는 RESTful을 만들어봤었다.
직전 게시글에서 전역 예외 처리에 대해서 공부했고,
이제는 Book 이라는 table 말고도 다른 table 도 만들어볼 것이고, 그 table 간의 관계 설정도 해볼 것이다.
책과 관련된 이미지를 업로드, 다운로드, 삭제 등을 RESTful API로 만들어볼 것이다.
Book 이라는 Entity 를 만들었었다. 이제는 BookImage 라는 Entity 를 만들것이다. (table 이 될 것)
책 하나에는 여러개의 책 이미지가 있을 수 있다. 교보문고 같은 책 판매 페이지를 보면, 알 수 있듯이 여러 개의 이미지가 등록되어 있을 수 있다. 그러므로 1:N 의 관계를 맺게 된다.
Book - @OneToMany / BookImage - @ManyToOne
책 이미지는 메인 화면에서 보여주는 작은 이미지, 썸네일 이미지가 있을 수 있다. (원본이미지를 스케일을 줄여서 보여주는 이미지가 썸네일)
예를 들어서, 책 목록이 여러개가 있는데 각각의 책을 대표하는 이미지가 있을 수 있다.
그 다음에, 거기서 책 하나를 선택을 해서 상세 페이지로 들어가게 되면 여러개의 이미지를 볼 수가 있다.
왼쪽, 오른쪽으로 넘겨서 볼 수 있는 이미지가 될 수 있다. 이걸 기본 이미지들이라고 하자. (어떤 곳에서는 위아래로 넘겨서 보는 이미지도 있었던 것 같긴 한데, 대부분은 왼쪽 오른쪽으로 넘겨서 볼 수 있는 이미지였다.)
그리고 나서 제품 상세 페이지의 이미지인데, 하나의 크~~은 이미지가 있을 수 있다.
그래서, 책 하나에는, 썸네일 이미지, 기본 이미지, 상세 이미지 이렇게 연결될 수가 있다. (thumb, basic, detail)
각각은 type = 1, type = 2, type = 3 이렇게 구분을 해서 DB 에 저장할 수가 있다.
그래서 1:N 의 관계가 될 것이다.
그러면 각각의 이미지를 업로드하려면 업로드에 대한 REST API 를 만들어주면 될 것이고,
브라우저에서 해당 이미지들을 보려면 이미지를 보여주는, 이미지 보기에 대한 REST API 도 만들것이고,
만약에 이미지를 삭제하려면 이미지 삭제에 대한 REST API 를 만들어볼 것이다.
originalFileName - 최초로 업로드할 때 만들어진 파일의 오리지널 이름
파일이 업로드되면 업로드 폴더에 이미지 이름이 중복이 될 수가 있다. 그래서 이름을 서버에서 중복이 안 될 수 있도록 바꿔주는데 그걸 fileName
그래서 실제로 서버에 올라갈 때는 fileName으로 올라갈 것이다. 그리고 나중에 다운로드를 하면 originalFileName 이 되어야 하니까 둘 다 사용할 것이다.
BookImage 는 Book 과 N:1 의 관계를 맺고 있다. 자식과 부모의 관계이므로, BookImage 는 FK 가 필요하다.
그래서 @ManyToOne 으로 다중성을 걸어준다.
이제 Entity가 만들어졌다.
업로드는 그러면 어디에 하는걸까?
기본적으로, static 이라는 폴더 아래에다가 uploads 폴더를 만들어서 거기다가 업로드를 할 것.
처음에 만들면, static.uploads 라고 보일텐데, Mark Directory as / Excluded 를 클릭하게 되면 저렇게 아래에 표시가 된다.
이제 저기다가 이미지를 어떻게 업로드할 것인지, 이미지 업로드, 파일 업로드는 여러 가지 방법이 많다.
여기서 하는 방법은 정석적으로 하나의 패턴이라고 생각해서 응용해보면 되겠다.
책의 id 별로 디렉토리를 자동으로 만들어서 저장되도록 할 것이다. 그래서 uploads 디렉토리 안에, 1, 2, 3, .... 이런식으로 책 id 별로 디렉토리가 만들어져서 이미지가 저장이 될 것이다. 그러면 1 디렉토리 안에는 type 1, 2, 3 의 이미지가 있을 것이다. (썸네일, 기본, 상세 이미지로) 나중에 view 단을 만들때 사용을 할 예정이다.
저렇게 저장이 되는건 프로그램에서 하는 것이다. 프로그램에서 uploads 폴더를 디렉토리 경로로 인식을 할 수 있도록 하기 위해서, application.yml 파일에다가 업로드 경로를 설정해놓을 것이다. 설정을 해놓으면, 프로그램에서 해당 경로를 읽어갈 수가 있다. 직접 프로그램(java 파일)에다가 해당 경로를 쓰는 것보다는 upload: path: 라는 키값을 접근을 해서 쓰는게 좋다.
키 값을 접근하는 방법은 프로그램에서 이렇게 하면 된다.
@Value("${upload.path}")
private String uploadPath;
@Value("${upload.path}") 라고 하면, application.yml 파일에서 경로 값(src/main/resources/static/uploads/)에 접근을 해서 그 경로값을 변수 (uploadPath) 에다가 담아서 사용할 수가 있다.
그리고 업로드를 할 때 용량에 대한 제한이 있을 수 있다.
너무 큰 용량이라면 서버에서 에러가 발생할 수 있기 때문에 마찬가지로 용량 제한을 걸어줄 수가 있다.
application.yml 에서 설정을 해놓을 수 있다.
파일 하나에 10MB 을 넘을 수 없도록 하고, 클라이언트가 여러개의 파일을 업로드할 수도 있는데 그럴 때 그 여러개의 파일의 크기의 합이 10MB 을 넘을 수 없도록 이렇게 설정을 할 수가 있다.
여기서, 경로 값(src/main/resources/static/uploads/)은 상대 경로이다.
절대 경로와 상대 경로의 차이는 간단하다.
절대 경로는 - D:드라이브, 또는 C:드라이브에서 부터 경로를 알려준거라면 절대 경로인것이다.
상대 경로는 - 우리 어플리케이션의 메인을 기준으로 해서, src/main/--> 이렇게 시작해서 경로를 알려준거라면 상대 경로인것이다.
어떤 회사의 경우에는 로컬 pc 에다가 이미지 파일을 저장할 수도 있을 것이고, 규모가 큰 회사의 경우에는 클라우드 저장소에다가 이미지 파일을 저장할 수도 있을 것이다. 그럴때마다 경로 설정 방법은 약간의 차이는 있겠지만 방법의 차이가 그렇게 크지는 않을 것이다.
Book 엔티티말고, BookImage 엔티티를 만들었고 BookImage 로 DB에 table 을 만들것이다. 그러면 거기에 들어갈 이미지를 업로드도 하고 다운로드도 하고 삭제도 하는 등의 REST API 를 만들것이다. 그래서 관련해서 프로그램을 만들 것이다. BookRestController 에다가 같이 REST API 를 만들 것. (따로 만들어도 된다.)
컨트롤러, 그렇다면 서비스, 레퍼지토리도 만들어야 한다. 그리고 이번에는 util 패키지와 ImageUtil 이라는 클래스를 정의한다.
레퍼지토리를 만들때에는, interface 라는 점, extends JpaRespository<> 한다는 점, 그리고 객체 정보와 해당 객체의 PK 의 data type 을 <> 에 넣어야 한다는 점.
서비스에서는 @Autowired 로 레퍼지토리 의존성 주입을 받는다. 여기서는 일단 저장을 할 것이기 때문에 save 메서드를 만들었다.
util 패키지에서 ImageUtil 에서,
업로드할 파일의 절대 경로를 만들어주는 메서드,
그리고 썸네일 이미지로 변환된 이미지를 반환해주는 메서드를 만들었고 import 해서 사용할 예정이다.
여기서, Scalr 의 경우에는 이미지 스케일 조정해주는 것으로, dependencies 에 입력해야 하는 것이 있다.
그리고나서 까먹지 말고, Controller 는 Service 를 의존성 주입 해서 사용하게 된다.
아래가, 업로드와 관련된 REST API 이다. 하나 하나씩 공부해볼 예정이다.
// 파일(이미지) 업로드 REST API
@Value("${upload.path}")
private String uploadPath;
// http://localhost:8081/{book_id}/{type}/upload/
@PostMapping(value = "/{book_id}/{type}/upload", consumes = {"multipart/form-data"})
public ResponseEntity<?> images_upload(@RequestPart(required = true) MultipartFile[] files,
@PathVariable Long book_id, @PathVariable int type){
Optional<Book> optionalBook = bookService.findById(book_id);
Book book;
if(optionalBook.isPresent()){
book=optionalBook.get();
}else{
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
List<String> successImageName = new ArrayList<>();
List<String> errorImageName = new ArrayList<>();
/*
int length = 10;
boolean useLetters=true;
boolean useNumbers=true;*/
// MultipartFile[] files 여기에서 파일 하나씩을 가져와서 작업을 해야한다
// MultipartFile: png, jpg, jpeg
Arrays.asList(files).stream().forEach(file->{
String contentType = file.getContentType();
if (contentType.equals("image/png")
|| contentType.equals("image/jpg")
|| contentType.equals("image/jpeg")){
// 정상적인 이미지인 경우 정보를 저장(List<String>)
successImageName.add(file.getOriginalFilename());
try{
String fileName = file.getOriginalFilename();
String generatedString = RandomStringUtils.random(10, true, true);
// 새로운 이미지 이름을 만든다.
String new_image_name = generatedString + fileName;
if (type==1){
new_image_name = "thumb_" + generatedString + fileName;
}
String absolute_fileLocation = ImageUtil.makePath(uploadPath, new_image_name, book_id);
System.out.println(absolute_fileLocation);
Path path = Paths.get(absolute_fileLocation);
if(type!=1){ // type = 2, 3
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
}
// 데이터베이스에 이미지 정보(BookImage)를 저장
BookImage bookImage = new BookImage();
bookImage.setOriginalFileName(fileName);
bookImage.setFileName(new_image_name);
bookImage.setBook(book); // 관계
bookImage.setType(type);
bookImageService.save(bookImage);
// 썸네일 이미지 경우 type = 1
if(type==1){
BufferedImage thumbnail = ImageUtil.getThumbnail(file, 300);
String thumbnail_location = ImageUtil.makePath(uploadPath, new_image_name, book_id);
// Files.copy(thumbnail.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
// ex) image/png <-- [1]으로 파일의 형식을 알 수 있음
ImageIO.write(thumbnail, file.getContentType().split("/")[1], new File(thumbnail_location));
}
}catch (Exception e) {
e.printStackTrace();
// 이미지가 아닌 경우 정보를 저장(List<String>)
errorImageName.add(file.getOriginalFilename());
}
}else{
// 이미지가 아닌 경우 정보를 저장(List<String>)
errorImageName.add(file.getOriginalFilename());
}
});
HashMap<String, List<String>> result = new HashMap<>();
result.put("SUCCESS", successImageName);
result.put("ERROR", errorImageName);
List<HashMap<String, List<String>>> response = new ArrayList<>();
response.add(result);
return ResponseEntity.ok(response);
}
일단, 아래는 질문 리스트이다.
질문 1. json 형태로 데이터를 주고 받을 때에는 @PostMapping 에서 "application/json" 이라고 해서, {} 기호가 없었는데, 여기서는 왜 {"multipart/form-data"} 라고 해서, { } 기호가 왜 있는 것인지?
질문 2. multipart/form-data 형식으로 전송된 데이터를 처리한다고 했는데 그게 어떤 형식의 데이터인 것인지?
질문 3. @RequestPart MultipartFile[] files 에 대해서 설명해줄래? @RequestPart 는 뭐고, MultipartFile 은 뭐고 [] 는 뭔지, 그리고 [] 는 <> 랑 차이가 뭔지.
질문 4. Arrays.asList(files).stream().forEach(file -> {
// 파일 하나씩 처리
});
여기에서도 마찬가지로 각각의 의미가 무엇인지 알려줄래?
질문 5. file.getContentType() 을 그냥 영어로 해석해보면, 내용물의 유형이 무엇인지 알려달라는 건데, 너가 MIME 타입을 확인한다고 말했거든? 그러면 MIME 타입이 뭘 말하는거야?
질문 5-1. 그리고 ContentType 이 png면 png, jpg 면 jpg 일것이지, 왜 image/png 인 건 뭐야? 그러면 만약 pdf 파일이라면, file.getContentType() 을 하면 어떻게 된다는거야?
질문 6. png, jpg, jpeg 가 각각 뭐고, 그 차이가 뭐야?
질문 7. 정상적인 이미지라면 successImageName 리스트에 추가를 할 건데, successImageName.add(file.getOriginalFilename()) 이라고 했는데, 여기서 add 까지는 이해가 되는데, file 에서 getOriginalFilename() 이라고 했는데, file 이 가지고 있는 정보가 뭐뭐가 있길래 이렇게 여러개의 get 메서드가 있는거야? 이 질문은 아마 질문 3의 MultipartFile 과 관련이 있을거 같네.
질문 8. RandomStringUtils.random() 에 대해서 좀 더 구체적으로 설명해줄래?
질문 9. String absolute_fileLocation = ImageUtil.makePath(uploadPath, new_image_name, book_id); 에 대해서 구체적으로 설명해줄래?
질문 10. Path 데이터 타입은 뭐야? 그래서 Path path = Paths.get(absolute_fileLocation); 에 대해서 구체적으로 설명해줄래?
질문 11. Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
type 가 2, 3 일때는 이렇게 했어. 이걸 하나하나씩 구체적으로 설명해줄래?
질문 11-1. 근데 type가 1 일 때에는 Files.copy(); 이렇게 하지 않고, ImageIO.write(thumbnail, file.getContentType().split("/")[1], new File(thumbnail_location)); 이렇게 했는데 왜 그런거지? 왜 다른건지?
질문 12. BufferedImage thumbnail = ImageUtil.getThumbnail(file, 300);
String thumbnail_location = ImageUtil.makePath(uploadPath, new_image_name, book_id);
이 부분은, util 패키지에서 ImageUtil.java 로 만들어놓은 것과 관련이 되어 있다. 해당 부분에 대해서 정말 상세하고 구체적으로, 하나하나씩 설명을 해줄래?
질문 13. ImageIO.write(thumbnail, file.getContentType().split("/")[1], new File(thumbnail_location));
이 부분에 대해서 정말 상세하고 구체적으로, 하나 하나 씩 다 설명을 해줄래?
질문 14. try catch 문에서, try 를 해봤는데 어떤 식으로든지 error 가 발생하면 catch 에서 error 를 말그대로 catch 해버리는 걸로 이해가 되는데, 맞아? printStackTrace() 가 뭘 의미하는거고 어떤 결과가 나오는거야?
질문 15. HashMap<> 이 뭐야? 어떤 데이터타입인거야? 그래서 HashMap<String, List<String>> 은 어떤 데이터 타입인거야? 그리고 List<HashMap<String, List<String>>> 은 어떻게 되는거야?
질문 15-1. 근데, HashMap 에서는 result.put 으로 해서 put 메서드인데, 왜 List<> 에서 response 의 경우에는 response.add 로 왜 add 메서드인거야? 차이가 뭐야?
너무 내용이 많아서, 여기서 잠깐 끊고, 다음 게시물로 넘어가서 작성하겠다.
'Spring Boot (+ RESTful)' 카테고리의 다른 글
RESTful 웹 서비스 구축 - # Project 04 - 이미지 업로드 (3) (0) | 2025.01.05 |
---|---|
RESTful 웹 서비스 구축 - # Project 04 - 이미지 업로드 (2) (0) | 2025.01.04 |
RESTful 웹 서비스 구축 - @ControllerAdvice / @ExceptionHandler (1) | 2025.01.02 |
RESTful 웹 서비스 구축 - @PutMapping / @DeleteMapping (0) | 2025.01.01 |
RESTful 웹 서비스 구축 - @GetMapping / responseCode (0) | 2024.12.31 |