JAVA (20) - I/O 기초(스트림)
1. 개요
자바에서 I/O(Input/Output)는 프로그램이 외부 장치(키보드, 파일, 네트워크, 모니터 등)와 데이터를 주고받는 입출력 작업을 의미한다. 자바는 모든 입출력을 스트림(Stream)이라는 일관된 개념을 통해 처리하며, 데이터의 단위에 따라 바이트 기반 스트림과 문자 기반 스트림으로 구분하여 지원한다.
2. 스트림(Stream)의 개념과 특징
자바에서의 스트림은 데이터가 이동하는 통로(흐름)를 의미한다. (※ Java 8의 Stream API와는 다른 개념이다.)
- 단방향성: 스트림은 단방향으로만 데이터가 흐른다. 따라서 입력(Input)과 출력(Output)을 동시에 수행하려면 입력 스트림과 출력 스트림을 각각 따로 생성해야 한다.
- FIFO (First In First Out): 먼저 들어간 데이터가 먼저 나오는 구조를 가진다.
- 지연 가능성: 데이터가 전부 들어오거나 출력될 때까지 프로그램이 대기 상태(Blocking)에 빠질 수 있다.
3. 바이트 기반 스트림 (Byte Stream)
그림, 음악, 동영상, 실행 파일 등 모든 종류의 이진 데이터(Binary Data)를 1바이트(byte) 단위로 입출력하는 스트림이다.
- 최상위 클래스:
InputStream(입력),OutputStream(출력) - 둘 다 추상 클래스다. - 주요 구현 클래스:
FileInputStream,FileOutputStream,ByteArrayInputStream,BufferedInputStream등
1
2
3
4
5
6
7
8
9
10
11
12
// 파일 복사 예시 (바이트 단위)
try (FileInputStream fis = new FileInputStream("input.png");
FileOutputStream fos = new FileOutputStream("output.png")) {
int data;
while ((data = fis.read()) != -1) { // 1바이트씩 읽기 (-1은 파일의 끝)
fos.write(data); // 1바이트씩 쓰기
}
} catch (IOException e) {
e.printStackTrace();
}
4. 문자 기반 스트림 (Character Stream)
텍스트 문자를 입출력할 때 사용하는 스트림이다. 자바에서 char는 2바이트(Unicode)를 사용하므로, 1바이트 단위인 바이트 스트림으로 인코딩된 텍스트를 처리할 경우 깨짐 현상이 발생할 수 있다. 이를 방지하기 위해 2바이트(문자) 단위로 처리한다.
- 최상위 클래스:
Reader(입력),Writer(출력) - 둘 다 추상 클래스다. - 주요 구현 클래스:
FileReader,FileWriter,BufferedReader,BufferedWriter등
1
2
3
4
5
6
7
8
9
10
// 텍스트 파일 읽기 예시 (문자 단위)
try (FileReader fr = new FileReader("example.txt")) {
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
} catch (IOException e) {
e.printStackTrace();
}
헷갈리는 부분
한글은 3바이트를 차지하는 경우가 많은데 그렇다면 2바이트씩 모으면 깨져야하는 것이 아닌가?
결론부터 말하면 전혀 잘리지 않는다.
문자 기반 스트림이 “2바이트 단위로 처리한다”는 말은 파일을 2바이트씩 무작정 툭툭 잘라서 읽는다는 뜻이 아니기 때문이다.
(1). “2바이트 단위로 처리한다”의 진짜 의미
여기서 ‘2바이트’는 파일의 저장 단위가 아니라, 자바 프로그램 내부(JVM 메모리)에서 문자를 다루는 기본 자료형인 char의 크기(2바이트, UTF-16)를 의미한다.
즉, “문자 기반 스트림(Reader)은 2바이트 단위로 처리한다”라는 말은:
“외부 파일이 1바이트든, 2바이트든, 3바이트든 상관없이, 인코딩 규칙에 맞게 해석해서 자바의 온전한 1글자(
char, 2바이트) 단위로 완성시켜서 반환해 준다”
라는 뜻이다.
(2). UTF-8 한글(3바이트)을 읽을 때 일어나는 실제 과정
FileReader나 InputStreamReader 같은 문자 기반 스트림 내부에는 디코더(Decoder, 인코딩 해석기)가 탑재되어 있다.
파일에 UTF-8로 저장된 한글 “가” (0xEA, 0xB0, 0x80 -> 총 3바이트)가 있을 때 문자 기반 스트림은 다음과 같이 동작한다.
- 바이트 분석: 문자 스트림이 파일에서 첫 번째 바이트(
0xEA)를 읽는다. - 인코딩 판별: UTF-8 디코더가 “아, 이 바이트의 표식을 보니 이건 3바이트짜리 한글 데이터의 시작이구나!” 하고 알아챈다.
- 완전한 조합: 뒤이어 따라오는 2개 바이트(
0xB0,0x80)까지 총 3바이트를 한꺼번에 묶어서 읽어 들인다. (여기서 데이터 손실이나 잘림이 발생하지 않는다.) - 자바 문자로 변환: 읽어 들인 3바이트를 자바 메모리에서 다룰 수 있는 1개의 문자(
char‘가’, Unicode0xAC00)로 변환하여 출력해 준다.
(3). 비교해서 이해하기
- 바이트 기반 스트림 (
InputStream): - 인코딩 해석 기능이 없다.
- UTF-8 한글 “가” (3바이트)를 읽으면, 디코더가 없으므로 그냥 1바이트씩 3번에 걸쳐서 쪼개서 가져온다.
쪼개진 1바이트는 온전한 글자가 아니므로
(char)로 변환하면 글자가 깨진다.- 문자 기반 스트림 (
Reader): - 인코딩 해석 기능이 있다.
- UTF-8 한글 “가” (3바이트)를 읽으면, 디코더가 3바이트를 올바르게 통째로 묶어서 읽은 뒤 자바의
char(2바이트) 변수에 온전한 한 글자로 담아준다.
요약
UTF-8로 인코딩된 3바이트짜리 한글 데이터를 문자 기반 스트림으로 읽어도 잘리지 않는다.
문자 기반 스트림은 기계적으로 2바이트씩 잘라오는 것이 아니라, 파일의 인코딩(UTF-8)을 인식하여 3바이트를 올바르게 모은 후 자바의 문자인 char(2바이트) 단위로 완성해서 가져오기 때문이다.
5. 보조 스트림 (Filter Stream)
보조 스트림은 실제 데이터를 직접 읽거나 쓰는 기능은 없지만, 다른 주 스트림(Primary Stream)에 연결하여 성능을 향상시키거나 추가적인 편의 기능을 제공하는 스트림이다.
(1) 성능 향상 보조 스트림 (BufferedInputStream / BufferedReader 등)
내부 버퍼(Buffer)를 사용하여 데이터를 한 번에 묶어서 처리함으로써 입출력 횟수를 대폭 줄인다.
1
2
3
4
5
6
7
8
9
10
// BufferedReader를 이용한 라인 단위 텍스트 읽기
try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
String line;
while ((line = br.readLine()) != null) { // 줄 단위로 읽기
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
(2) 형변환 보조 스트림 (InputStreamReader / OutputStreamWriter)
바이트 스트림을 문자 스트림으로 변환해 주는 역할을 한다. 표준 입력(System.in)처럼 바이트 단위로 들어오는 입력을 텍스트 데이터로 변환할 때 많이 쓰인다.
6. 정리
자바의 I/O는 데이터를 주고받는 단방향 통로인 스트림(Stream)을 기반으로 동작한다. 이미지나 바이너리 파일은 1바이트 단위의 InputStream/OutputStream을 사용하고, 텍스트 파일은 2바이트(문자) 단위의 Reader/Writer를 사용하는 것이 기본이다. 입출력 성능을 극대화하기 위해서는 BufferedReader와 같은 버퍼 기반의 보조 스트림을 조합하여 사용하는 것이 바람직하다.