메서드 기본 틀 잡기
가장 먼저 클라이언트가 업로드한 파일을 서버가 받을 수 있도록 출입구를 만듭니다.
Spring에서 클라이언트가 업로드한 파일은 MultipartFile 객체로 들어옵니다. 이는 메모리나 디스크에 임시 저장된 파일의 바이너리 데이터와 파일명 등의 메타데이터를 관리하는 spring 전용 interface입니다. 이 객체를 통해 파일의 실제 바이트 스트림에 접근할 수 있습니다.
파일 전송은 보안과 데이터 크기 문제로 통상 POST 방식을 사용합니다. 괄호 안의 annotation은 클라이언트가 file 이라는 이름으로 보낸 데이터를 자바의 MultipartFile 객체로 받아내겠다는 선언입니다.
@PostMapping("/upload-image")
public String uploadImage(@RequestParam("file") MultipartFile file) {
}
@PostMapping("/upload-image")
public String uploadImage(@RequestParam("file") MultipartFile file) {
String url = "http://localhost:8000/predict";
}
데이터 바디 구성하기
이제 통신을 위한 바디를 만듭니다.
HTTP 통신에서 이미지 파일을 전송할 때는 일반적인 JSON 형식을 사용할 수 없습니다. 반드시 multipart/form-data 규격을 사용해야 하는데, spring의 HttpMessageConverter는 HTTP Request 전송 시 이 규격을 조립하기 위해 MultiValueMap 타입의 객체를 요구합니다. File part와 text part를 boundary로 명확히 구분하여 담을 수 있는 최적의 자료구조이기 때문입니다.
따라서 일반 맵이 아닌 MultiValueMap을 생성하고, 클라이언트에게 받은 파일 객체에서 실제 data stream을 추출하여 상자에 담습니다.
단, MultiValueMap에 파일을 담을 때 MultipartFile 객체 자체를 그냥 넣으면, spring이 이를 전송 가능한 파일로 인식하지 못하고 에러를 발생시킵니다. 반드시 getResource() 메서드를 호출해서 byte stream과 파일명 정보가 포함된 resource 타입으로 변환한 뒤에 맵에 담아야 합니다. 그래야 네트워크를 탈 때 binary 데이터로 올바르게 직렬화됩니다.
@PostMapping("/upload-image")
public String uploadImage(@RequestParam("file") MultipartFile file) {
String url = "http://localhost:8000/predict";
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", file.getResource());
}
헤더 설정하기
바디가 완성되었으니 이 데이터가 multipart/form-data 형식임을 명시하는 헤더를 만듭니다.
파이썬 서버가 날아온 바이너리 데이터를 해석하려면 이 데이터가 어떤 형식인지 미리 알아야 합니다. HttpHeaders 객체를 생성하고 content-type을 multipart/form-data로 명시적으로 지정해 줍니다.
이 설정이 들어가야 파이썬 서버가 날아온 데이터를 단순 텍스트가 아닌 파일로 해석할 수 있습니다.
@PostMapping("/upload-image")
public String uploadImage(@RequestParam("file") MultipartFile file) {
String url = "http://localhost:8000/predict";
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", file.getResource());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
}
요청 엔티티 조립하기
준비된 바디와 헤더를 하나의 완전한 HTTP 요청 메시지로 합칩니다.
HTTP 통신은 기본적으로 헤더와 바디 두 부분으로 구성됩니다. 앞서 만든 파일이 담긴 맵을 바디로, 콘텐츠 타입이 지정된 객체를 헤더로 삼아 하나의 HttpEntity 객체로 묶어줍니다.
이처럼 HttpEntity 객체에 두 요소를 집어넣어 네트워크를 탈 수 있는 전송 준비를 마칩니다.
@PostMapping("/upload-image")
public String uploadImage(@RequestParam("file") MultipartFile file) {
String url = "http://localhost:8000/predict";
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", file.getResource());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
}
@PostMapping("/upload-image")
public String uploadImage(@RequestParam("file") MultipartFile file) {
String url = "http://localhost:8000/predict";
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("file", file.getResource());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
return restTemplate.postForObject(url, requestEntity, String.class);
}
기본 전송 방식의 한계
일반적으로 웹에서 데이터를 서버로 보낼 때는 JSON이나 application/x-www-form-urlencoded 방식을 사용합니다. 이런 데이터는 모두 String이라는 동일한 형태를 띄고 있어서, HTTP Request Body 안에 하나의 덩어리로 묶어서 보내도 서버가 쉽게 Parsing 할 수 있습니다.
Text와 Binary 데이터의 충돌
하지만 게시판에 글을 쓰면서 이미지 파일을 같이 첨부하는 상황은 완전히 다릅니다. 글 제목은 Text 데이터이고, 이미지는 0과 1로 이루어진 Binary 데이터입니다. 성격이 전혀 다른 두 데이터를 HTTP Body 하나에 뭉뚱그려 넣으면, 서버 입장에서는 어디까지가 String이고 어디서부터가 이미지 파일인지 해독할 수 없어 에러가 발생합니다.
Part를 나누는 Multipart 방식
이러한 데이터 충돌을 해결하기 위해 만들어진 HTTP 웹 표준 규격이 바로 multipart/form-data 입니다. 이름 그대로 하나의 HTTP Request를 여러 개의 독립적인 Part로 쪼개서 통신한다는 의미입니다.
Boundary를 통한 구역 분리
Multipart 규격을 사용하면 전송하는 데이터들 사이에 Boundary라는 복잡한 무작위 문자열로 물리적인 경계선을 칩니다. 첫 번째 Boundary 구역에는 String 데이터를 넣고, 두 번째 Boundary 구역에는 이미지 파일을 넣는 방식입니다. 중요하게 볼 점은 각 Part가 자신만의 개별 Header를 가질 수 있다는 것입니다. 덕분에 이 구역은 image/jpeg Content-Type이고, 저 구역은 text/plain 타입이라고 서버에게 명확히 알려줄 수 있습니다.
Spring Boot 코드와의 연결
앞서 Java 코드에서 HttpHeaders 객체에 MediaType.MULTIPART_FORM_DATA 를 명시한 이유가 바로 이것입니다. 이 설정을 추가해야 Spring 내부의 HttpMessageConverter가 네트워크로 데이터를 쏘기 전에 알아서 Boundary를 생성하고 구역을 나누어 줍니다. 이 데이터를 수신하는 FastAPI 서버 역시 request header의 multipart/form-data 명시를 확인하고, Boundary 경계선을 따라 파일 객체만 정확하게 추출해 낼 수 있게 됩니다.