ByteArray class
정의 및 역할
ByteArray는 원시적인 8비트 부호 있는 정수(byte)의 순서가 있는 배열이다. 컴퓨터 과학에서 가장 기본적인 데이터 형식이며, 모든 종류의 디지털 정보(텍스트, 이미지, 오디오, 프로그램 파일 등)는 궁극적으로 바이트의 연속으로 표현된다.
ByteArray는 이러한 바이트 시퀀스를 메모리에 담는 Kotlin 타입이다.
Kotlin은 성능상의 이유로 IntArray, BooleanArray 등 원시 타입 배열을 제공한다. 이들은 Generic (Array<T>)와 달리 박싱(Boxing, 원시 타입을 객체로 감싸는 것) 오버헤드가 없어 더 빠르다. ByteArray는 이 원시 타입 배열 중 하나다.
데이터 입출력 및 변환
네트워크 통신이나 파일 저장 시 텍스트 데이터를 바이트로 변환하는 것은 필수다. 때문에 인코딩(Encoding) 방식을 지정하는 것이 매우 중요하다.
// String -> ByteArray
val text = "Hello"
val bytes: ByteArray = text.toByteArray(Charsets.UTF_8)
// ByteArray -> String
val decodedText = String(bytes, Charsets.UTF_8)
print(decodeText) // 출력: "Hello"
입출력 스트림 연동 (I/O Integration)
ByteArrayOutputStream (쓰기)
ByteArrayOutputStream은 데이터를 메모리 버퍼에 바이트 형태로 임시로 쌓아둔다. 파일이나 소켓처럼 실제 I/O 장치에 연결되지 않고, 오직 메모리 내부의 ByteArray를 구성하는 데 사용된다.
import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
fun writeToByteArrayStream() {
// 1. ByteArrayOutputStream 객체 생성 (버퍼는 내부적으로 관리됨)
val baos = ByteArrayOutputStream()
// 2. 데이터 작성
baos.wirte(10) // 10진수 10 (0x0A)
val message = "Hello, Stream"
baos.write(message.toByteArray(StandardCharsets.UTF_8))
baos.write(byteArrayOf(1,2,3))
println("Current buffer size: ${baos.size()}")
// 3. toByteArray를 사용하여 완성된 ByteArray 획득
val finalBytes: ByteArray = baos.toByteArray()
}
ByteArrayOutputStream의 가장 중요한 역할은 'toByteArray()' 메서드다.
- write()을 통해 버퍼에 누적된 모든 바이트를 새로운 독립적인 ByteArray 객체로 복사하여 반환한다.
- 이 메서드는 버퍼 내용을 비우지 않고, ByteArrayOutputStream 객체는 계속해서 추가 쓰기 작업에 사용될 수 있다.
ByteArrayInputStream (읽기)
ByteArrayInputStream은 이미 존재하는 ByteArray를 마치 소켓이나 파일처럼 순차적으로 읽을 수 있는 스트림으로 변환하여 사용한다.
import java.io.ByteArrayInputStream
fun readFromByteArrayStream(source: ByteArray) {
// 1. ByteArrayInputStream 객체 생성
val bais = ByteArrayInputStream(source)
// 2. 데이터 읽기
var firstByte = bais.read()
if (firstByte != -1) {
println("첫 번째 바이트 (Int): $firstByte")
}
// 3. 버퍼를 사용하여 여러 바이트 한 번에 읽기 예시 (효율적)
val buffer = ByteArray(5) // 5바이트를 읽을 버퍼 생성
val bytesRead = bais.read(buffer) // 읽은 바이트 수를 반환
if (bytesRead > 0) {
println("버퍼로 읽은 바이트 수: $bytesRead")
val part = String(buffer, 0, bytesRead, StandardCharsets.UTF_8)
println("읽은 내용: \"$part\"")
}
// 4. 모든 데이터를 끝까지 읽을 때까지 반복
println("남은 데이터 건너뛰기: ${bais.available()} 바이트")
while (bais.read() != -1) {
// 모든 데이터 소진
}
println("-> 읽기 완료. ${bais.read()}는 -1 (EOF)")
}
read() 및 read(buffer)는 스트림에서 바이트를 읽어 Int 값으로 반환한다. 더 이상 읽을 데이터가 없으면 -1을 반환한다.
DataOutputStream / DataInputStream (구조화된 직렬화/역직렬화)
바이트 배열은 단순한 바이트의 나열일 뿐이다. Int나 Long 같은 특정 자료형으로 읽고 쓰려면, 몇 바이트를 읽어야 해당 자료형이 완성되는지 지정해야 한다. 이를 돕는 것이 DataOutputStream과 DataInputStream이다.
이 클래스들은 ByteArrayOutputStream과 ByteArrayInputStream 위에 래핑(Wrapping)되어 작동하며, 4 바이트는 Int, 8 바이트는 Long 등으로 규격화하여 처리한다.
1. DataOutputStream : 원시 타입을 바이트 배열에 작성
DataOutputStream은 항상 빅 엔디안(Big-Endian, 네트워크 바이트 순서) 방식으로 데이터를 저장한다.
import java.io.DataOutputStream
import java.io.DataInputStream
import java.io.ByteArrayOutputStream
fun serializeData() {
val baos = ByteArrayOutputStream()
val dos = DataOutputStream(baos) // ByteArrayOutputStream 래핑
// 1. 자료 유형별 데이터 작성
val length = 250
dos.writeInt(length) // Int (4 바이트)
val type: Short = 5
dos.writeShort(type.toInt()) // Short (2 바이트)
dos.writeBoolean(true) // Boolean (1바이트)
dos.writeUTF("Serialization") // 문자열(UTF-8)
dos.flush()
dos.close()
val serializedBytes = baos.toByteArray()
println("-> 최종 데이터 크기: ${serializedBytes.size} 바이트")
// (4B Int) + (2B Short) + (1B Boolean) + (문자열 길이 + 문자열 바이트)
}
2.DataInputStream : 구조화된 바이트 배열을 원시 타입으로 읽기
DataOutputStream에서 자료가 쓰여진 순서와 자료형 그대로 읽어 들여야 한다.
import java.io.ByteArrayInputStream
import java.io.DataInputStream
fun deserializeData(serializedBytes: ByteArray) {
val bais = ByteArrayInputStream(serializedBytes)
val dis = DataInputStream(bais) // ByteArrayInputStream 래핑
// 작성된 순으로 데이터 읽기
val length = dis.readInt() // 1. Int (4바이트)
val type = dis.readShort() // 2. Short (2바이트)
val success = dis.readBoolean() // 3. Boolean (1바이트)
val message = dis.readUTF() // 4. 문자열(UTF-8)
println("Length (Int): $length")
println("Type (Short): $type")
println("Success (Boolean): $success")
println("Message (String): $message")
}
고급 활용 및 네트워킹
네트워크 바이트 순서 (Endianness)
엔디안(Endianness)이란, 멀티바이트 데이터(예: 4바이트 정수 Int나 8바이트 정수 Long)를 메모리나 바이트 스트림에 저장할 때 바이트를 배열하는 순서를 의미한다.

