| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 개발자팁
- 파이썬
- JVM
- Java
- 가비지컬렉션
- 개발공부
- 예외처리
- 클린코드
- 코딩인터뷰
- 객체지향
- HashMap
- 백준
- 코딩공부
- 자바
- 정렬
- 자료구조
- 자바프로그래밍
- 메모리관리
- 코딩테스트준비
- 자바기초
- 프로그래밍기초
- 코딩테스트팁
- 개발자취업
- 멀티스레드
- 코딩테스트
- 프로그래머스
- 알고리즘공부
- 자바공부
- 자바개발
- 알고리즘
- Today
- Total
코드 한 줄의 기록
Java I/O 완벽 가이드: 바이트와 문자 스트림의 차이를 정확히 이해하기 본문
Java를 공부하면서 I/O 관련 코드를 작성하다 보면 자연스럽게 마주하게 되는 의문이 있습니다. 왜 FileInputStream과 FileReader가 따로 있을까? BufferedInputStream과 BufferedReader의 차이는 뭘까? 이번 글에서는 제가 공부하면서 정리한 Java I/O의 핵심 개념들을 여러분과 함께 나누고 싶습니다.
스트림이란 무엇인가?
먼저 기본부터 시작해봅시다. Java에서 말하는 '스트림'은 물이 한쪽 방향으로 흐르는 것처럼, 데이터가 일방향으로 연속적으로 흐르는 것을 의미합니다. 프로그램이 외부로부터 데이터를 읽거나 외부로 데이터를 보낼 때 이 스트림을 통해 이루어집니다.
중요한 특징이 하나 있습니다. 스트림은 단방향이라는 것입니다. 따라서 동시에 입력과 출력을 처리하려면 입력 스트림과 출력 스트림 두 개가 필요합니다. 마치 수도처럼요. 물을 받는 수도와 물을 버리는 수도가 따로 있는 것처럼요.
또 하나 알아둬야 할 특징은 FIFO(First In First Out) 구조라는 것입니다. 먼저 보낸 데이터가 먼저 받아지고, 중간에 건너뛸 수 없이 순서대로 데이터를 주고받습니다.
바이트 스트림 vs 문자 스트림: 근본적인 차이
Java I/O의 가장 중요한 개념은 바이트 스트림과 문자 스트림의 구분입니다. 이것이 InputStream/OutputStream과 Reader/Writer로 나뉘는 이유이기도 합니다.
바이트 스트림: InputStream과 OutputStream
바이트 스트림은 데이터를 1byte 단위로 입출력합니다. 이미지, 동영상, 음악 파일 등 모든 종류의 바이너리 데이터를 다룰 수 있어서 가장 기본이 되는 스트림입니다.
InputStream은 바이트 기반 입력 스트림의 최상위 추상 클래스이고, OutputStream은 바이트 기반 출력 스트림의 최상위 추상 클래스입니다. 이들의 주요 메서드들을 살펴보겠습니다.
// InputStream의 주요 메서드
int read() // 1바이트를 읽음, 더 이상 읽을 데이터가 없으면 -1 반환
int read(byte[] b) // 바이트 배열 크기만큼 읽어서 배열에 저장
int read(byte[] b, int off, int len) // 최대 len개의 바이트를 off 위치부터 저장
void close() // 스트림을 닫고 자원 반납
// OutputStream의 주요 메서드
void write(int b) // 1바이트를 쓴다 (int에서 끝 1바이트만 사용)
void write(byte[] b) // 바이트 배열 전체를 쓴다
void write(byte[] b, int off, int len) // 바이트 배열의 일부를 쓴다
void flush() // 버퍼에 남은 내용을 출력소스로 내보낸다
void close() // 스트림을 닫고 자원 반납
중요한 것은 read() 메서드가 왜 반환 타입이 int일까 하는 부분입니다. byte 타입은 0~255의 범위만 표현할 수 있는데, 데이터가 없을 때 -1을 반환해야 하기 때문입니다. 따라서 반환 타입을 int로 확장하여 0~255와 -1까지 표현할 수 있도록 한 것입니다.
문자 스트림: Reader와 Writer
문자 스트림은 데이터를 문자 단위(char)로 입출력합니다. Java의 char는 2byte이기 때문에, 바이트 스트림으로 한글 같은 멀티바이트 문자를 제대로 처리하기 어렵습니다.
예를 들어 한글 "안"은 UTF-8 인코딩으로 3byte인데, 바이트 스트림이 1byte씩 읽으면 "안"을 제대로 읽지 못합니다. 이런 문제를 해결하기 위해 Reader와 Writer가 제공됩니다.
// Reader의 주요 메서드
int read() // 1개 문자를 읽음, 범위는 0~65535, 없으면 -1 반환
int read(char[] c) // char 배열 크기만큼 읽어서 배열에 저장
int read(char[] c, int off, int len) // 최대 len개의 문자를 off 위치부터 저장
boolean ready() // 읽을 준비가 되어 있는지 확인
void close() // 스트림을 닫고 자원 반납
// Writer의 주요 메서드
void write(int c) // 1개 문자를 쓴다 (int에서 끝 2byte만 사용)
void write(char[] c) // char 배열 전체를 쓴다
void write(String str) // 문자열을 쓴다
void write(char[] c, int off, int len) // char 배열의 일부를 쓴다
void write(String str, int off, int len) // 문자열의 일부를 쓴다
void flush() // 버퍼에 남은 내용을 출력소스로 내보낸다
void close() // 스트림을 닫고 자원 반납
Reader와 Writer는 단순히 char 단위로 처리한다는 것만이 아니라, 다양한 문자 인코딩과 Java의 유니코드 간의 자동 변환을 처리합니다. 이것이 정말 중요한 포인트입니다.
실제 사용: FileInputStream/FileOutputStream vs FileReader/FileWriter
바이트 스트림의 구현체로는 FileInputStream과 FileOutputStream이 있고, 문자 스트림의 구현체로는 FileReader와 FileWriter가 있습니다.
바이트 스트림 사용 예시
try {
// 바이트 기반으로 파일에서 데이터 읽기
FileInputStream fis = new FileInputStream("test.txt");
byte[] buffer = new byte;
int readBytes = fis.read(buffer);
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
이 코드에서 한글 파일을 읽으면 문제가 생깁니다. 한글을 이루는 여러 바이트가 분리되어 읽혀서 올바른 문자로 해석되지 않습니다.
문자 스트림 사용 예시
try {
// 문자 기반으로 파일에서 데이터 읽기
FileReader fr = new FileReader("test.txt");
char[] buffer = new char;
int readChars = fr.read(buffer);
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
문자 스트림을 사용하면 한글을 포함한 텍스트 파일을 안전하게 읽을 수 있습니다. 문자 인코딩 변환이 자동으로 처리되기 때문입니다.
보조 스트림으로 성능 최적화하기
여기까지가 기본 스트림입니다. 이제 성능을 개선할 수 있는 보조 스트림에 대해 알아봅시다. 보조 스트림은 실제 데이터를 입출력하지 않고, 기반 스트림에 추가 기능을 제공합니다.
버퍼링으로 I/O 성능 향상
가장 흔히 사용되는 보조 스트림은 버퍼 기능을 제공하는 것입니다. BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter가 있습니다.
버퍼가 없을 때를 생각해봅시다. 파일에 1000바이트를 쓸 때 1바이트씩 쓰면 하드 디스크에 1000번 접근해야 합니다. 이것은 매우 비효율적입니다.
버퍼를 사용하면 메모리에 임시로 데이터를 저장했다가 버퍼가 가득 차면 한 번에 하드 디스크로 보냅니다. 결과적으로 하드 디스크 접근 횟수가 크게 줄어들어 성능이 향상됩니다.
입력도 마찬가지입니다. 버퍼에 미리 데이터를 읽어두면, 프로그램은 버퍼에서 고속으로 데이터를 읽을 수 있습니다.
BufferedInputStream과 BufferedOutputStream
try {
// 기반 스트림과 보조 스트림을 함께 사용
FileInputStream fis = new FileInputStream("large_image.jpg");
BufferedInputStream bis = new BufferedInputStream(fis);
// 또는 한 줄로
// BufferedInputStream bis = new BufferedInputStream(
// new FileInputStream("large_image.jpg")
// );
byte[] buffer = new byte;
int readBytes;
while ((readBytes = bis.read(buffer)) != -1) {
// 데이터 처리
}
bis.close(); // 보조 스트림을 닫으면 기반 스트림도 함께 닫힘
} catch (IOException e) {
e.printStackTrace();
}
BufferedReader와 BufferedWriter
문자 스트림의 보조 스트림은 추가로 유용한 기능을 제공합니다.
try {
// BufferedReader는 readLine() 메서드를 제공!
FileReader fr = new FileReader("data.txt");
BufferedReader br = new BufferedReader(fr);
String line;
while ((line = br.readLine()) != null) {
// 한 줄씩 처리할 수 있음 - 매우 편함!
System.out.println(line);
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
BufferedReader의 readLine() 메서드는 정말 유용합니다. 줄 단위로 데이터를 읽을 수 있어서 코드가 간결해집니다. 알고리즘 문제를 풀 때도 자주 사용하는 방식이죠.
바이트와 문자 스트림의 성능 비교
실제로 얼마나 성능 차이가 나는지 궁금할 겁니다. 다음은 일반적인 벤치마크 결과입니다.
- FileInputStream (버퍼 없음): 가장 느림 (1바이트씩 읽기)
- BufferedInputStream: 매우 빠름 (버퍼링)
- FileReader: 느림 (1문자씩 읽기)
- BufferedReader: 가장 빠름 (버퍼링 + 줄 단위 처리)
따라서 실전에서는 대부분 BufferedReader나 BufferedWriter를 사용합니다.
문자 인코딩 문제와 InputStreamReader/OutputStreamWriter
여기서 한 가지 더 알아야 할 것이 있습니다. 바이트 스트림을 사용하면서도 문자를 정확히 처리하고 싶을 때가 있습니다. 이때 사용하는 것이 변환 스트림인 InputStreamReader와 OutputStreamWriter입니다.
// 바이트 스트림을 문자 스트림으로 변환
try {
FileInputStream fis = new FileInputStream("file.txt");
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line); // 한글도 정확히 출력됨
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
InputStreamReader는 바이트 스트림을 문자 스트림으로 변환합니다. 생성자에서 문자 인코딩을 지정할 수 있습니다. 이것은 표준 입력(System.in)에서 한글을 입력받을 때 자주 사용됩니다.
// 키보드에서 한글을 안전하게 읽기
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in, "UTF-8")
);
String input = br.readLine();
OutputStreamWriter는 반대 방향입니다. 문자를 바이트로 변환하여 바이트 스트림으로 보냅니다.
try {
FileOutputStream fos = new FileOutputStream("output.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
BufferedWriter bw = new BufferedWriter(osw);
bw.write("안녕하세요! 한글 출력입니다.");
bw.flush();
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
실전 팁: flush()와 close() 이해하기
버퍼를 사용할 때 꼭 알아야 할 두 가지 메서드가 있습니다.
flush(): 버퍼에 남아 있는 데이터를 즉시 출력 대상으로 보냅니다. 버퍼가 가득 차지 않아도 지금 바로 보내고 싶을 때 사용합니다.
close(): 스트림을 닫습니다. close()를 호출하면 내부적으로 flush()를 자동으로 실행한 후 자원을 반납합니다.
따라서 대부분의 경우 flush()를 명시적으로 호출할 필요는 없습니다. close()만 제대로 호출하면 됩니다. 다만 프로그램이 계속 실행되어야 하면서 중간에 데이터를 확실히 보내고 싶을 때 flush()를 사용합니다.
try {
BufferedWriter bw = new BufferedWriter(
new FileWriter("result.txt")
);
bw.write("첫 번째 줄");
bw.flush(); // 이 시점에 파일에 기록됨
// 계산 작업...
bw.write("두 번째 줄");
bw.close(); // flush() 자동 실행 후 자원 반납
} catch (IOException e) {
e.printStackTrace();
}
스트림 체이닝으로 유연한 I/O 처리
Java I/O의 강력한 기능 중 하나는 스트림을 연결(체이닝)할 수 있다는 것입니다.
// 여러 보조 스트림을 연결
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"),
"UTF-8"
)
);
이 예제를 풀어 설명하면:
FileInputStream: 파일에서 바이트를 읽음InputStreamReader: 바이트를 UTF-8로 문자로 변환BufferedReader: 변환된 문자를 버퍼링하여 줄 단위로 읽음
이런 식으로 필요한 기능을 조합하여 사용할 수 있습니다. 매우 유연합니다.
어떤 스트림을 사용해야 할까?
지금까지의 내용을 정리해보겠습니다.
텍스트 파일을 읽을 때
BufferedReader br = new BufferedReader(new FileReader("file.txt"));
String line;
while ((line = br.readLine()) != null) {
// 처리
}
br.close();
텍스트 파일을 쓸 때
BufferedWriter bw = new BufferedWriter(new FileWriter("file.txt"));
bw.write("텍스트 데이터");
bw.close();
이미지 같은 바이너리 파일을 다룰 때
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("image.jpg")
);
byte[] buffer = new byte;
int readBytes;
while ((readBytes = bis.read(buffer)) != -1) {
// 바이너리 처리
}
bis.close();
키보드 입력을 받을 때
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in)
);
String input = br.readLine();
Java I/O를 제대로 이해하려면 바이트와 문자의 구분이 핵심입니다. 바이트 스트림과 문자 스트림, 그리고 버퍼링으로 인한 성능 향상까지 이해하면 Java 프로그래밍이 한층 수월해집니다.
특히 문자 인코딩 문제로 한글이 깨지는 현상은 Java 개발자라면 한두 번쯤은 겪게 되는데, 이 개념들을 정확히 이해하고 있으면 문제 해결이 정말 간단해집니다.
다음에는 이 개념들을 바탕으로 실전 프로젝트에서 어떻게 활용되는지, 그리고 NIO(New I/O) 같은 고급 주제까지 다루고 싶습니다. 함께 배우고 성장하는 개발자가 될 수 있기를 바랍니다!
Java 소켓과 HTTP 통신: 기초부터 클라이언트 구현까지 완벽 정리
네트워크 프로그래밍을 배우면서 "소켓"이라는 용어를 처음 접하는 개발자들은 이게 정확히 뭔지, HTTP와는 어떻게 다른지 헷갈리는 경우가 많습니다. 저도 처음엔 그랬거든요. 하지만 현장에서
byteandbit.tistory.com
'JAVA' 카테고리의 다른 글
| Java 직렬화 완벽 가이드: 직렬화, transient 키워드, 역직렬화 보안까지 한 번에 이해하기 (0) | 2025.11.10 |
|---|---|
| Java 파일 유틸리티 개발 시 성능, 자원, 예외처리를 모두 고려하는 방법 (0) | 2025.11.10 |
| Java 소켓과 HTTP 통신: 기초부터 클라이언트 구현까지 완벽 정리 (0) | 2025.11.08 |
| 자바 파일 및 디렉터리 완벽 가이드: Path와 Files로 배우는 실전 활용법 (0) | 2025.11.06 |
| Java Stream Collectors 완전 정복: groupingBy와 partitioningBy로 데이터 그룹화하기 (0) | 2025.11.06 |