티스토리 뷰
서버 to 서버로 데이터를 전송할 일이 생겼는데.... 문제가 생겼다.
문제 상황은 다음과 같다.
1. A 서버에서 B서버로 요청을 보낼 계획
2. A 서버는 S3에서 파일정보를 가져와서 ByteArray 형태로 들고 있음
3. B 서버에는 MultipartFile을 파라미터로 받는 API(ex. /api/file)가 존재함 - 수정 불가(FE에서 사용 중)
4. A 서버에서 B 서버로 /api/file을 요청해야 함 - 데이터만 삽입하고 끝내기엔 이력이나 업데이트해야할 정보가 많음
5. A 서버에서는 FeginClient를 사용 중(HTTP 통신을 위해서 사용 중, 굳이 고집하지 않아도 됨)
요약하면, 요청을 보내기 위해서 스프링부트 서버에서
MultipartFile을 어떻게 만들 것인가? 와 FeignClient로 Multipart 파일을 어떻게 전송할 것인가? 이다.
차근 차근 알아보자.
MultiparfFile을 어떻게 만들 것인가?
내가 찾아본 방법은 총 세 가지다.
1. MockMultipartFile로 만들기
2. DiskFileItem로 CommonsMultipartFile을 만들기
3. CustomMultipartFile 만들기
1번은 MockMultipartFile 객체의 의존성이 spring-test기 때문에 상용 서비스 환경에서는 사용하면 안된다.
내부 구성요소가 MultipartFile과 거의 동일하고 생성자로 다양한 형식을 지원하기 때문에 유혹에 빠지기 쉽지만, 권장하는 방식은 아니다.
2번도 좋은 방법이다.
코드는 다음과 같다.
public MultipartFile byteArrayToMultipartFile(byte[] byteArray, String fileName) {
File tempFile = null;
try {
// 1. 임시 파일 생성
tempFile = File.createTempFile(fileName, "");
try (OutputStream os = Files.newOutputStream(tempFile.toPath())) {
os.write(byteArray);
}
// 2. DiskFileItem 생성
DiskFileItem fileItem = new DiskFileItem(fileName, Files.probeContentType(tempFile.toPath()),
false, fileName, (int) tempFile.length(), tempFile.getParentFile());
// 3. 임시 파일의 내용을 DiskFileItem으로 복사
try (InputStream input = Files.newInputStream(tempFile.toPath());
OutputStream output = fileItem.getOutputStream()) {
IOUtils.copy(input, output);
}
// 4. CommonsMultipartFile 생성 및 반환
return new CommonsMultipartFile(fileItem);
} catch (IOException e) {
// 에러 처리
} finally {
// 임시 파일 삭제
}
}
그러나 이 방식은 현재로선 조금 아쉽다.
왜냐하면 DiskFileItem으로 만드는 과정에서 로컬에 파일을 저장해야 하기 때문이다.
io를 close 하는 과정도 반드시 필요하고 파일도 삭제해줘야하기 때문에 손이 많이 간다.
결론은 3번 방법을 선택했다.
파일 정보를 ByteArray로 들고 있는데, ByteArray를 생성자 파라미터로 받는 CustomMultipartFile을 구성했다.
public class CustomMultipartFile implements MultipartFile {
private final byte[] input;
public CustomMultipartFile(byte[] input) {
this.input = input;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(input);
}
@Override
public String getName() {
return "";
}
@Override
public String getOriginalFilename() {
return "";
}
@Override
public String getContentType() {
return MediaType.APPLICATION_JSON_VALUE;
}
@Override
public boolean isEmpty() {
return input.length == 0;
}
@Override
public long getSize() {
return input.length;
}
@Override
public byte[] getBytes() {
return input;
}
@Override
public void transferTo(File dest) throws IOException {
try (FileOutputStream fos = new FileOutputStream(dest)) {
fos.write(input);
}
}
}
사용하고 싶은 위치에서 new CustomMultipartFile(byteArray)로 사용하면 끝이다.
그냥 뚝딱뚝딱 쓸 수 있는 구현체가 있으면 좋은데, 스프링에서 기본적으로 제공하는 MultipartFile의 구현체가 없다.
OpenFeign으로 어떻게 보낼 것인가?
자 그다음은 MultipartFile을 요청 데이터로 실어서 보내야한다. 문제는 어떻게 보낼 것인가? 이다.
받는 쪽 서버의 요청 객체를 보면 다음과 같다.
public class MultipartFileRequest {
private String userId;
priavte MultipartFile file;
...
}
아하 동일하게 요청을 보내는 쪽에도 저렇게 구성해서 보내면 되겠구나라는
생각이 들지만 막상 보내려고하니 어떤 어노테이션으로 구성해야할지 잘 모르겠다.
@ModelAttribute 같은 어노테이션은 받는 쪽에서 처리하는 거라 발송하는 쪽에서는 당연히 안된다.
@RequestParam + @RequestBody를 써야하나.. @RequestPart + @RequestParam을 써야하나 고민하다가 검색해보니
메인테이너가 @RequestPart를 쓰라고 친절하게 리뷰를 달아줬다.
https://github.com/spring-cloud/spring-cloud-openfeign/issues/896
이는 Multipart/from-data의 요청이 다른 Content-Type들과는 다르게 구성되기 때문이다.
application/json 형식과 간단히 비교해보면 이해하기 쉽다.
POST /api/upload/json
Content-Type: application/json
{
"name": "example",
"description": "This is an example."
}
POST /api/upload/text
Content-Type: text/plain
{
"name": "example",
"description": "This is an example."
}
POST /api/upload/multipart
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="json"
{
"name": "example",
"description": "This is an example."
}
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="example.txt"
Content-Type: text/plain
This is the content of the file.
------WebKitFormBoundary7MA4YWxkTrZu0gW--
파일이 포함된 것도 있지만, Multipart/from-data의 구성이 확실히 다르다.
여기서 RequestPart에 들어가는 이름 부분이 name 부분이다.
하지만 받는 쪽에서 별다른 설정을 하지 않는다면, 파일명까지는 확인하지만 json 객체의 name까지 보진 않는 것 같다.
마치며
전부터 궁금하던걸 한번에 싹 정리했다.
왜 MultipartFile이 interface라는 것과 mulitpart/form-data의 요청 방식을 진작에 알아보지 못했을까 싶다.
요청 데이터를 뜯어볼 일이 별로 없었는데, 웹 도메인에서 일하려면 어느정도는 숙지하는게 좋을 것 같다.
'개발 > SPRING' 카테고리의 다른 글
테스트를 도입하면서 느낀점과 고민들 (0) | 2024.08.14 |
---|---|
Kotlin + SpringBoot 서버에 테스트 도입기. Kotest와 MockK (0) | 2024.07.19 |
Layered 아키텍처에서 Hexagonal 아키텍처로 리팩토링하면서 느낀 점 (0) | 2024.07.04 |
OpenFeign 간단 사용법과 FeignException 핸들링하기 (0) | 2024.03.22 |
Google Document Translation API 사용하기 (with. Spring Boot) (1) | 2024.02.20 |
- Total
- Today
- Yesterday
- ChatGPT
- elasticsearch
- openAI API
- terraform
- Elastic cloud
- S3
- lambda
- AOP
- OpenFeign
- serverless
- 티스토리챌린지
- AWS EC2
- GIT
- cache
- docker
- java
- springboot
- EKS
- CloudFront
- AWS
- 람다
- 오블완
- MySQL
- 스프링부트
- Log
- 후쿠오카
- JWT
- Kotlin
- Spring
- OpenAI
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |