1. 글을 작성하게 된 계기
사람들과 스프링 서버를 만들어보는 프로젝트를 진행하며, 이미지 파일이 깨져서 나오는 문제가 발생했습니다. 이를 해결하는 과정에서 왜 이런 일이 발생하는지, 이를 어떻게 해결했는지에 대해 정리하고 싶어 해당 글을 작성하게 되었습니다.
- 왜 이미지를 문자열로 바꿔서 응답하면 데이터가 깨질까?
- 어떻게 해결해야 할까?
2. 데이터 손실
이미지가 나오지 않는 이유는 데이터가 손실됐기 때문인데, 이에 대해 먼저 살펴보겠습니다. 이미지, 음악, 영화와 같은 바이너리 파일을 텍스트로 전송하면 데이터가 손실될 수 있는데, 이는 문자 집합만으로 바이너리 데이터를 모두 표현할 수 없기 때문입니다.
애초에 웹에서 전송되는 형식(Content-Type)이 구분 돼 있기도 하고요.
예를 들어, ASCII를 사용해 바이너리 데이터를 변환하는 경우를 살펴보겠습니다. ASCII 문자 집합은 128개의 문자(7bit)만을 포함하고 있기 때문에, 해당 범위를 넘어가는 데이터를 변환할 때 데이터가 손실됩니다. ASCII 문자 집합만으로는 바이너리 파일에 포함된 다양한 데이터를 표현할 수 없기 때문입니다.
바이너리 데이터는 0과 1의 조합으로 구성되어 있으며, 이를 통해 7bit가 넘는 복잡한 데이터를 표현할 수 있습니다. 이는 ASCII의 범위를 초과하므로 올바르게 표현/변환될 수 없는 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@DisplayName("byte 변환 단위 테스트")
class ByteConvertUnitTest {
@Test
@DisplayName("ASCII 범위를 넘어가면 문자열이 깨진다.")
public void base64EncodingEqualsTest() {
byte[] binaryDataArray = {(byte) 0xe4, (byte) 0xb8, (byte) 0x80};
String asciiStr = new String(binaryDataArray, US_ASCII);
String originalData = Base64.getEncoder().encodeToString(binaryDataArray);
String asciiData = Base64.getEncoder().encodeToString(asciiStr.getBytes(US_ASCII));
assertNotEquals(originalData, asciiData);
}
}
범위를 초과하는 데이터를 올바르게 변환하기 위해서는 Base64 인코딩, Buffer, Stream, Channel과 같은 다른 방법을 사용해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@DisplayName("byte 변환 단위 테스트")
class ByteConvertUnitTest {
@Test
@DisplayName("Base64 인코딩/디코딩을 통해 원본 문자열을 복원할 수 있다.")
void base64EncodingTest() {
String str = "한글";
byte[] byteArray = str.getBytes(UTF_8);
String base64Encoded = Base64.getEncoder().encodeToString(byteArray);
byte[] decodedBytes = Base64.getDecoder().decode(base64Encoded);
String decodedStr = new String(decodedBytes, UTF_8);
assertEquals(str, decodedStr);
}
}
정리해 보면 바이너리 데이터(이미지)를 문자열로 변환해 응답 본문에 실었기 때문에 데이터 손상이 발생한 것입니다. 지금까지 이미지 데이터를 문자열 변환해 전송하면 왜 데이터가 손상되는지를 살펴보았습니다. 이제 이를 어떻게 해결했는지 살펴보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class StaticView implements View {
......
private void setResponseBody(
HttpServletResponse response,
byte[] imageData
) {
// 바이너리 데이터를 문자열로 치환 후 본문에 실음
response.setBody(new String(imageData));
}
}
3. 문제 해결
이미지 데이터를 버퍼에 담은 후, 소켓에 데이터를 write 했습니다. 즉, 바이너리 데이터를 문자열로 바꾸는 과정에서 데이터가 손상될 수 있기 때문에 바이너리 데이터를 직접 처리한 것입니다.
1
2
3
4
5
SocketChannel channel = response.getSocketChannel();
ByteBuffer buffer = ByteBuffer.wrap(body);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
Base64 인코딩 방식을 활용할 수도 있지만 Socket과 Channel을 사용하고 있었기 때문에 해당 방식을 채택했습니다.
이를 통해 화면을 올바르게 렌더링할 수 있었습니다.
4. 정리
바이너리 데이터를 문자열로 변환하는 과정에서 데이터가 손상될 수 있습니다. 따라서 이미지, 영상 등과 같은 바이너리 데이터를 전송해야 할 경우, 바이너리 데이터를 직접 전송/처리해야 합니다.
