[Redmine API] webclient를 이용하여 api 서버에서 레드마인 api로 파일 업로드

    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

    Rest api - Redmine

    Redmine API¶ Redmine exposes some of its data through a REST API. This API provides access and basic CRUD operations (create, update, delete) for the resources described below. The API supports both XML and JSON formats. API Description¶ Resource Status

    www.redmine.org

    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://cofs.tistory.com/334

    Java 파일을 바이너리 스트링으로, 바이너리 스트링을 파일로 변환 / 파일 전송

    Java 파일을 바이너리 스트링으로, 바이너리 스트링을 파일로 변환 File to Binary String, Binary String to File 파일을 다른 곳으로 전송해 주어야 할 경우가 생겼다. 파일을 전송하는 방법에는 여러가지가

    cofs.tistory.com


    https://junhyunny.github.io/spring-boot/vue.js/multipartfile/

    MultipartFile 인터페이스와 파일 업로드

    <br /><br />

    junhyunny.github.io

    728x90

    댓글