본글은 'Do it 자바 완전 정복' 책을 통한 공부내용을 정리한 글입니다.
자바 입출력(Java IO)는 프로그램을 기준으로 외부로부터 데이터가 들어오는 입력(input)과 프로그램에서 외부로 나가는 출력(output)으로 구성된다.
byte 단위 입출력과 char 단위 입출력
자바의 입출력은 크게 byte 단위의 입출력과 char 단위의 입출력으로 나뉘는데, byte 단위의 입출력은 말 그대로 송수신하고자 하는 데이터를 byte 단위로 쪼개 보내고 받는 것이다. 모든 데이터는 byte의 모음이므로 어떠한 파일이든 상관없다.
char 단위 입출력은 채팅 프로그램과 같이 텍스트 전송에 특화된 방법이다. 물론 byte 단위 입출력으로도 텍스트 정보를 송수신 할 수 있지만, char 단위 입출력이 훨씬 효츌적이다.
byte 단위의 입출력에는 InputStream과 OutputStream 추상 클래스, char 단위의 입출력에는 Reader와 Writer 추상 클래스가 사용된다. 각 추상 클래스의 하위 클래스도 이들의 이름을 포함하고 있다.
InputStream과 OutputStream의 상속 구조
먼저 입력의 최상위 추상 클래스인 InputStream을 상속해 추상 메서드를 구현한 대표적인 자식 클래스에는 FileInputStream, BufferedInputStream, DataInputStream 등이 있으며, 자바에서 콘솔 입력을 위해 미리 객체를 생성해 제공하는 System.in도 이 InputStream의 추상 메서드를 구현한 클래스의 객체다.
출력을 위한 최상위 추상 클래스는 OutputStream이며, 이를 구현한 대표적인 자식 클래스로는 FileOutputStream, BufferedOutputStream, DataOutputStream 클래스와 출력에 특화된 PrintStream 등을 들 수 있다. 자바에서 콘솔 출력을 위해 미리 만들어 제공하는 객체인 System.out도 PrintStream 타입의 객체다.
byte 단위의 입출력에 사용되는 클래스의 이름을 보면 기본이 되는 클래스명을 포함하고, 입력과 출력이 대부분 대칭 구조이다. 다만 출력일 때는 출력에 특화된 PrintStream 클래스가 하나 더 추가되었다 생각하자.
InputStream의 주요 메서드
int available() | InputStream의 남은 바이트 수를 리턴 |
abstract int read() | int(4byte)의 하위 1byte에 읽은 데이터를 저장해 리턴(추상 메서드) |
int read(byte[] b) | 읽은 데이터를 byte[] b의 0번째 위치부터 저장하며 읽은 바이트 수를 리턴 |
int read(byte[] b, int off, int len) | len 개수만큼 읽은 데이터를 byte[] b의 off 위치부터 저장 |
void close() | InputStream의 자원 반환 |
InputStream 클래스는 추상 클래스이므로 직접 객체를 생성 할 수 없어 아래와 같이 직접 클래스를 생성한다 가정해본다.
class MyInputStream extends InputStream {
@Override
public int read() throws IOException {
return 0;
}
}
직접 생성한 클래스 MyInputStream의 read() 메서드는 아무런 기능이 없는 메서드로, 나머지 2개의 상속받은 메서드 read(byte[] b), read(byte[] b, int offset, int len)만 사용해 입력을 잘 받아올 수 있을것 같지만, 나머지 메서드 내부에서 read() 메서드를 호출해 사용하므로 read() 메서드가 아무런 기능을 하지 않는다면 나머지 2개의 메서드 또한 제기능을 하지 못한다. 또한 실제 입력을 처리하기 위해 속도적인 측면도 매우 중요하므로 JNI를 이용해 오버라이딩 한다.
위의 이유로, 직접 InputStream을 상속해 자식 클래스를 생성하고 객체를 생성하는 일은 어렵고, 번거롭다. 자바는 여러가지의 자식 클래스를 제공하고있고, 이들 클래스 내부에 read()가 이미 구현되어 있으므로 우리는 자식 클래스로 객체를 사용하면 된다.
OutPutStream의 주요 메서드
OutPutStream 클래스 역시 추상 클래스로, 내부에는 write(int b) 추상 메서드를 포함하고 있으므로 직접 객체를 생성할 수 없다.
void flush() | 메모리 버퍼에 저장된 output stream 내보내기(실제 출력 수행) |
abstract void write(int b) | int(4byte)의 하위 1byte를 output 버퍼에 출력(추상 메서드) |
void write(byte[] b) | 매개변수로 넘겨진 byte[] b의 0번째 위치에서부터 메모리 버퍼에 출력 |
void write(byte[] b, int off, int len) | byte[]의 off 위치에서 len개를 읽은 후 출력 |
void close() | OutputStream의 자원 반환 |
OutputStream 추상 클래스도 직접 자식 클래스를 생성하고자 할 때 write(int b)를 JNI를 이용해 오버라이딩 해야 한다. 따라서 자바 API에서 제공하는 다양한 자식 클래스를 활용해 OutputStream의 객체를 생성하는 것이 일반적이다.
InputStream 객체 생성 및 활용
이제 다양한 자식 클래스를 활용해 InputStream과 OutputStream 객체를 생성하고, 활용하는 방법을 알아본다.
FileInputStream으로 InputStream 객체 생성
FileInputStream은 InputStream을 상속한 대표적인 클래스로, File 내용을 byte 단위로 읽는다. FileInputStream 객체는 아래의 2가지 생성자를 이용해 생성할 수 있다. 생성자의 입력매개변수로는 File 객체를 직접 넘겨 주거나 문자열로 파일의 경로를 넘겨줄 수 있다.
FileInputStream(File file) | 매개변수로 넘어온 file을 읽기 위한 InputStream 생성 |
FileInputStream(String name) | 매개변수로 넘어온 name 위치의 파일을 읽기 위한 InputStream 생성 |
FileInputStream 객체를 이용해 File에서 데이터를 읽는 과정은 크게 3단계로 구성된다. 아래의 코드를 참고한다.
import java.io.*;
public class FileInputStream1 {
public static void main(String[] args) throws IOException {
// 입력 파일 생성
File inFile = new File("src/files/FileInputStream1.txt");
//InputStream 객체 생성
InputStream is = new FileInputStream(inFile);
int data;
while((data = is.read()) != -1) {
// 음수 리턴시 파일의 끝을 의미
System.out.println("읽은 데이터 : " + (char)data);
System.out.println("남은 바이트 수 : " + is.available());
}
// InputStream 자원 반납
// 자원 반납시 FileInputStream 객체는 더이상 사용x, 파일과의 연결 종료
is.close();
}
}
영문 데이터 파일 읽고 출력
이번에는 3가지 형태로 오버로딩된 read() 메서드를 통해 데이터를 읽어 출력하는 과정을 살펴본다.
read()의 offset의 의미는 파일 중간 부분부터 읽으라는 말이 아닌 읽은 데이터를 배열의 중간 부분 부터 넣으라는 의미이다.
import java.io.*;
public class FileInputStream2 {
public static void main(String[] args) throws IOException {
// 입력 파일 생성
File inFile = new File("src/files/FileInputStream1.txt");
// 1. 1byte 단위 읽기
InputStream is1 = new FileInputStream(inFile);
int data;
while ((data=is1.reae()) != -1) {
System.out.print((char)data);
}
is1.close();
// 2. n-byte 단위 읽기 (byte[]의 처음 위치에서부터 읽은 데이터 저장
InputStream is2 = new FileInputStream(inFile);
byte[] byteArray1 = new byte[9];
int count1;
while ((count1 = is2.read(byteArray1)) != -1) { // count1 = 9, 9, 2, -1
for (int i = 0; i < count1 ; i++) {
System.out.print((char)byteArray[i]);
}
}
is2.close();
// FileInput: count1 = 9
// Stream Te: count1 = 9
// st : count1 = 2
// 3. n-byte 단위 읽기 (앞에서 length만큼 읽어 byte[] offset 위치에서부터 입력)
InputStream is3 = new FileInputStream(inFile);
byte[] byteArray2 = new byte[9];
int offset = 3;
int length = 6;
int count2 = is3.read(byteArray2, offset, length);
for (int i = 0; i < offset + count2 ; i++) {
System.out.print((char)byteArray2[i]);
} // FileIn
is3.close();
}
}
위의 과정은 영문으로만 구성된 파일의 데이터를 읽어 영문으로 출력하는 것이므로 기본값 문자셋이 무엇인지 전혀 문제되지 않는다. MS949와 UTF-8 모두 영문자는 1byte로 저장하기 때문이다.
한글 데이터 파일 읽고 출력
한글은 MS949에서 2byte, UTF-8에서 3byte로 구성된다. 따라서 한글 파일을 읽을 때는 텍스트파일이 어떠한 문자셋으로 저장되어 있는지가 중요하다.
먼저 한글 데이터를 포함하고 있는 FileInputStream2.txt 파일은 "안녕하세요"라는 5글자의 한글을 포함하여 UTF-8 문자셋으로 저장되어 있다고 가정한다.
1byte씩 읽어 읽은 값은 리턴하는 read() 메서드는 문자당 2-3byte를 갖는 한글 데이터를 읽어 출력하는데에는 어려움이 있다. 고로 read(byte[] b)와 read(byte[] b, int offset, int len) 두 개의 메서드만 살펴본다.
import java.io.*;
import java.nio.charset.*;
public class FileInputStream3 {
public static void main(String[] args) throws IOException {
// 1. 입력 파일 생성
File inFile = new File("src/files/FileInputStream2.txt");
// 2. n-byte 단위 읽기(byte[]의 처음 위치에서부터 읽은 데이터 저장)
InputStream is2 = new FileInputStream(inFile);
byte[] byteArray1 = new byte[9]; // 한 번에 읽을 수 있는 최대 바이트수 9
int count1;
while ((count1 = is2.read(byteArray1)) != -1) {
String str = new String(byteArray1, 0, count1, Charset.forName("UTF-8"));
System.out.print(str);
}
is2.close();
// 안녕하
// 세요
// 3. n-byte 단위 읽기(앞에서 length만큼 읽어 byte[] offset 위치에 넣기)
InputStream is3 = new FileInputStream(inFile);
byte[] byteArray2 = new byte[9]; // 최소 offset + length
int offset = 3;
int length = 6;
int count2 = is3.read(byteArray2, offset, length);
String str = new String(byteArray2, 0, offset + count2, Charset.defaultCharset());
System.out.println(str); // 안녕
is3.close();
}
}
OutputStream 객체 생성 및 활용
FileOutputStream으로 OutputStream 객체 생성
FileOutputStream은 데이터를 File 단위로 쓰는 OutputStream을 상속한 클래스로, 객체를 다음 4가지 생성자로 생성할 수 있다.
FileOutputStream(File file) | 매개변수로 넘어온 file을 쓰기 위한 OutputStream 생성. append = true일 때 이어쓰기, append = false일 때 새로 덮어쓰기 (default = false) |
FileOutputStream(File file, boolean append) | |
FileOutputStream(String name) | 매개변수로 넘어온 name 위치의 파일을 쓰기 위한 OutputStream 생성. append = true일 때 이어쓰기, append = false일 때 덮어쓰기(default = false) |
FileOutputStream(String name, boolean append) |
이렇게 FileOutputStream 생성자로 객체를 생성하는 것은 애플리케이션에서 File 쪽으로만 흐르는 선을 이었다고 생각하면 된다. 즉, 오로지 파일 쓰기만 가능하다는 의미다.
영문 데이터 파일 출력
import java.io.*;
public class FileOutputStream1 {
public static void main(String[] args) throws IOException {
File outFile = new File("src/files/FileOutputStream1.txt");
if (!outFile.exists()) outFile.createNewFile();
// 파일을 쓸 때 생략 가능(자동 생성)
// 1. 1byte 단위 쓰기
OutputStream os1 = new FileOutputStream(outFile);
os1.write('J');
os1.write('A');
os1.write('V');
os1.write('A');
os1.write('\r'); // 13(생략가능)
os1.write('\n'); // 10 개행 /r/n
// 파일 작성시에는 \n 만으로 개행 가능. 윈도우 콘솔에서 엔터 입력시 \r, \n의 순으로 입력
os1.flush();
os1.close();
// 2. n-byte 단위 쓰기(byte[]의 처음부터 끝까지 데이터 쓰기)
OutputStream os2 = new FileOutputStream(outFile, true); // 내용 연결
byte[] byteArray1 = "Hello!".getBytes();
os2.write(byteArray1);
os2.write('\n');
os2.flush();
os2.close();
// 3. n-byte 단위 쓰기 (byte[]의 offset부터 length개의 byte 데이터 쓰기)
OutputStream os3 = new FileOutputStream(outFile, true); // 내용 연결
byte[] byteArray2 = "Better the last smile".getBytes();
os3.write(byteArray2, 7, 8);
os3.flush();
os3.close();
}
}
실행 결과
한글 데이터 파일 출력
import java.io.*;
public class FileOutPutStream2 {
public static void main(String[] args) [
File outFile = new File("src/files/FileOutputStream2.txt");
// 1. n-byte 단위 쓰기(byte[]의 처음부터 끝까지 데이터 쓰기)
OutputStream os2 = new FileOutputStream(outFile, true); // 내용 연결
byte[] byteArray1 = "안녕하세요".getBytes(Charset.forName("UTF-8"));
os2.write(byteArray1);
os2.write('\n');
os2.flush();
os2.close();
// 안녕하세요
// 2. n-byte 단위 쓰기(byte[]의 offset부터 length개의 byte 데이터 쓰기)
OutputStream os3 = new FileOutputStream(outFile, true); // 내용 연결
byte[] byteArray2 = "반갑습니다."getBytes(Charset.defaultCharset());
os3.write(byteArray2, 6, 6);
os3.flush();
os3.close();
//습니
}
}
콘솔로 InputStream 사용
앞에서 살펴본 FileInputStream과 FileOutputStream 생성자의 매개변수로 FileDescriptor 클래스의 정적 필드인 in과 out을 넘겨 객체를 생성하면, 이후 FileInputStream과 FileOutputStream 객체를 이용해 콘솔로 입출력 할 수 있다.
InputStream is = new FileInputStream(FileDescriptor.in); // 콘솔 입력 스트림 설정
OutputStream os = new FileOutputStream(FileDescriptor.out); // 콘솔 출력 스트림 설정
하지만 위 방법보다는 자바에서 이미 콘솔의 입출력을 위해 제공된 객체를 사용하는 것이 가장 간단한 방법인데, 입력을 하는 데에 InputStream 타입의 System.in 객체가 제공되며, 출력을 위한 객체는 PrintStream 타입의 System.out이다.
System.in 객체로 InputStream 사용
System.in은 자바 API에서 제공하는 콘솔 입력을 위한 InputStream 객체로 단 1개의 객체를 미리 생성해 제공하므로 close() 메서드로 자원을 반납시에는 이후 콘솔로 입력받을 수 없다. 따라서 System.in을 사용해 콘솔 입력을 받을 때는 자원을 반납하지 않는다. 콘솔로 입력한 데이터는 엔터를 입력한 시점에 InputStream으로 전달된다.
운영체제마자 Enter를 입력했을 때 InputStream으로 전달되는 데이터가 다르므로 실제 입력된 데이터까지만 읽어 화면에 출력하기 위해서 아래의 코드를 참고한다. 윈도우의 콘솔에서 엔터 입력시 \r(13)
, \n
순으로 2byte의 데이터가 순서대로 입력된다. 맥에서 엔터 입력시 \n(10)
만 입력된다.
// 윈도우에서 실제 입력된 데이터까지만 읽어 화면에 출력
while ((data = is.read()) != '\r') System.out.println(data);
while ((data = is.read()) != 13) System.out.println(data);
// 맥
while ((data = is.read()) != '\n') System.out.println(data);
while ((data = is.read()) != 10) System.out.println(data);
만일 콘솔에서 연속적으로 입력을 받는다면 버퍼에 남아 있는 데이터는 다음 입력이 실행도 되기 전에 버퍼에 남겨진 값을 읽으므로 남아 있는 데이터는 read() 메서드를 통해 반드시 꺼내야 한다.
콘솔로 입력된 한글 데이터를 처리하는 과정에서는 앞과 동일하게 1byte씩 읽는 read() 메서드는 비효율적이므로 read(byte[] b)
또는 read(byte[] b, int off, int len)
을 사용한다.
콘솔로 OutputStream 사용
System.out 객체로 OutputStream(PrintStream) 사용
콘솔에 데이터를 출력할 때도 자바에서 객체화해 제공하는 PrintStream 타입의 System.out
객체를 사용할 수 있다.
System.out 객체 역시 콘솔 출력 용으로 단 1개의 객체를 생성해 제공하므로 사용한 후 close()
로 자원 반납시에는 이 객체를 이용해 콘솔을 출력할 수 없다.
콘솔 입력과 달리 콘솔에 출력할 때는 운영체제와 상관없이 \r+\n
, \n
모두로 개행할 수 있다. 또한 System.out 객체의 write() 메서드 자체는 출력 버퍼에 기록하므로 실제 콘솔로 출력하기 위해서는 flush()
메서드를 호출해야 한다.
입출력 필터링
InputStream과 OutputStream은 byte 단위로 데이터를 출력하다 보니 속도가 느리고, 다양한 타입(int, double 등)으로 바로 출력할 수 없다. 이러한 문제점을 개선할 수 있는 것이 FilterInputStream과 FilterOutputStream이다. 대표적인 클래스로는Buffered(Input/Output)Stream
, Data(Input/Output)Stream
, PrintStream
을 들 수 있다.
이들 클래스를 활용하면 입출력 속도를 증가시키고, byte단위의 입출력이 아닌 int, float, double 또는 문자열로 데이터를 입출력 할 수 있다.
BufferedInputStream과 BufferedOutputStream을 이용한 속도 향상
Buffered(Input/Output)Stream은 입출력 과정에서 메모리 버퍼를 사용해 속도를 향상시키는 클래스다. Buffered(Input/Output)Stream의 기본적인 개념은 일단 쓰고자 하는 데이터를 메모리 버퍼에 기록하고, 한 번씩 모아 파일에 쓴다는 말이다. 버퍼의 사이즈가 50이라면 메모리 버퍼에 50개의 데이터가 가득찰 때 1번씩 파일에 엑세스하고 100byte의 데이터를 기록하기 위해 2번만 하드디스크에 액세스한다.
import java.io.*;
public class BufferedInputOutputStream {
public static void main(String[] args) {
File orgfile = new File("src/files/mycat.jpg");
File copyfile1 = new File("src/files/mycat_copy1.jpg");
File copyfile2 = new File("src/files/mycat_copy2.jpg");
long start, end, time1, time2;
// BufferedStream 사용 x
start = System.nanoTime();
try(InputStream is = new FileInputStream(orgfile);
OutputStream os = new OutputStream(copyfile1);) {
// 데이터 복사
int data;
while((data = is.read()) != -1) {
os.write(data);
}
} catch(IOException e) {e.printStackTrace();}
end = System.nanoTime();
time1 = end - start;
System.out.println("BufferedStream 사용 x : " + time1);
start = System.nanoTime();
// BufferedStream 사용 o
try (InputStream is = new FileInputStream(orgfile);
BufferedInputStream bis = new BufferedInputStream(is);
// 속도 향상을 위한 추가 처리
OutputStream os = new OutputStream(copyfile2);
BufferedOutputStream bos = new BufferedOutputStream(os);) {
// 속도 향상을 위한 추가 처리
// 데이터 복사
int data;
while((data = bis.read()) != -1) {
bos.write(data);
}
} catch(IOException e) {e.printStackTrace();}
end = System.nanoTime();
time2 = end - start;
System.out.println("BufferedStream 사용 o : " + time2);
}
}
DataInputStream과 DataOutputStream을 이용한 데이터 타입 다양화
InputStream과 OutputStream은 데이터를 오직 byte 단위로만 입출력 할 수 있다. 4byte 크기인 int 데이터 저장시에는 1byte로 4번 저장해야한다는 말인데, 문제는 이 값을 정확히 읽는 것이 절대 쉽지 않다는 것이다.
이런 문제점을 해결해주는 필터가 Data(Input/Output)Stream이다. Data(Input/Output)Stream는 다양한 데이터 타입(int, long, float, double, String 등)으로 입출력을 지원하는 클래스다.
Data(Input/Output)Stream 클래스의 대표적인 메서드
DataInputStream 메서드 | DataOutputStream 메서드 | |
상속받은 메서드 | int read(byte[] b) int read(byte[] b, inf off, int len) |
void write(int b) void write(byte[] b, int off, int len) |
추가 메서드 | boolean readBoolean() byte readByte() char readChar() short readShort() int readInt() long readLong() float readFloat() double readDouble() String readUTF() |
void writeBoolean(boolean v) void writeByte(int v) void writeChar(int v) void writeShort(int v) void writeInt(int v) void writeLong(long v) void writeFloat(float v) void writeDouble(double v) void writeUTF(String str) void writeBytes(String s) |
특이한 점은 readUTF()
와 writeUTF(String str)
또는 writeBytes(String s)
메서드를 포함하므로 앞에서 다룬 예제처럼 한글의 입출력을 위해 문자열 -> byte[], byte[] -> 문자열과 같은 변환 단계를 거치지 않고 쉽게 한글을 포함한 문자열을 읽고 쓸 수 있다.
import java.io.*;
public class DataInputOutputStream {
public static void main(String[] args) throws IOException {
File dataFile = new File("src/files/file1.data");
// 데이터 쓰기(DataOutputStream)
try (OutputStream os = new FileOutputStream(dataFile);
DataOutputStream dos = new DataOutputStream(os);) {
dos.writeInt(35);
dos.writeDouble(5.8);
dos.writeChar('A');
dos.writeUTF("안녕하세요");
dos.flush();
}
// 데이터 읽기(DataInputStream)
try (InputStream is = new FileInputStream(dataFile);
DataInputStream dis = new DataInputStream(is);) {
System.out.println(dis.readInt()); // 35
System.out.println(dis.readDouble()); // 5.8
System.out.println(dis.readChar()); // A
System.out.println(dis.readUTF()); // 안녕하세요
}
}
}
다양한 타입의 데이터를 저장한 후 저장 데이터를 메모장으로 열면 데이터가 깨져 보이는데, 이는 당연한 일이다. 메모장은 모든 데이터를 문자셋에 따라 문자(1byte~4byte)로 인식해 표기하기 때문이다.
필터 조합 - Buffered(Input/Output)Stream + Data(Input/Output)Stream
앞에서 알아본 Buffered(Input/Output)Stream과 Data(Input/Output)Stream은 얼마든 조합해 사용할 수 있다. 각각의 생성자 매개변수로는 InputStream과 OutputStream 객체가 들어가는데, Filter(Input/Output)Stream도 이들의 자식 클래스이므로 매개변수로 사용될 수 있기 때문이다.
필터의 순서는 어떠한 순서로 구성해도 문법상 오류는 나지 않지만, 최종적으로 하고자 하는 기능을 포함하고 있는 Filter(Input/Output)Stream이 마지막에 위치하도록 필터를 조합하면 된다.
PrintStream
PrintStream은 다양한 타입에 특화된 클래스다. 자동 flush() 기능을 제공하므로 따로 flush() 메서드를 호출할 필요 없다. 여태 많이 사용해온 System.out의 객체 타입이 바로 PrintStream이다.
PrintStream의 생성자는 다음과 같이 매개변수로 파일의 위치 정보 또는 OutputStream 객체를 받을 수 있다.
// 1. 출력할 파일을 매개변수로 직접 받을 때
PrintStream(File file)
PrintStream(String fileName)
// 2. OutputStream을 매개변수로 받을 때
PrintStream(OutputStream out)
PrintStream(OutputStream out, boolean autoFlush)
PrintStream의 대표적인 메서드
개행 포함 | 개행 미포함 |
void println(boolean b) void println(char c) void println(int i) void println(long l) void println(float f) void println(double d) void println(String s) void println(Object obj) void println() -> 출력값 없이 개행만 수행 |
void print(boolean b) void print(char c) void print(int i) void print(long l) void print(float f) void print(double d) void print(String s) void print(Object obj) PrintStream printf(String format, Object...) -> 연속호출가능 |
'Backend > JAVA' 카테고리의 다른 글
[JAVA] JVM의 메모리 구조 (0) | 2024.01.17 |
---|---|
[Java] #19.3 자바 입출력 - char 단위 입출력 (0) | 2023.08.15 |
[Java] #19.1 자바 입출력 - 파일 관리와 문자셋 (0) | 2023.08.13 |
[Java] #18 람다식 (0) | 2023.08.10 |
[Java] #17.5-6 Stack<E> 컬렉션 클래스, Queue<E> 컬렉션 인터페이스 (0) | 2023.08.09 |