학습사항
- 스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O
- InputStream과 OutputStream
- Byte와 Character 스트림
- 표준 스트림 (System.in, System.out, System.err)
- 파일 읽고 쓰기
프로그램이 프로그램 내부의 데이터만 사용하는 경우는 드물 것이다. 필요에 따라 외부의 데이터를 가져와 사용해야 하는데 디스크나 다른 메모리 공간에 있는 그리고 더 나아가 네트워크를 타고 들어오는 프로그램 외부의 데이터를 입력받고 또 출력해야하는 경우가 있을 것 이다.
I/O 는 Input 과 Output 의 준말로, 프로그램 외부로 부터의 입력과 외부로의 출력을 의미한다.
버퍼는 CPU 와 입출력장치의 속도 차이에서 나오는 비효율성을 줄이기 위해 등장한 개념이다. CPU의 연산속도는 입출력장치가 데이터를 찾아서 가져오거나 필요한 곳에 데이터를 저장하는 속도에 비해 훨씬 빠르다. 따라서 그 속도 차이를 해결하기 위해 메모리 공간에 버퍼라는 공간을 두고 (Input 의 경우) 그 공간에 입력장치가 읽은 데이터들을 차곡차곡 쌓아두고 CPU 는 다른 연산을 하다 필요한 시점에 버퍼공간에 쌓여있는 데이터들을 한번에 연산할 수 있게된다.
자바를 개발한 개발자들은 I/O 를 위한 클래스들을 만들어 놓았다.
해당 클래스들은 java.io 패키지와 java.nio 패키지에 만들어져 있다.
java.nio 보다 먼저 만들어진 java.io 패키지에 속한 클래스들은 기본적으로 stream 기반의 I/O 를 지원한다.
stream 기반의 I/O 는 입력과 출력이 단방향으로만 이루어진다.
즉, input stream 을 통해서 입력을 받을 수 있고 output stream 을 통해서 출력할 수 있다.
그리고 java.io 는 blocking 한 I/O 를 기반한다. 즉, thread 에서 입출력을 위한 메소드 호출시 데이터를 읽거나 쓰기 전까지 해당 thread 는 blocking 된다.
java.io
1. stream 기반의 I/O
2. blocking
다음으로 new io 를 뜻하는 java.nio 에 속한 클래스들은 channel 기반의 I/O 를 지원한다.
channel 기반의 I/O 는 입력과 출력이 양방향으로 가능한 통로를 제공한다.
또 java.nio 는 기본적으로 입출력시 Buffer 사용한다 물론, java.io 에서도 필터스트림을 활용하면 buffer 를 활용할 수 있다는 점에서 많은 차이가 있는 것은 아니지만, java.nio 의 ByteBuffer 클래스의 allocateDirect() 메소드를 사용하면 커널 버퍼를 사용할 수도 있다. 기본적으로 다른 모든 버퍼는 OS 의 버퍼에서 JVM 내부의 버퍼로 데이터를 옮기는 작업이 들어가는데 커널 버퍼를 활용하면 그러한 작업을 하지 않아도 되니 더 빠른 입출력이 가능해진다. 단, allocateDirect() 는 내부적으로는 C언어를 호출해 시스템 메모리 영역에 버퍼를 생성하고 해제하는데 이 과정에서 드는 비용이 있음으로 한 번 만들고 오래 사용하는 경우에 사용할 때 효용이 있다.
그리고 java.nio 는 non-blocking 을 지원한다
java.nio
1. channel 기반의 I/O
2. 기본적으로 buffer 사용
3. non-blocking 을 지원
Byte Stream 기반의 모든 입력관련 클래스들의 superclass 격인 추상클래스로
AudioInputStream, ByteArrayInputStream, FileInputStream, FilterInputStream, InputStream, ObjectInputStream, PipedInputStream, SequenceInputStream, StringBufferInputStream
같은 클래스들이 상속하고 있다.
public class InputOutputStream {
public static void main(String[] args) throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream("/Users/jhkim/Desktop/velog/whiteship-study/src/main/java/weeks13/test.txt");
int i = 0;
try{
while ((i = fileInputStream.read()) != -1){
System.out.write(i);
}
} catch (IOException ioe){
ioe.printStackTrace();
} finally {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
--->
test.txt 파일 내용
This is test
This is test
---------------------
console 출력 내용
This is test
This is test
InputStream 은 byte 단위로 파일을 읽어온다. 또한 Buffer를 사용하지 않음으로 큰 데이터를 읽어오는데 있어서 성능이 많이 부족한 편이다.
Buffere 를 활용한 코드와 얼마나 차이가 나는지 다음코드를 통해 살펴보도록 하자.
//InputStream
long start = System.currentTimeMillis();
FileInputStream fileInputStream = new FileInputStream("/Users/jhkim/Desktop/velog/whiteship-study/src/main/java/weeks13/test.txt");
int i = 0;
try{
while ((i = fileInputStream.read()) != -1){
System.out.write(i);
}
} catch (IOException ioe){
ioe.printStackTrace();
} finally {
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("걸린시간 : " + (System.currentTimeMillis() - start));
-->
파일내용~~
걸린시간 : 10817
// 버퍼를 사용하는 BufferedInputStream
long start = System.currentTimeMillis();
BufferedInputStream bufferedStream = new BufferedInputStream(new FileInputStream("/Users/jhkim/Desktop/velog/whiteship-study/src/main/java/weeks13/test.txt"), 1024);
int j = 0 ;
try{
while ((j = bufferedStream.read()) != -1){
System.out.write(j);
}
} catch (IOException ioe){
ioe.printStackTrace();
} finally {
try {
bufferedStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
System.out.println("걸린시간 : " + (System.currentTimeMillis() - start));
-->
파일내용~~
걸린시간 : 1860
거의 10배에 가까운 속도차이를 내고있는데, 파일의 크기가 커질 수록 성능의 차이는 더 크게 날 것이다.
Byte Stream 기반의 모든 출력관련 클래스들의 superclass 격인 추상클래스로
ByteArrayOutputStream, FileOutputStream, FilterOutputStream, ObjectOutputStream, OutputStream, PipedOutputStream
다음의 클래스들이 상속하고 있다.
File file = new File("/Users/jhkim/Desktop/velog/whiteship-study/src/main/java/weeks13/output.txt");
FileOutputStream fileOutputStream = new FileOutputStream(file);
int data = 0;
try{
while ((data = System.in.read()) != -1){
fileOutputStream.write(data);
}
} catch (IOException ioe){
} finally {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
----> console 창에
aksdfakjsdfakjsdbfajhsdbfasjdhbfa
asdfasfasdfasdfs
adfasdasdfsadf
asdfasdfsdf
nwefnwkefnwkefnwejfanwekfnawefnawefnawekfanwekfjawnefajwefanwekfawenjkf
---- 다음과 같은 값을 입력하자 output.txt 파일이 생성되고
aksdfakjsdfakjsdbfajhsdbfasjdhbfa
asdfasfasdfasdfs
adfasdasdfsadf
asdfasdfsdf
nwefnwkefnwkefnwejfanwekfnawefnawefnawekfanwekfjawnefajwefanwekfawenjkf
---- 같이 입력되는 것을 확인할 수 있었다.
앞서서 살펴폰 InputStream 과 OutputStream 은 대표적인 ByteStream 이다. ByteStream 은 이름처럼 1바이트 단위로 데이터를 입출력한다.
그에 반해 CharacterStream 은 2바이트 단위로 데이터를 전송하는데, 자바는 기본적으로 character 값들이 유니코드 규약에 맞춰져 있다. 그리고 유니코드는 기본단위가 2바이트임으로 문자를 입출력할때, CharacterStream 을 쓰는 것이 가장 적절하다고 할 수 있다.
CharacterStream 은 클래스명에 Reader, Writer 가 들어가 있다.
- ByteStream 기반의 InputStream Classes
- ByteStream 기반의 OutputStream Classes
- CharacterStream 기반의 Reader Classes
- CharacterStream 기반의 Writer Classes
사진출처 : https://www.javatpoint.com/
표준 스트림이란 프로그램과 환경(키보드, 모니터, 콘솔..) 사이에 연결된 입출력 스트림을 의미한다.
표준 입출력 스트림은 JVM 이 메모리에 올라갈때 자동생성해주는 것 같다 (?)
콘솔로부터의 입력을 받는 표준 스트림을 가리키는 상수이다. 해당객체는 JVM 이 메모리에 올라갈때 생성된다고 한다. InputStream 으로 byte 단위로 입력을 받을 수 있다.
System.out.print("입력 : ");
int input = System.in.read();
System.out.println("출력 : " + (char)input);
-----> console
입력 : h
출력 : h
-----> 만약 한글을 입력한다면?
입력 : 안
출력 : ì
* 한글은 유니코드로 2byte 단위이기 때문에 InputStream 으로는 1바이트 밖에 읽지 못해,
원하지 않는 값이 출력 되었다
InputStream 은 ByteStream 으로 한글을 입력받는데 있어서 어려움이 있다.
이 경우에는 CharacterStream 인 InputStreamReader 을 활용할 수 있다.
System.out.print("입력 : ");
InputStreamReader inputStreamReader = new InputStreamReader(System.in);
int input = inputStreamReader.read();
System.out.println("출력 : " + (char)input);
----> console
입력 : 안
출력 : 안
만약 2byte 가 넘는 데이터를 읽고싶다면, BufferedReader 를 활용할 수도 있을 것이다.
System.out.print("입력 : ");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
String inputData = bufferedReader.readLine();
System.out.println("출력 : " + inputData);
----> console
입력 : 안녕하세요
출력 : 안녕하세요
혹은, Scanner 를 사용할 수 도 있다.
System.out.print("입력 : ");
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();
System.out.println("출력 : " + inputData);
----> console
입력 : 안녕하세요
출력 : 안녕하세요
콘솔로 출력을 하는 표준 스트림을 가리키는 상수이다.
대표적인 사용의 예시로 System.out.println() 을 생각해볼 수 있다.
표준 에러 출력 장치를 가리키는 상수이다
InputStream 과 OutputStream 의 예시들에서 파일을 읽고 써보았다.
FileInpuStream, FileReader 을 사용해서 파일을 입력받을 수 있는데,
BufferedInputStream 혹은 BufferedReader 로 감싸 파일의 용량이 큰 경우 효율적으로 파일을 읽을 수 있다.
try {
BufferedReader bufferedReader = new BufferedReader(new FileReader(new File("/Users/jhkim/Desktop/velog/whiteship-study/src/main/java/weeks13/output.txt")));
String line = "";
while ((line=bufferedReader.readLine()) != null){
System.out.println(line);
}
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
-----> console
aksdfakjsdfakjsdbfajhsdbfasjdhbfa
asdfasfasdfasdf
adfasdasdfsadf
asdfasdfsdf
nwefnwkefnwkefnwejfanwekfnawefnawefnawekfanwekfjawnefajwefanwekfawenjkf
다음으로 FileWriter 를 BufferedWriter 로 감싸 파일을 생성해보자.
File file = new File("/Users/jhkim/Desktop/velog/whiteship-study/src/main/java/weeks13/newFile.txt");
try {
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file));
bufferedWriter.write("Hello");
bufferedWriter.newLine();
bufferedWriter.write("!!!");
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
Reference
https://www.baeldung.com/java-io-vs-nio
https://codevang.tistory.com/155?category=827591
https://www.javatpoint.com
스터디 깃헙주소 : https://github.com/whiteship/live-study/issues/13
예제코드 깃헙레포 : https://github.com/JadenKim940105/whiteship-study/tree/master/src/main/java/weeks13