3차 과제 프로젝트 진행 중, 레드마인 api로 파일 업로드하는 기능을 구현했다.
webclient를 이용하여 api 서버에서 레드마인 api로 파일 업로드 .. !
순서는 다음과 같다.
1. 클라이언트 -> api 서버 호출
2. api 서버 -> 레드마인 api 서버 호출 (webclient 사용)
3. 레드마인 api 서버 -> api 서버
4. api 서버 -> 클라이언트
효율적인 코드인지에 대해 검증을 받지 못해(?) 소스 코드를 올리는 게 부끄럽다.
그래도 하울의 움직이는 성처럼 얼레벌레 동작은 하기 때문에 조금이나마 도움이 되는 사람이 있다면 좋겠다.
Redmine api 문서 먼저 읽어보는 걸 추천!
https://www.redmine.org/projects/redmine/wiki/Rest_api#Attaching-files
1. postMan으로 redmineAPi 테스트
1) Basic Auth 설정
-> 안하면 401 에러 response
2) Header에서 Content-Type을 application/octet-stream로 설정
-> 안하면 422 에러 response
3) request Body binary로 보내야함 (form-data 아님!!!!)
🪓 삽질로그
request body is file content가 도대체 뭘 의미하는지 모르겠어서, 엄청나게 삽질했다 ㅠ
JSON으로도 보내보고 form-data로도 보내봤는데 이미지가 자꾸 깨져서, 결국 서버에 들어가서 업로드된 파일을 까봤다(?)
레드마인 웹서버에서 파일을 업로드한 파일을 까보면 다음과 같은 형식이다.
그리고 내가 보낸 것
알고 보니 저 형식은 바이너리 데이터였다.
그니까 나도 바이너리 형식으로 보내야 했던 것이다 ^.ㅠ....
4) response 확인
2. webclient를 이용해서 api 서버 -> 레드마인 api 서버 호출
그런데 나는 클라이언트단에서 바로 레드마인 api를 호출하는 것이 아닌 api 서버를 거쳐야 했다.
Controller
// redmine api 파일 업로드 시에는 token 생성이 필수이며,
// formdata로 전달 받은 file을 MultipartFile 타입으로 받아 getBytes()로 바이트 배열로 변환함 => 해당 value를 webclient의 bodyValue에 담는다.
// 클라이언트단에 id와 token return함
@PostMapping("/uploads.json")
public UploadTokenDto createFileToken(@ModelAttribute MultipartFileDto dto){
MultipartFile multipartFile = dto.getFile();
// button_01.png와 같이 파일명.확장자 형식으로 출력됨
// System.out.println(multipartFile.getOriginalFilename());
Map<String ,Map<String,Object>> resToken = null ; // await
UploadTokenDto res = null;
// 파일명 중복 방지를 위해 UUID 객체 생성
UUID uuid =UUID.randomUUID();
String uploadFileName = multipartFile.getOriginalFilename();
uploadFileName = uuid.toString() + uploadFileName;
try {
WebClient webClient = WebClient.builder()
.baseUrl(BaseURLType.API_SERVER.getUrl())
.defaultCookie("cookie-name", "cookie-value")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
.build();
// 레드마인 api에서 생성되는 responseToken을 클라이언트로 return
resToken = webClient.post().uri("/uploads.json?filename="+multipartFile.getOriginalFilename())
.header(HttpHeaders.AUTHORIZATION, AdminAuth.BASIC_BASE_64.getKey()) // 파일 업로드를 위해 Content-Type을 application/octet-stream으로 변경
.bodyValue(multipartFile.getBytes()) // multipartFile.getBytes() 호출시 바이트 배열 반환되며, 이를 bodyValue로 담아줌
.retrieve() // client message 전송
.bodyToMono(Map.class) // body type
.block();
int resId = (Integer) resToken.get("upload").get("id");
String token = (String) resToken.get("upload").get("token");
// uploadTokenDto에 레드마인 api에서 전달 받은 값 담음
res = UploadTokenDto.builder()
.id(resId)
.token(token)
.build();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
// 로컬 디스크로 파일 업로드
FileOutputStream writer = new FileOutputStream(업로드할 파일경로 + uploadFileName);
// 해당 경로에 파일 생성
writer.write(multipartFile.getBytes());
} catch (Exception e) {
return res;
}
return res;
}
설명
레드마인에게 응답 받는 형식은 다음과 같다.
upload의 vlaue로 json 객체가 들어있으니까, Map<String,Map<String,Object>>로 resToken을 선언하였다.
{
"upload": {
"id": 183,
"token": "183.a3ee21ce10d3577c8854f1fae0b3524121cfd078d84741020008190ecae66415"
}
}
근데 클라이언트에는 token 값만 필요하니까 굳이 upload로 감싸서 보낼 필요는 없을 거 같아서,
전달 받은 데이터를 resId, token에 담아 UploadTokenDto 객체를 생성하였고, 클라이언트에는 이 객체를 반환하였다.
조금 어려웠던 것
form-data로 받아온 파일을 다시 binary형식으로 바꿔서 webclient에 넣어서 보내줘야 하는데, 도대체 어떻게 보내야 할지 감이 안 왔다..ㅎ여러 삽질을 한 끝에 bodyValue를 multipartFile.getBytes()로 하여 전송했다. getBytes()는 전송받은 파일을 바이트 배열로 바꿔주는 메서드다. 이걸로 설정해야 이미지가 깨지지 않고 잘 들어갔다.
resToken = webClient.post().uri("/uploads.json?filename="+multipartFile.getOriginalFilename())
.header(HttpHeaders.AUTHORIZATION, AdminAuth.BASIC_BASE_64.getKey()) // 파일 업로드를 위해 Content-Type을 application/octet-stream으로 변경
.bodyValue(multipartFile.getBytes()) // multipartFile.getBytes() 호출시 바이트 배열 반환되며, 이를 bodyValue로 담아줌
.retrieve() // client message 전송
.bodyToMono(Map.class) // body type
.block();
UploadTokenDto
@Data
@AllArgsConstructor
@RequiredArgsConstructor
@Builder
public class UploadTokenDto {
private int id;
private String token;
}
3. PostMan을 통해 api 검증
먼저 레드마인 api의 url 경로를 내가 설정한 api 경로로 변경해준다.
나의 경우 http://localhost:8081/test/uploads.json?filename=파일명.확장자로 변경하였다.
그리고 Body를 form-data 형식으로 하였다. 왜냐면 클라이언트에서 파일을 넘겨줄 때 바이너리 형식이 아닌 form-data 형식으로 받고 싶어서이다. key는 file, value로 파일 업로드 해주면 끝! ( key는 임의로 지어주면 된다. )
📝 느낀 점
하....... 3일 동안의 삽질을 통해 정말 많은 걸 배웠다. 그래도 가장 걱정하던 기능이 해결 되어서 다행이다.
remine API 커뮤니티에서도 파일 업로드 관련 정보를 찾지 못해서, 애를 많이 먹었었다.
내 코드는 클라이언트에서 form-data로 받은 파일을 다시 binary로 변경해서 redmine API를 호출하고 있는데,
저 방식이 효과적이고, 클린한 코드인가..?에 대해서는 의문이다. 🧐 그래도 일단 기능적으로는 동작하니까 넘어가는 걸로 .. !
바이너리 코드 : 컴퓨터가 인식할 수 있는 0과 1로 구성된 이진코드
바이트 코드 : CPU가 아닌 가상 머신에서 이해할 수 있는 코드를 위한 이진 표현법이다. 즉, 가상 머신이 이해할 수 있는 0과 1로 구성된 이진코드를 의미.
application/octet-stream : (옥텟 스트림) : 8비트 단위의 바이너리 데이터를 의미
🔗 Refs.
https://junhyunny.github.io/spring-boot/vue.js/multipartfile/
'🏰 Back-end' 카테고리의 다른 글
[SonarQube] Local 환경에서 정적분석 돌려보기 (SonarQube 설치 및 구동) (0) | 2023.07.09 |
---|---|
[MariaDB] 에폭 타임 형태의 TEXT 데이터를 date 타입으로 변환 (1) | 2023.06.11 |
Dump Data insert(=import) (0) | 2023.04.09 |
[Redmine API] 구성원(member) 다중 insert (0) | 2023.03.05 |
[Docker] 로컬 PC에서 mariaDB container로 접속하기 (0) | 2023.01.27 |
댓글