ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • StompJs에서 이미지 이진 데이터 전송하기, 문제 해결
    문제 해결 기록 2022. 9. 6. 17:55

    이 글에는 제가 프로젝트를 수행하면서 StompJs를 통한 이진 데이터 전송 방법 및 겪은 어려움을 제 방식대로 해결한 내용을 담았습니다. 제 방법보다 더 나은 방법을 알고계시면 댓글 등으로 공유해주시면 감사하겠습니다.

     

    StompJs의 공식 문서를 참고하면 StompJs는 5버전 이상부터 이진 데이터 전송을 지원한다고 합니다.

     

    Starting version 5, sending binary messages is supported. To send a binary message body, use the binaryBody parameter. It should be a Uint8Array.

     

    const binaryData = generateBinaryData(); // This need to be of type Uint8Array
    // setting content-type header is not mandatory, however a good practice
    client.publish({
      destination: '/topic/special',
      binaryBody: binaryData,
      headers: { 'content-type': 'application/octet-stream' },
    });

    위는 공식문서 발췌 내용입니다.

     

    저 설명에서는 서버에 보내고자하는 이진 데이터를 8비트 데이터로 변환하여 전송하라고 명시하고있습니다.

    8비트 데이터는 Javascript에서 기본으로 제공하는 클래스인 Uint8Array의 생성자 함수에 넣어 8비트 데이터로 만들 수 있습니다.

     

    const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        if (e.currentTarget.files) {
            const targetFile = e.currentTarget.files[0];
            const fileReader = new FileReader();
            fileReader.onload = handleReaderOnLoad;
            if (fileReader && targetFile) fileReader.readAsArrayBuffer(targetFile);
        }
    }
    const handleReaderOnLoad = (readerEvent: ProgressEvent<FileReader>) => {
        const result = readerEvent.target?.result;
            if (result && typeof result !== 'string') {
                imageFile = new Uint8Array(result);
        }
    }

    위의 handleOnChange함수는 type속성이 file인 태그로 value가 변화하면 작동합니다.

    해당 함수 안의 fileReader는 File, Blob를 읽을 수 있는 FileReader 클래스의 객체입니다. 

    fileReader는 readAsArrayBuffer이라는 메소드를 통해 Blob 데이터를 다시 int 배열의 형식인 ArrayBuffer 자료형으로 파일을 변환시킵니다.

    참고로 ArrayBuffer이라고 불리는 이 int형 배열들은 일정한 길이별로 데이터들이 송수신되는 버퍼링 시스템에 사용되는 그 형식입니다.

     

    여기서 readAsArrayBuffer 메소드를 호출할 때, fileReader의 onload 메소드에 할당해준 handleReaderOnLoad 함수가 실행됩니다. 해당 함수는 자동으로 이벤트 객체를 인자로 받습니다. 그 이벤트 객체의 target 속성의 객체에는 다시 result 속성이 있는데 이 속성의 value가 우리가 readAsArrayBuffer 메소드에 넣었던 blob 파일입니다.

     

    어쨌거나, handleReaderOnLoad 함수의 마지막 비즈니스 로직을 보시면 imageFile이라는 변수에 blob파일을 8비트짜리 배열(ArrayBuffer)로 만든 결과를 할당해줍니다.

     

    해당 결과를 콘솔창에 출력해보겠습니다.

     

    첫 번째 출력 내용은 결과 ArrayBuffer의 길이이고 두 번째는 내용입니다.

     

    해당 사진의 속성 정보입니다. 크기가 64.666 바이트이고 위의 ArrayBuffer의 길이도 64.666이니까 우리의 브라우저가 임무를 잘 수행한것 같습니다!

     

    ArrayBuffer의 내용도 -125 부터 125 까지의 int 배열로 이루어졌으니 blob 파일을 8비트로 잘 분해시킨것같네요. 8비트의 표현 범위가 0 ~ 255니까 말입니다.

     

    그럼 이대로 서버에 소켓 전송을 해보겠습니다.

    온전한 데이터가 전송되었는지 확인을 위해 여기서도 log를 찍어보겠습니다.

    과연 결과는??..

    ??

    ???..

     

    길이가 64666 바이트이던 이진 이미지 데이터가 227963바이트로 길이가 늘어났으며

    내용도 40 ~ 50대 사이의 int 배열로 값이 작아졌습니다.

     

    stomp 소켓 통신에서 HttpHeader에 해당하는 simpleMessageHeaderAccessor를 출력해보니 위의 이미지 크기인 64.666가 아님을 확인했습니다. 따라서 애당초 클라이언트에서 해당 이진 이미지 데이터를 잘못된 형식으로 보내고있다고 추측했습니다.

     

    혹시나 npm과 gradle의 stomp 버전을 확인해보니 문제는 없었습니다.

    또한, Springboot docs에서 찾은 방법인 아래의 코드를 추가해보는 것도 도움이 되지 못했습니다. 이 코드는 WebSocketMessageBrokerConfigurer 인터페이스를 구현한 객체에 메소드로 추가해주는 config 코드입니다.

     

    @Override
    public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
        messageConverters.add(new ByteArrayMessageConverter());
        return false;
    }

    그래서 이 부분에서 이틀동안 고생을 좀 했습니다.

     

    제가 해결한 방식은 문제를 조금 비틀어 해결했습니다. 정공법이 아님을 감안하시고 참고하시길 바랍니다!

     

    이 사진을 보시면 int 배열의 범위가 40~60 사이를 순회합니다. 이를 자세히 살펴보니 44가 중간에 계속 껴있습니다.

    44의 아스키 코드값은 쉼표입니다.

     

    그래서 저는 쉼표를 사이에두고 나열된 무엇인가가 전송되었다는 뜻이라고 생각했습니다. 그렇게 생각하니 각 숫자 하나까지도 아스키코드로 생각해보게 되었습니다.

     

    위의 사진을 기준으로 설명하겠습니다. 49의 아스키코드값은 문자(char) 1입니다. 51은 3이고 55는 7입니다.

    따라서 위의 출력값들은 137, 84, 103, ~ 등으로 나아가는 숫자의 배열임을 알 수 있었으며,

    브라우저 콘솔창에 찍힌 이진 이미지 데이터로 만들어진 ArrayBuffer값을 확인해보니 똑같은 값을 가지고있었습니다.

     

    즉, 137, 84, 103의 ArrayBuffer를 전송하면 이 것이 알 수 없는 버그로 1, 3, 7, 쉼표, 8, 6, ,쉼표, 1, 0, 3 ~ 이렇게 쪼개져 전송되고있었던 것입니다. 

     

    또한, 그러니 브라우저에서 64.666로 찍히던 byteLength가 서버의 로그에서는 더욱 긴 길이인 227,963으로 출력되었던 것임을 파악했습니다.

     

    문제의 핵심 패턴을 파악했으니 이 쪼개진 데이터를 다시 묶어주는 작업이 필요했습니다.

    public byte[] extractImageByteData(byte[] bytes, int imageSize) {
        final int COMA = 44;
        final int MAX = (bytes.length - 1);
        byte[] imageByte = new byte[imageSize];
        StringBuilder stringBuilder = new StringBuilder();
        int count = 0;
        int integer;
        for (int i=0; i<bytes.length; i++) {
            if (i == MAX) stringBuilder.append((char) bytes[i]);
            if (bytes[i] == COMA) {
                integer = Integer.parseInt(stringBuilder.toString());
                imageByte[count++] = (byte) integer;
                stringBuilder.delete(0, stringBuilder.length());
            } else stringBuilder.append((char) bytes[i]);
        }
        return imageByte;
    }

    그렇게 흩어진 ArrayBuffer를 쉼표를 중심으로 원래 값으로 합쳐주는 알고리즘 로직을 컨트롤러에서 호출하게 만들었습니다. 해당 알고리즘의 자세한 설명은 생략하겠습니다.

     

    저 메소드는 결국 원본 Arraybuffer의 길이와 값으로 이진 이미지 데이터를 만들어줍니다. 

    참고로 위 메소드에서 두 번째 인자로 받는 imageSize 변수는 원본 이미지의 byteLength를 headers객체 안에 함께 보내어 원본 이미지 크기에 딱 맞는 byte 배열을 선언할 수 있게하였습니다.

     

    만약 아무런 예외나 에러 없이 해당 메소드가 완료된다면 서버 컴퓨터의 디렉토리 안에 다시 재조립한 이미지 파일들이 저장되고, 해당 채팅방 안의 stomp socket 메세지 구독자들에게 이를 알리는 메세지를 보내게됩니다.

     

    그 메세지를 받은 사용자의 브라우저는 방금 전송된 이미지의 경로를 src 속성으로 가진 img 태그를 생성하게하여 모든 채팅방 인원의 브라우저에 전송된 이미지를 렌더링하는 로직을 마지막으로 구현하여 해당 기능을 완성하였습니다.

     

    앞서 기술하였듯이, 이번 문제 핸들링은 docs와 블로그 등에서도 해결법을 찾기 힘들어 좀 다른 방법으로 구현해 보았습니다. 

     

    더 나은 해결 방법이 있으면 언제든지 댓글 부탁드립니다. ^^

     

     

     

Designed by Tistory.