REST API Proxy 서버 개발 - 이미지 전달
Proxy 서버란? (클라이언트-서버-서버)
프록시 서버(영어: proxy server 프록시 서버[*])는 클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해 주는 컴퓨터 시스템이나 응용 프로그램을 가리킨다. 서버와 클라이언트 사이에 중계기로서 대리로 통신을 수행하는 것을 가리켜 '프록시', 그 중계 기능을 하는 것을 프록시 서버라고 부른다.
출처: wikipedia
서버 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 뒤에서 볼 수 있는 오페라 서버 랙 서버(영어: server, 문화어: 봉사기)는 클라이언트에게 네트워크를 통해 정보나 서비스를 제공하는 컴퓨터 시스템으로 컴퓨터
ko.wikipedia.org

즉 클라이언트 요청시 서버가 바로 요청에 대한 응답을 하는 것이 아니라,
요청을 다른 서버에 보내서 그 서버의 응답을 다시 클라이언트에게 보내는, 중간다리 역할을 한다.
설계

만들 코드에 대한 설명은 이렇다.
- 모바일은 POST 요청을 하여 multipart 폼 데이터 (사진)이 Spring 서버에 전달된다.
- Spring 서버는 전달받은 사진을 AI 서버에 전달한다.
- AI 서버는 사진을 받아 OCR 처리를 한 후 JSON 형식으로 결과를 응답한다.
- Spring 서버는 JSON 데이터를 요청한 모바일에 전달한다.
여기서 별로 중요하지 않은 부분은
- 모바일이 요청을 보낸다 : 모바일이 아닌 서버가 요청을 보내도 상관없다. POST 요청이기만 하면 된다.
- AI 서버는 OCR 처리를 결과데이터를 준다 : OCR처리를 하던 어떤거를 하던 결과데이터가 JSON이라는 것만 알면 된다.
- Spring 서버는 데이터를 모바일에 전달한다 : 모바일에 전달하던 서버에 전달하던 응답 형식은 같다.
간단히 말하자면
요청 ---> Spring 서버 ----> AI 서버
요청보냄: 사진 ---> JSON
응답받음: JSON <---
즉 여기서 Spring서버가 프록시 서버이고, 사진을 전달만 하면 된다.
하지만 쉽지 않았다..
우선 프록시서버(1번, 2번, 3번)을 구현한 후
받은 데이터를 스프링 내에서 처리하는 코드(4번,5번)을 구현할 예정이다.
AI 서버
Django와 REST framework를 사용하여 이미지 OCR(광학 문자 인식)과 관련된 작업을 수행하는 API 서버이다.
API Spec
요청
{
"image": "첨부 이미지"
}
응답(JSON)

