https://preparingfor.tistory.com/6
저번 글에 이어서 이제는 구상하였던 이미지 저장/조회 서버를 코드로 옮겨보려고 합니다.
이미지를 Controller로 받아오기 위해서 이미지는 무슨 데이터 타입으로 받아와야하는지 알아봐야했습니다.
찾아보니 Spring에서는 MultipartFile 타입으로 이미지를 받아온다고 합니다.
MultipartFile type
MultipartFile은 Spring Framework에서 제공하는 복잡한 파일 업로드 과정을 추상화한 인터페이스입니다.
MultipartFile은 다음과 같은 메서드를 지원하는데요
getBytes(): 파일의 내용을 바이트 배열로 반환합니다.
getContentType(): 파일의 MIME 타입을 문자열로 반환합니다.
getInputStream(): 파일 내용을 읽기 위한 InputStream을 반환합니다.
getName(): 파라미터의 이름을 반환합니다.
getOriginalFilename(): 클라이언트에서 업로드한 파일의 실제 이름을 반환합니다.
getSize(): 파일의 크기(바이트 단위)를 반환합니다.
isEmpty(): 파일이 비어있는지 여부를 반환합니다.
transferTo(File dest): 업로드한 파일을 지정된 파일로 저장합니다.
우리가 파일을 어떻게 가져오는지 내부 코드를 알지 못해도 간편하게 파일을 받을 수 있게 해줍니다.
특히 multipartFile은 이름처럼 이미지나 텍스트파일이나 종류에 상관없이 받아올 수 있는데요
이를 spring이 아닌 java로만 코딩한다면 이미지와 텍스트 파일을 따로 처리해주어야 합니다.
이미지는 byte 단위이고 텍스트 파일은 char 단위이기 때문에 처리 방법이 다르기 때문이죠.
(java에서 char는 2byte입니다.)
실제로 java 코드로 파일을 받아오려면 어떻게 해야할까요?
MultipartFile을 안쓰고 이미지를 받아오려면..
MultipartFile은 파일을 메모리에 전부 로딩하지않고 스트림을 통해 직접 파일 시스템으로 저장을 하는데요
이게 무슨말일까요?
우리 Java Application의 자체 메모리는 그렇게 크지 않습니다.
하지만 갑자기 커다란 크기의 파일이 넘어와서 그걸 코드 상의 byte[] 배열에 모두 옮긴다면 어떻게 될까요?
OutOfMemoryError가 뜨면서 프로그램이 죽을 수도 있겠죠
그래서 Java는 한번에 파일을 읽어오지 않고 받을 수 있을 만큼의 적은 양만 임시 버퍼에 옮긴 후 파일 시스템에 전달합니다.
(위 그림처럼 말이죠)
이를 간단하게 java 코드로 작성하면 아래와 같습니다.
public class FileServer {
public static void main(String[] args) throws Exception {
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("서버가 포트 8080에서 실행 중입니다...");
try (Socket clientSocket = serverSocket.accept();
InputStream inputStream = clientSocket.getInputStream();
FileOutputStream fileOutputStream = new FileOutputStream("received_file.txt")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, bytesRead);
}
System.out.println("파일이 성공적으로 저장되었습니다.");
}
}
}
}
MultipartFile은 이 과정을 추상화해서 개발자들이 편하게 파일을 받아올 수 있게 해주는 것이죠.
이제 본격적으로 이미지를 받아오는 코드를 작성해봅시다
Controller 코드 작성
MultipartFile 타입으로 이미지를 받는 아래와 같은 Controller를 작성하였습니다.
@PostMapping("/upload")
@Operation(summary = "이미지 여러장 보내기",
requestBody = @RequestBody(content = @Content(mediaType = "multipart/form-data")))
public ResponseEntity<String> uploadFiles(@RequestParam("files") MultipartFile[] files) {
for (MultipartFile file : files) {
try {
fileService.uploadFile(file);
} catch (IOException e) {
e.printStackTrace();
return new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
}
}
return ResponseEntity.ok("Files uploaded successfully");
}
(@Operation 어노테이션은 swagger docs를 위한 코드니 신경쓰지 않으셔도 됩니다.)
@RequestParam을 통해 Http request 파라미터를 읽어오고 있는데요.
Http Request 파라미터
주로 URL의 query string에 포함되거나, Post 요청의 본문(body)에 담겨 전송됩니다.
여기서 'files'라는 필드 값은 어떤 걸 의미하는 걸까요?
client에서 서버에 보내는 Http Request 패킷을 살펴보겠습니다.
POST /api/files/upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 12345
Connection: keep-alive
...
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="files"; filename="example1.jpg"
Content-Type: image/jpeg
(여기에 example1.jpg 파일의 바이너리 데이터가 들어갑니다.)
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="files"; filename="example2.jpg"
Content-Type: image/jpeg
(여기에 example2.jpg 파일의 바이너리 데이터가 들어갑니다.)
----WebKitFormBoundary7MA4YWxkTrZu0gW--
Http 패킷의 본문에 form-data들 중 name="files"인 것들이 보이네요
그럼 @RequestParam("files") MultipartFile[] files 는 name 필드가 files인 것만 받는 것을 의미하겠습니다.
이처럼 multipartFile을 잘 받아와서 FileService에 넘기도록 코드를 구성했습니다.
Service 코드 작성
Service에서는 받은 이미지를 S3로 넘겨주는 코드를 작성해보겠습니다
(DataBase에 주소를 저장하는 것은 member Id와 이미지를 엮어야하는데 member 도메인 구현을 아직 하지 않았기 때문에 추후에..)
@Service
@RequiredArgsConstructor
public class FileService {
private final AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
public void uploadFile(MultipartFile file) throws IOException {
File fileObj = convertMultiPartFileToFile(file);
// 파일을 amazonS3에 업로드합니다.
amazonS3.putObject(new PutObjectRequest(bucketName, file.getOriginalFilename(), fileObj));
fileObj.delete(); // To clean up the temporary file
}
private File convertMultiPartFileToFile(MultipartFile file) throws IOException {
// 프로젝트의 루트 디렉토리 경로를 가져옵니다.
String rootPath = System.getProperty("user.dir");
// 업로드된 파일의 원본 이름을 가져옵니다.
String originalFileName = file.getOriginalFilename();
// 파일을 저장할 전체 경로를 구성합니다.
String fullPath = rootPath + File.separator + (originalFileName != null ? originalFileName : "defaultFileName");
File convFile = new File(fullPath);
// MultipartFile의 내용을 새로 생성된 File 객체로 복사합니다.
file.transferTo(convFile);
return convFile;
}
}
이미지를 저장하고 S3에 업로드를 하는 코드를 작성하였습니다.
테스트로 저희 프로젝트 로고를 보내보았는데요
s3 버킷에도 저희 프로젝트의 로고가 잘 들어가네요
(s3를 spring boot에 연결하는 것은 추후에 기회가 된다면 올려보도록 하겠습니다. 캡처할게 너무 많은...)
이제 이미지를 받아와서 s3에 저장하는 코드를 간단하게라도 짰구나 이제 제대로 보강을 해볼까 하는 찰나..
Github를 둘러보던 중 신기한 코드를 보았다..!
깃허브에서 다른 분들은 S3 이미지에 저장을 어떻게 하고 있는지 둘러보고 있던 중 아래와 같은 코드를 발견하였습니다.
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/api/v1/images")
public class ImageController {
private final S3Service s3Service;
/**
* Presigned-url 발급
*/
@GetMapping("/presigned-url")
public CustomResponseEntity<ImageResponse> getPresignedUrl(@RequestParam("fileName") String fileName) {
return CustomResponseEntity.success(new ImageResponse(s3Service.getPreSignedUrl(fileName)));
}
}
음..? 이미지를 받아야하는데 String으로 파일 이름만 받고 끝이네..?
Presigned-url을 발급한다구요?
그게 먼데요..?
간단하게 요약하자면
file을 굳이 서버를 거쳐 s3로 보내야되나라는 고민을 해볼 수 있는데
실제로 s3에 url을 미리 발급해서 client가 직접 s3에 파일을 업로드할 수 있게 하는 것 입니다.
예전에 파일 저장 서버를 구상할 때 대용량은 서버를 거쳐서 저장하면 서버가 힘들것 같아
프론트에서 직접 s3에 저장시키면 되지 않을까? 하는 생각을 한적이 있었는데요..
그냥 이미지도 그렇게 하면 되잖아..?
역시 선배들은 다 이런고민을 하셨군요
다음 시간에는 presigned-url 방식으로 이미지를 저장하는 방법을 알아보겠습니다.
https://preparingfor.tistory.com/8
'spring' 카테고리의 다른 글
[spring security] 왜 500번 에러도 401(Unauthorized)에러가 될까? (0) | 2024.04.07 |
---|---|
이미지 저장/조회 서버 만들기(3) - AWS Presigned URL 이미지 업로드 (0) | 2024.02.26 |
이미지 저장/조회 서버 만들기(1) - 저장 어디에..? (0) | 2024.02.11 |
SpringBoot + JPA + postgreSQL 프로젝트를 docker를 이용해 fly.io로 배포해보자 - 1부 (0) | 2023.01.20 |
SpringBoot+JPA 프로젝트에 mySQL를 적용해보자 (0) | 2022.09.30 |