빅 엔디안 (Big-Endian)
- Big End First : 가장 중요한 바이트(MSB, Most Significant Byte)를 가장 낮은 메모리 주소(배열의 0번 인덱스)에 저장한다.
- 사람이 읽는 방식과 같다. (예: 숫자 1234를 쓸 대, 1이 가장 중요하고 먼저 나온다.)
리틀 엔디안 (Little-Endian)
- Little End First : 가장 덜 중요한 바이트(LSB, Least Significant Byte)를 가장 낮은 메모리 주소(배열의 0번 인덱스)에 저장한다.
- 대부분의 개인용 컴퓨터(x86, x64 아키텍처)가 사용하는 방식이다.
네트워크 통신에서 빅 엔디안을 사용하는 이유
네트워크 통신(TCP/IP 프로토콜)에서는 빅 엔디안을 표준으로 사용한다. 이를 네트워크 바이트 순서(Network Byte Order) 라고 한다.
데이터 전송 전에 항상 빅 엔디안으로 변환하고, 수신 후 필요하다면 로컬 시스템의 엔디안으로 다시 변환해야 한다.
패킷 처리 및 프로토콜 구현 (Length Prefixing)
ByteArray를 이용하여 안정적인 데이터 통신을 하려면 패킷(Packet) 구조를 정의하고 구현해야 한다.
네트워크 패킷은 일반적으로 다음 두 부분으로 구성된다.
- 헤더 (Header) : 패킷을 설명하는 메타데이터(예: 패킷 총 길이, 패킷 종류, 세션 ID 등), 고정된 크기로 정의된다.
- 바디 (Body) : 실제 데이터 (예: 채팅 메시지, 파일 내용) 등 가변 크기를 가진다.
// 채팅 서버 프로토콜의 예시: [Length (4B)] [Type (4B)] [Body (가변)]
val HEADER_SIZE = 8 // Length 4B + Type 4B
// 수신 로직 (개념):
// 1. 소켓에서 정확히 HEADER_SIZE(8B)를 읽어옵니다.
// 2. 이 8B를 DataInputStream을 통해 Length(Int)와 Type(Int)으로 분리합니다.
// 3. Length 필드에 저장된 값만큼 소켓에서 Body를 추가로 읽어옵니다.
// 4. Header와 Body를 합쳐 완전한 패킷(ByteArray)을 구성합니다.
fun readPacket(inputStream: InputStream): Packet {
val dis = DataInputStream(inputStream)
// 1. Length 필드 읽기 (4바이트 블로킹)
val length = dis.readInt() // 🚨 여기서 다음 바이트가 들어올 때까지 블로킹
// 2. Type 필드 읽기 (4바이트 블로킹)
val type = dis.readInt()
// 3. Body 바이트 읽기 (length - 8) 만큼 정확히 읽음
val bodySize = length - 8
val bodyBytes = ByteArray(bodySize)
dis.readFully(bodyBytes) // 🚨 bodySize 만큼 정확히 읽을 때까지 블로킹
// ... 패킷 객체 생성
}
length 필드가 있기 때문에, 서버는 남은 바이트 스트림에서 다음 패킷이 시작하기 전에 정확히 몇 바이트를 읽어야 하는지를 알 수 있어 데이터 경계를 정확히 구분할 수 있다.
참고 출처
- ByteArrayInputStream (Java Platform SE 8)
- ByteArrayOutputStream (Java Platform SE 8)
- JAVA IO - 바이트기반스트림 :ByteArrayInputStream과 ByteArrayOutputStream
Powered By. Gemini
'Backend' 카테고리의 다른 글
| Mockito-inline 5.2.0 설정 시 문제 해결 가이드 (Kotlin + Gradle + JDK17) (0) | 2025.10.30 |
|---|---|
| [Backend] JVM의 의존성 관리와 빌드 도구 분석 (0) | 2025.10.17 |
| [Backend] 서버-클라이언트 연결 (ServerSocket & Socket) (0) | 2025.10.16 |
| [Backend] Kotlin N:N Chat Application (Blocking Socket) (0) | 2025.10.14 |
| [Backend] Pub-Sub 패턴: 경쟁적 소비 모델 vs. Broadcast (0) | 2025.10.10 |