✅ 요청을 보면 위 데이터 형식이 JSON이라고 생각되어 JSON으로 보낼 수 있는데, 그러면 안된다.
JSON으로 이미지를 보내면 Jackson 라이브러리에서 java.io.FileDescriptor 클래스에 대한 직렬화(Serialization)를 처리할 수 있는 직렬화기(Serializer)를 찾지 못해서 발생하는 오류가 발생한다.
에러 이름
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class java.io.FileDescriptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.HashMap["image"]->org.springframework.web.multipart.MultipartFileResource["inputStream"]->java.io.FileInputStream["fd"])
그러므로 multipart 폼 데이터로 보내는 것이 맞다(코드는 아래에)
프록시 서버 구현(1~3번)
코드 작성 시작시에 아래 블로그를 참고하여 코드를 작성했다. (하지만 문제 발생..)
https://velog.io/@dailylifecoding/Spring-MVC-multipartFile-sending-technique
[Spring MVC] MultipartFile을 서버 또는 Java 코드에서 전송하는 방법
Java 혹은 Spring MVC + Servlet 기반의 서버에서 MultipartForm 을 전송하는 방식에 대해서 알아본다.
velog.io
최종 수정하여 돌아가는 코드는 아래에 있다.
위 블로그의 코드에서 서버 API에 맞춰 고친 코드
import com.cks.bogeom.domain.ImageData;
import com.cks.bogeom.service.StorageService;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class StorageController {
final private StorageService storageService;
private static final RestTemplate REST_TEMPLATE;
static {
// RestTemplate 기본 설정을 위한 Factory 생성
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(8000);
factory.setReadTimeout(8000);
factory.setBufferRequestBody(false); // 파일 전송은 이 설정을 꼭 해주자.
REST_TEMPLATE = new RestTemplate(factory);
}
@PostMapping("/proxyUpload")
public ResponseEntity<?> uploadImages(@RequestParam("image") MultipartFile image) throws IOException {
LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
JsonNode response;
HttpStatus httpStatus = HttpStatus.CREATED;
try {
if (!image.isEmpty()) {
map.add("image", image.getResource());
}
System.out.println("image: " +map.get("image"));
System.out.println("map : "+map.toString());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
String url = "http://localhost:8080/api/test_rest_template_get";
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity
= new HttpEntity<>(map, headers);
response = REST_TEMPLATE.postForObject(url, requestEntity, JsonNode.class);
}
catch (HttpStatusCodeException e) {
HttpStatus errorHttpStatus = HttpStatus.valueOf(e.getStatusCode().value());
String errorResponse = e.getResponseBodyAsString();
System.out.println("에러 1" + errorResponse);
return new ResponseEntity<>(errorResponse, errorHttpStatus);
}
catch (Exception e) {
System.out.println("에러 2"+ e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
System.out.println("전송 성공");
System.out.println("entity : " + new ResponseEntity<>(response, httpStatus));
return new ResponseEntity<>(response, httpStatus);
}
@RequestMapping("/test_rest_template_get")
@ResponseBody
public ResponseEntity<?> test2(List<MultipartFile> image) {
try {
image.forEach(img -> {
System.out.println(img);
System.out.println(img.getContentType());
System.out.println(img.getOriginalFilename());
});
HashMap<String, String> resultMap = new HashMap<>();
resultMap.put("result", "success");
System.out.println("good");
System.out.println("image:"+image);
return ResponseEntity.ok(resultMap);
} catch (Exception e) {
System.out.println("에러 발생했어.");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
위 코드는 잘 돌아간다. (서버에 보내기 전까지는)
코드 설명
/proxyUpload로 image를 이미지파일로 보내면 /test_rest_template_get 이 응답하여 받은 파일에 대한 정보를 출력한다.
잘 작성된 코드이지만 url을 서버 url로 수정하면 500번 에러가 발생된다.
문제 발생
에러 1
/proxyUpload로 보낸 이미지가 AI 서버에서 인식하지 않는 문제가 발생했다.
500번 에러의 굴레에서 빠져나갈수없었다..
에러 상세 정보
org.springframework.web.client.ResourceAccessException: I/O error on POST request for serverUrl: Error writing request body to server; nested exception is java.io.IOException: Error writing request body to server
요청의 본문(body)를 서버에 전송하는 과정에서 오류가 발생했음을 의미한다. 파일 전송시 발생되는 오류이다.
문제 포인트
- image.getResource()로 이미지 파일을 서버에 전송하고있는데, 이 방식은 파일 전송에 적합하지 않다.
- 해결하기위해서는 Resource 객체 대신에 ByteArrayResource나 FileSystemResource 등의 파일 리소스를 사용해야 한다.
- ByteArrayResource는 큰 파일의 경우 메모리 부족 문제가 발생하므로 FileSystemResource방식(파일을 임시 폴더에 저장하고 해당 파일 리소스를 전달하는 방식)도 고려할 수 있다.
- POST 요청을 보낼 때, factory.setBufferRequestBody(false); 설정을 사용하여 버퍼링을 비활성화하고 있다. 이 설정은 파일 전송 시에 사용되는데, 이를 사용하려면 Content-Length 헤더를 정확히 설정해야 한다. 그렇지 않으면 요청 본문을 서버에 전송하는 동안 I/O 에러가 발생할 수 있다.
- 해결하기 위해서는 factory.setBufferRequestBody(false); 문장을 주석처리하거나, factory.setBufferRequestBody(true); 로 설정하면 된다.
- Content-Length 헤더를 정확하게 설정해도 된다.
static {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(16000);
factory.setReadTimeout(16000);
// factory.setBufferRequestBody(false); // 해당 설정 주석 처리
REST_TEMPLATE = new RestTemplate(factory);
}
✅ 이미지 코드는 에러 2 해결에 나와있다.
에러 2
org.springframework.http.converter.HttpMessageConversionException 오류가 발생했다.
No serializer found for class java.io.FileDescriptor와 같은 내용이 나와있는데, 이는 java.io.FileDescriptor 클래스에 대한 직렬화(serialize)를 처리할 수 있는 직렬화기(serializer)가 없어서 발생한다. inputStream은 FileDescriptor 객체를 가지고있는데, 이 객체는 기본적으로 직렬화할 수 없기 때문이다.
에러 상세 정보
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class java.io.FileDescriptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class java.io.FileDescriptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.HashMap["image"]->com.cks.bogeom.domain.ImageWrapper["image"]->org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile["inputStream"]->java.io.FileInputStream["fd"])
문제 포인트
- MultipartFile을 직렬화할 수 없어서 문제가 발생한다.
- MultipartFile을 byte[] 형태로 변환하여 전송하면 된다.
- 파일 업로드시 파일 이름을 지정하지않으면 리소스의 이름이 기본 파일 이름으로 사용된다. 대부분의 경우 파일 이름을 함께 전송하는것이 좋다.
- 나의 경우에는 ⭐️파일 이름을 지정⭐️하니까 500번 에러가 안떴다..!!!!
수정코드

최종 수정 코드
package com.cks.bogeom.api;
import com.cks.bogeom.service.StorageService;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class StorageController {
final private StorageService storageService;
// application.yml 파일에서 AI 서버 URL 읽어오기
@Value("${ai.server.url}")
private String aiServerUrl;
private static final RestTemplate REST_TEMPLATE;
static {
// RestTemplate 기본 설정을 위한 Factory 생성
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(16000);
factory.setReadTimeout(16000);
// factory.setBufferRequestBody(false);
REST_TEMPLATE = new RestTemplate(factory);
}
//ai 서버에 이미지 전달하는 API, /api/proxyUpload
@PostMapping("/proxyUpload")
public ResponseEntity<?> uploadImages(@RequestParam("image") MultipartFile image) throws IOException {
JsonNode response;
HttpStatus httpStatus = HttpStatus.CREATED;
try {
LinkedMultiValueMap<String, Object> data = new LinkedMultiValueMap<>();
ByteArrayResource resource = new ByteArrayResource(image.getBytes()) {
@Override
public String getFilename() {
return image.getOriginalFilename();
}
};
//{"image":이미지파일}을 map에 추가한다.
data.add("image", resource);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA); //폼 데이터로 보낸다.
//요청 entity 만들기
HttpEntity<LinkedMultiValueMap<String, Object>> requestEntity = new HttpEntity<>(data, headers);
//요청 보내고 응답 받기
response = REST_TEMPLATE.postForObject(aiServerUrl, requestEntity, JsonNode.class);
//에러 잡기
} catch (HttpStatusCodeException e) {
HttpStatus errorHttpStatus = HttpStatus.valueOf(e.getStatusCode().value());
String errorResponse = e.getResponseBodyAsString();
System.out.println("에러 1" + errorResponse);
return new ResponseEntity<>(errorResponse, errorHttpStatus);
} catch (Exception e) {
System.out.println("에러 2" + e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
System.out.println("전송 성공");
System.out.println("entity: " + new ResponseEntity<>(response, httpStatus));
return new ResponseEntity<>(response, httpStatus);
}
}
결과화면

감격을 감출 수 없다..ㅎㅎㅎ

또한 chatgpt에게 무한의 감사를 보낸다. 😍
받은 JSON 데이터를 스프링 내부에서 처리해서 JSON 데이터로 돌려주는 코드(4~5번)은 후에 작성하겠다.
누군가에게 도움이 되길 바라면서 이만 끝!!