목표
자바의 Input과 Output에 대해 학습하기
학습할 것
- 스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O
- InputStream과 OutputStream
- Byte와 Character 스트림
- 표준 스트림 (System.in, System.out, System.err)
- 파일 읽고 쓰기
I/O (입출력)
- 프로그램에서 사용된 데이터를 영구적으로 저장하여 후에 다른 프로그램에서 사용하기 위해 필요한 기능
- 즉, 프로그램에서 생성된 데이터를 파일 형태로 컴퓨터의 하드 디스크나 보조 기억 장치에 저장한 다음 다른 프로그램에서 이 데이터를 사용하는 기능.
스트림 (Stream) / 버퍼 (Buffer) / 채널 (Channel) 기반의 I/O
Stream
- 스트림이란? 순서가 있는 데이터를 의미하는 추상적인 개념이며, 자바는 입출력을 위해 스트림을 사용한다.
- 사용자가 프로그램에서 스트림을 이용하여 입출력을 수행하면, 입출력 스트림을 실제 입출력 하드웨어 장치에 연결하는 것은 JVM에 의해 이루어진다.
- 즉, 스트림을 사용함으로써 자바 사용자는 입출력 수행 시 실제 입출력 하드웨어와 상관없이 일관된 개념으로 사용할 수 있다.
자바는 입출력을 위해 스트림을 생성하고 다루는 클래스들을 java.io 패키지로 제공하고 있으며, 이 패키지를 이용하면 사용자는 다양하고 복잡한 입출력 장치와 상관없이 일관된 방법으로 입출력을 수행할 수 있다.
Java NIO(New IO)는 Java용 IO API로 기존 IO API와는 다른 IO 모델을 제공한다.
기존 IO API에서는 byte stream과 character stream을 사용했지만,
Java NIO 에서는 buffer, Channel을 사용한다.
그리고 Java NIO는 Channel, Buffer, Selector를 핵심으로 구성된다.
Channel
- 보통 Java NIO의 모든 IO는 Channel로 부터 시작된다. Channel은 Stream과 비슷하다.
- Channel의 데이터를 버퍼로 읽을 수 있으며, 또한 버퍼에서 채널로 데이터를 쓸 수 있다.
아래 언급 할 buffer와 유사하지만 몇 가지 차이점이 있다.
- 스트림은 단방향(읽거나 쓰거나)이지만 채널은 양방향이다(둘다 가능하다)
- 채널은 비동기식으로 읽고 쓸 수 있다
- 채널은 항상 버퍼로 읽거나 쓸 수 있다
Java NIO에 구현되어 있는 채널들은
- FileChannel : 파일에 데이터를 읽고 쓴다
- DatagramChannel : 네트워크 UDP를 통해 데이터를 읽고 쓴다
- SocketChannel : 네트워크 TCP를 통해 데이터를 읽고 쓴다
- ServerSocketChannel : 웹서버 처럼 TCP 연결을 수신한다. 연결이 들어올 때마다 SocketChannel이 생성된다.
등 이 있다.
Buffer
- Java NIO의 버퍼는 Java NIO의 채널과 상호작용할 때 사용된다. 채널로부터 버퍼로 읽거나, 버퍼에서 읽어 채널에 데이터를 쓴다.
- 버퍼는 기본적으로 데이터를 쓸 수 있는 메모리 블록이며, 나중에 다시 읽을 수 있다.
- 이 메모리 블록은 NIO 버퍼 객체로 싸여 있으며, 메모리 블록으로 더 쉽게 작업하는 메소드를 제공한다.
버퍼 사용하여 데이터를 읽고 쓰는 4가지 단계는?
- 버퍼에 데이터 쓰기
- buffer.flip() 호출
- 버퍼에서 데이터 읽기
- buffer.clear() 또는 buffer.compact() 호출
버퍼에 데이터를 쓸 때 버퍼에서는 사용중인 데이터의 양을 추적한다.
데이터를 읽어야 할 경우에 flip() 메소드를 이용하여 버퍼를 쓰기 모드에서 읽기 모드로 전환해야 하며, 읽기 모드에서는 버퍼에 기록된 데이터를 읽을 수 있다.
모든 데이터를 읽은 후에는 버퍼를 지워야 다시 쓸 수 있다. clear() 또는 compact()를 호출하여 지울 수 있다.
clear() 는 전체 버퍼를 지우며, compact()는 읽은 데이터만 지운다.
다음은 Java NIO에 구현되어 있는 버퍼 타입이다
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
Byte 스트림과 Character 스트림
스트림 : 입출력 데이터의 추상적인 표현
스트림에는 문자 스트림과 바이트 스트림 두 가지 형태가 있다.
- Character 스트림 : 16비트 문자나 문자열들을 읽고 쓰기 위한 스트림
- 문자 스트림의 입출력을 위해서는 Reader, Writer 클래스와 그 하위 클래스를 이용
- Byte 스트림 : 8비트의 바이트를 읽고 쓰기 위한 스트림
- InputStream, OutputStream 클래스와 그 하위 클래스를 이용
Character 스트림
Writer와 Reader 클래스는 추상 클래스이다.
이 클래스 들은 문자 스트림의 입출력에 필요한 많은 메소드를 제공한다.
Writer 클래스는 문자 스트림의 출력 기능을 제공하며, Reader 클래스는 문자 스트림의 입력 기능을 제공한다.
<Writer 클래스의 주요 메소드>
메소드 이름 | 설명 |
void close() | 출력 스트림을 닫는다 |
void flush() | 출력 버퍼에 저장된 모든 데이터를 출력 장치로 전송 |
void write(int c) | c의 하위 16비트를 스트림으로 출력 |
void write(char buffer[]) | buffer 배열에 있는 문자들을 스트림으로 출력 |
void write(char buffer[], int idx, int size) | buffer 배열의 idx 위치로부터 size 크기만큼의 문자들을 스트림으로 출력 |
void write(String s) | 문자열 s를 스트림으로 출력 |
void write(String s, int idx, int size) | 문자열의 idx 위치로부터 size 크기만큼의 문자들을 스트림으로 출력 |
<Reader 클래스의 주요 메소드>
메소드 이름 | 설명 |
void close() | 입력 스트림을 닫는다 |
int read() | 다음 문자를 읽어 반환한다. 입력 스트림에 읽을 문자가 없으면 대기한다. 읽을 문자가 파일의 끝이면 -1을 반환 |
int read(char buffer[]) | 입력 스트림으로부터 buffer 배열 크기만큼의 문자를 읽어 buffer에 저장 |
int read(char buffer[], int offset, int numChars) | 입력 스트림으로부터 numChars에 지정한 만큼의 문자를 읽어 buffer의 offset 위치에 저장하고 읽은 문자의 개수를 반환 |
void mark(int numChars) | 입력 스트림의 현재 위치에 mark |
boolean markSupported() | 현재의 입력 스트림이 mark()와 read()를 지원하면 true 반환 |
boolean ready() | 다음 read()문을 수행할 수 있으면 true, 입력 스트림이 없어 기다려야 하면 false 반환 |
void reset() | 입력 스트림의 입력 시작 부분을 현재의 위치에서 가장 가까운 이전의 mark 위치로 설정 |
int skip(long numChars) | numChars로 지정된 문자 수만큼을 스킵하고 스킵된 문자의 수를 반환 |
Byte 스트림
바이스 스트림은 8비트인 바이트 단위로 입출력을 제어하므로 파일을 2진 데이터로 취급할 수 있다.
OutputStream과 InputStream 클래스는 문자 스트림의 Writer, Reader 클래스와 같이 추상 클래스이다.
추상 클래스로서 바이트 스트림의 입출력을 위한 메소드들이 선언되어 있다.
이 메소드 들은 하위 클래스의 객체를 통해서 많이 사용되는 메소드이다.
아래에서 주요 메소드를 확인하자!
InputStream과 OutputStream
<OutputStream 클래스의 주요 메소드>
: 바이트 스트림을 출력하는 메소드들을 제공
메소드 이름 | 설명 |
void close() throws IOException | 출력 스트림을 닫는다 |
void flush() throws IOException | 버퍼에 남아 있는 출력 스트림을 모두 출력 |
void write(int i) throws IOException | 정수 i의 하위 8비트를 출력 |
void write(byte buffer[]) throws IOException | buffer의 내용을 출력 |
void write(byte buffer[], int idx, int size) throws IOException | buffer의 idx위치로부터 size만큼 바이트 출력 |
<InputStream 클래스의 주요 메소드>
: InputStream 클래스는 입력(키보드, 파일 등)으로부터 데이터를 읽어 오는 메소드를 제공한다.
시스템의 표준 입력인 System.in이 InputStream 클래스의 객체를 의미한다
메소드 이름 | 설명 |
int available() | 현재 읽기 가능한 바이트의 수를 반환 |
void close() | 입력 스트림을 닫는다 |
int read() | 입력 스트림으로부터 한 바이트를 읽어 int 값으로 반환한다. 더 이상 읽을 값이 없을 경우 -1을 반환 |
int read(byte buffer[]) | 입력 스트림으로부터 buffer[] 크기만큼을 읽어 buffer 배열에 저장하고 읽은 바이트 수를 반환 |
int read(byte buffer[], int offset, int numBytes) | 입력 스트림으로부터 numBytes 만큼을 읽어 buffer[] 의 offset 위치에 저장하고 읽은 바이트 수를 반환 |
int skip(long numBytes) | numBytes 로 지정된 바이트를 스킵하고 스킵된 바이트 수를 반환 |
void mark(int numBytes) | 입력 스트림의 현재의 위치에 mark 한다. |
boolean markSupported() | 현재의 입력 스트림이 mark()와 reset()을 지원하면 true를 반환 |
void reset() | 입력 스트림의 입력 시작 부분을 현재의 위치에서 가장 가까운 이전의 mark 위치로 설정 |
스트림은 크게 두가지로 나뉜다
- 입력 스트림(Input Stream) : 프로그램으로부터 데이터를 읽어 들이는 스트림
- 출력 스트림(Output Stream) : 프로그램으로부터 데이터를 내보내는 스트림
프로그램으로부터 데이터를 읽어 들여야 하는 상황이라면 입력 스트림을 생성해야 하고,
프로그램으로부터 데이터를 전송해야 하는 상황이라면 출력 스트림을 생성해야 한다.
다음과 같이 스트림을 형성할 수 있다
InputStream in = new FileInputStream("run.exe");
이를 통해 2가지 사실을 알 수 있다.
- 스트림의 생성은 곧 인스턴스의 생성
- FileInputStream 클래스는 InputStream 클래스를 상속한다.
FileInputStream은 입력 스트림의 생성을 위한 클래스이며, 그 대상이 파일일 뿐이다. 파일과의 입력 스트림 형성을 위한 클래스이다.
스트림을 형성하여 run.exe에 저장되어 있는 데이터를 읽어 들이는 통로를 만든 셈이다.
이제 FileInputStream에 정의되어 있는 메소드를 통해 데이터를 읽으면 된다.
그런데 FileInputStream의 인스턴스를 InputStream의 참조변수로 참조하고 있음을 알 수 있다.
이는 FileInputStream 클래스가 InputStream 클래스를 상속하기 때문에 가능하다.
여기서 InputStream 클래스는 무엇?인가?
InputStream 클래스는 바이트 단위로 데이터를 읽어 들이는 모든 입력 스트림이 상속하는 (Object 클래스 다음의) 최상위 클래스이다.
그리고 이 클래스에서 정의하는 대표적인 메소드 두가지는 다음과 같다.
- public abstract int read() throws IOException
- public void close() throws IOException
InputStream 클래스를 상속하는 하위 클래스에서 입력에 대상에 맞게 read 메소드를 정의한다.
이와 마찬가지로, InputStream에 대응하는 클래스는 OutputStream 이다.
OutputStream은 모든 출력 스트림이 상속하는 최상위 클래스이다.
표준 스트림 (System.in, System.out, System.err)
java.lang 에 패키지에 있다.
표준 입출력 스트림 미리 생성되어 있는 스트림이기 때문에 사용자가 생성하지 않아도 된다.
System.in는 InputStream이며, 표준 입력을 제공한다.
- InputStream 클래스는 최상위 클래스이며 추상 클래스이므로 객체를 생성할 수 없다.
- 바이트 단위로만 입출력이 허용된다.
System.out과 System.err는 PrintStream이며 표준 출력과 표준 에러를 제공한다.
- PrintStream는 OutputStream의 자식 클래스이기 때문에 Exception을 안전하게 처리한 메소드로 구성된다.
- 그렇기 때문에 try~catch구문을 작성할 필요가 없다
파일 읽고 쓰기
FileWriter 클래스는 OutputStreamWriter 클래스로부터 상속된 클래스이다.
이 클래스는 파일에 문자를 출력하는 기능을 제공한다.
import java.io.*;
import java.util.Scanner;
public class FileWriterTest{
public public static void main(String args[]) throws Exception { // 예외처리
Scanner stdin = new Scanner(System.in);
String source = "사용자로부터 파일명을 입력 받기\n"
+ "입력 받은 문자열의 내용이 저장\n"
+ "문자열을 이용하여 문자 배열을 만든 후\n"
+ "문자 배열로 파일에 출력하는 프로그램\n";
char input[] = new char[source.length()];
source.getChars(0, source.length(), input, 0); // 문자열을 문자 배열로 만든다
System.out.println("파일명을 입력 : ");
String s = stdin.next();
FileWriter fw = new FileWriter(s); // 파일명으로 FileWriter 객체 생성
fw.write(input); // 문자 배열을 파일에 출력
fw.close(); // 출력 스트림 닫기
System.out.println(s + "파일이 생성됨");
}
}
FileReader 클래스는 InputStreamReader 클래스로부터 상속된 클래스이다.
파일로부터 문자를 입력받기 위해 사용한다.
이 클래스를 사용할 때는 FileNotFoundException 예외를 발생시킬 수 있기 때문에 프로그램에서 예외를 처리해주어야 한다.
import java.io.*;
import java.util.Scanner;
public class FileReaderTest{
public public static void main(String args[]) throws Exception {
Scanner stdin = new Scanner(System.in);
System.out.println("읽어 들일 파일명을 입력 : ");
String s = stdin.next();
FileReader fr = new FileReader(s); // 읽어 들일 파일명으로 객체 생성
int i;
while(i=fr.read()) != -1){ // 한 문자씩 읽음
System.out.print((char)i); // 문자 출력
}
fr.close(); // 입력 스트림 닫기
}
}
Reference
- 처음 시작하는 JAVA 프로그래밍, 김충석 저
- 난 정말 JAVA를 공부한 적이 없다구요, 윤성우 저
- tutorials.jenkov.com/java-nio/index.html
- hyeonstorage.tistory.com/235
'Java Live Study' 카테고리의 다른 글
Live Study 14주차 : 제네릭 (0) | 2021.03.06 |
---|---|
Live Study 12주차 : 애노테이션 (0) | 2021.02.06 |
Live Study 9주차 : 예외 처리 (0) | 2021.01.16 |
Live Study 8주차 : 인터페이스 (0) | 2021.01.16 |
Live Study 7주차 : 패키지 (0) | 2021.01.02 |