[Java] 외부 라이브러리 없이 Multipart / Formdata 전송하기

박진용·2021년 1월 7일
1
post-thumbnail

Mutipart

  • 웹 클라이언트가 요청할 떄, Http 프로토콜의 body 부분에 데이터를 여러 부분으로 나눠서 보내는 것

  • 주로 파일 전송시 사용, 파일을 한번에 여러개의 부분으로 연결되어 전송된다. 이렇게 나뉘어서 전송되는 것을 Multipart data라고 한다.

  • 기본적으로 Content-Type에 "Multipart/form-data"라는 것을 지정해주어야, 서버에서 처리가 가능하다.

모든 Multipart의 하위 유형은 공통된 구문을 공유하는데 Content-Type에 boundary를 파라미터로 요구한다.
이 boundary는 나누어진 form-data의 경계를 의미하는데, Content-Type의 파라미터로 주어진 값에 두개의 하이픈("--")으로 구성되어 있다.

출처 - The Multipart Content-Type 7.2.1

이는 HTTP 통신 규격으로 지정되어 있으며, 이 규격에 맞게 Http header와 body 데이터를 생성 후 서버에 요청하면 서버에서 해당 규격으로 데이터를 파싱 후 처리하게 된다.

구현

java에서 기본 내장 패키지인 java.net.http.HttpClient는 Multipart전송시에 상당히 불편한 인터페이스를 가지고 있다. Body에 들어가는 부분을 직접 구현해 주어야 한다는 것.

BigInteger를 통해, 랜덤한 수를 생성해 Boundary로 사용할 문자열을 만들었다.

HashMap에 formdata의 key, value로 사용될 값들을 넣어준다.

내장 라이브러리인 HttpRequest에 헤더로 Content-type을 적어줄때, 필자의 경우에는 세미콜론을 빠뜨려서 잠시 헤매었으니 주의하도록 한다.

private HttpRequest.BodyPublisher multipartToByte (Map<Object, Object> map, String boundary) throws IOException {
    List<byte[]> byteArrays = new ArrayList<>();
    StringBuilder stringBuilder = new StringBuilder();

    for (Map.Entry<Object, Object> data : map.entrySet()) {
      stringBuilder.setLength(0);
      stringBuilder.append(DOUBLE_HYPHEN)
          .append(boundary)
          .append(LINEFEED);

      if (data.getValue() instanceof Path) {
        Path filePath = (Path)data.getValue();
        String mimeType = Files.probeContentType(filePath);
        byte[] fileByte = Files.readAllBytes(filePath);

        stringBuilder.append("Content-Disposition: form-data; name=")
            .append(QUTATE)
            .append(data.getKey())
            .append(QUTATE)
            .append("; filename= ")
            .append(QUTATE)
            .append(data.getValue())
            .append(QUTATE)
            .append(LINEFEED)
            .append("Content-Type: ")
            .append(mimeType)
            .append(LINEFEED)
            .append(LINEFEED);

        byteArrays.add(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));
        byteArrays.add(fileByte);
        byteArrays.add(LINEFEED.getBytes(StandardCharsets.UTF_8));
      } else {
        stringBuilder.append("Content-Disposition: form-data; name=")
            .append(QUTATE)
            .append(data.getKey())
            .append(QUTATE)
            .append(";")
            .append(LINEFEED)
            .append(LINEFEED)
            .append(data.getValue())
            .append(LINEFEED);
        byteArrays.add(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));
      }
    }

    stringBuilder.setLength(0);
    stringBuilder.append(DOUBLE_HYPHEN)
        .append(boundary)
        .append(DOUBLE_HYPHEN);
    byteArrays.add(stringBuilder.toString().getBytes(StandardCharsets.UTF_8));

    return HttpRequest.BodyPublishers.ofByteArrays(byteArrays);
  }

Map과 boundary를 인자로 받아 순환하도록 한 후, boundary로 시작한다.

StringBuilder를 통해 문자열을 완성시킨후 getBytes()함수로 바이트배열화해 바이트배열 리스트에 담아주도록 한다.

getValue()의 값이 Path의 인스턴스 여부에 따라 다른 내용을 만드는데, 차이점은 filename이 추가된다는 것과,

formdata의 body부분에 파일 데이터가 쓰여진다는 점이다. 주의할점은 HTTP통신시에 "\r\n"을 개행으로 사용한다는 것과

한줄이 끝났을때 개행을 하지않거나, 혹은 잘못 개행하거나 했을경우 데이터가 정상적으로 보내지지 않을 수 있다.

서버에서 데이터를 받지 못했을경우, 잘못 만든것이 아닌지 확인해 보도록 하자

마지막 boundary는 boundary뒤에 하이픈("--")을 두개 더 붙여서 body의 끝임을 알려주도록 한다.

profile
하루하루 깊어지자

0개의 댓글