본문 바로가기
Java/이것이 자바다

IO 기반 입출력

by k-mozzi 2023. 5. 12.
반응형
Preface

 

이번 장은 IO 기반의 입출력과 네트워킹에 대해 다루는데, 내용이 너무 많아 네트워크와 관련된 부분은 따로 업로드할 생각이다.

 

입출력에 관한 내용을 접하는 것이 이번이 처음이었다면 사용 방법을 익히는 데 시간이 꽤 걸렸을 듯하지만,

 

백준에서 문제를 풀어보며 이미 Scanner, BufferedReader 등 다양한 입출력 관련 메소드들을 사용해봐서 그런지 대부분의 내용을 큰 어려움 없이 이해하고 넘어갈 수 있었다.


 

1. 입력 스트림과 출력 스트림

 

 

- 스트림(Stream): 단일 방향으로 연속적으로 흘러가는 것

→ 단방향이므로 하나의 스트림으로 IO를 모두 처리할 수 없다.

1) 입력 스트림: 프로그램이 데이터를 입력받을 때 사용하는 스트림

2) 출력 스트림: 프로그램이 데이터를 보낼 때 사용하는 스트림

 

 

- 스트림 클래스의 종류

1) 바이트 기반 스트림: 모든 종류의 데이터를 송수신할 수 있음

2) 문자 기반 스트림: 문자만을 송수신할 수 있음

구분 바이트 기반 스트림 문자 기반 스트림
입력 스트림 출력 스트림 입력 스트림 출력 스트림
최상위 클래스 InputStream OutputStream Reader Writer
하위 클래스 XXXInputStream
(FileInputStream)
XXXOutputStream
(FileOutputStream)
XXXReader
(FIleReader)
XXXWriter
(FileWriter)

 

 

- InputStream: 바이트 기반 입력 스트림의 최상위 클래스로 추상 클래스

- read( ) 메소드: 입력 스트림으로부터 1바이트를 읽고 4바이트 int 타입으로 리턴
1) 리턴된 4바이트 중 끝의 1바이트에만 데이터가 들어 있다.
2) 더 이상 바이트를 읽을 수 없다면 -1을 리턴한다.


- read(byte[ ] b) 메소드: 입력 스트림으로부터 매개값으로 주어진 바이트 배열의 길이만큼 바이트를 읽고 배열에 저장
1) 실제로 읽은 바이트 수가 배열의 길이보다 작을 경우 읽은 수만큼만 리턴한다.
2) 더 이상 바이트를 읽을 수 없다면 -1을 리턴한다.
3) 많은 양의 바이트를 읽을 때 사용하면 좋다.


- read(byte[ ] b, int off, int len) 메소드: 입력 스트림으로부터 len개의 바이트만큼 읽고, 매개값으로 주어진 바이트 배열 b[off]부터 len개까지 저장
1) 읽은 바이트 수인 len개를 리턴한다.
2) 실제로 읽은 바이트가 len개보다 작을 경우 읽은 수만큼 리턴한다.
3) 더 이상 바이트를 읽을 수 없다면 -1을 리턴한다.
→ off는 저장을 시작할 인덱스이다.


- close( ) 메소드: InputStream을 더 이상 사용하지 않을 경우 호출하여 사용했던 시스템 자원을 풀어준다.

 

 

- OutputStream: 바이트 기반 출력 스트림의 최상위 클래스로 추상 클래스

→ 출력 스트림은 내부에 작은 버퍼가 있어서 데이터가 출력되기 전에 버퍼에 쌓여있다가 순서대로 출력된다.

- write(int b) 메소드: 매개 변수로 주어진 int 값에서 끝에 있는 1바이트만 출력 스트림으로 보냄


- wirte(byte[ ] b) 메소드: 매개값으로 주어진 바이트 배열의 모든 바이트를 출력 스트림으로 보냄


- write(byte[ ] b, int off, int len): b[off]부터 len개의 바이트를 출력 스트림으로 보냄


- flush( ) 메소드: 버퍼에 잔류하고 있는 데이터를 모두 출력시키고 버퍼를 비움
→ 더 이상 출력할 데이터가 없을 때 호출해서 버퍼에 잔류하는 모든 데이터를 출력되도록 해야 한다.


- close( ) 메소드: OutputStream을 더 이상 사용하지 않을 경우 호출하여 사용했던 시스템 자원을 풀어준다.

 

 

- Reader: 문자 기반 입력 스트림의 최상위 클래스로 추상 클래스

- read( ) 메소드: 입력 스트림으로부터 한 개의 문자(2바이트)를 읽고 4바이트 int 타입으로 리턴
1) 더 이상 문자를 읽을 수 없다면 -1을 리턴한다.


- read(char[ ] cbuf] 메소드: 입력 스트리믕로부터 매개값으로 주어진 문자 배열의 길이만큼 문자를 읽고 배열에 저장
1) 읽은 문자 수를 리턴한다.
2) 실제로 읽은 바이트 수가 배열의 길이보다 작을 경우 읽은 수만큼만 리턴한다.
3) 더 이상 문자를 읽을 수 없다면 -1을 리턴한다.
4) 많은 양의 문자를 읽을 때 사용하면 좋다.


- read(char[ ] cbuf, int off, int len) 메소드: 입력 스트림으로부터 len개의 문자만큼 읽고 매개값으로 주어진 문자 배열 cbuf[off]부터 len개까지 저장
1) 읽은 문자 수인 len개를 리턴한다.
2) 실제로 읽은 문자 수가 len개보다 작을 경우 읽은 수만큼 리턴한다.
3) 더 이상 문자를 읽을 수 없다면 -1을 리턴한다.


- close( ) 메소드: Reader를 더 이상 사용하지 않을 경우 호출해서 사용했던 시스템 자원을 풀어준다.

 

 

- Writer: 문자 기반 출력 스트림의 최상위 클래스로 추상 클래스

→ answk 출력 스트림은 내부에 작은 버퍼가 있어서 데이터가 출력되기 전에 버퍼에 쌓여있다가 순서대로 출력된다.

- write(int c) 메소드: 매개 변수로 주어진 int 값에서 끝에 있는 2바이트(한 개의 문자)만 출력 스트림으로 보냄


- write(char[ ] cbuf) 메소드: 매개값으로 주어진 char[ ] 배열의 모든 문자를 출력 스트림으로 보냄


- write(char[ ] c, int off, int len) 메소드: c[off]부터 len개의 문자를 출력 스트림으로 보냄


- write(String str) 메소드: 문자열 전체를 출력 스트림으로 보냄


- write(String str, int off, int len) 메소드: 주어진 문자열 off 순번부터 len개까지의 문자를 보냄


- flush( ) 메소드: 버퍼에 잔류하고 있는 데이터를 모두 출력시키고 버퍼를 비움
→ 더 이상 출력할 문자가 없을 때 호출해서 버퍼에 잔류하는 모든 데이터를 출력되도록 해야 한다.


- close( ) 메소드: Writer를 더 이상 사용하지 않을 경우 호출해서 사용했던 시스템 자원을 풀어준다.

 


 

2. 콘솔 입출력

 

 

- 콘솔(Console): 시스템을 사용하기 위해 키보드로 입력을 받고 화면으로 출력하는 소프트웨어

1) System.in: 콘솔로부터 데이터를 입력받을 때 사용

2) System.out: 콘솔에 데이터를 출력할 때 사용

3) System.err: 에러를 출력할 때 사용

 

 

- System.in은 InputStream 타입의 필드이므로 InputStream 변수로 참조할 수 있다.

InputStream is = System.in;

 

 

- 숫자로된 아스키 코드 대신 키보드에서 입력한 문자를 직접 얻으려면 read( ) 메소드로 읽은 아스키 코드를 char로 타입변환 하면 된다.

char inputChar = (char) is.read();

 

 

- InputStream의 read( ) 메소드는 1바이트만 읽으므로 한글과 같이 2바이트를 필요로 하는 유니코드는 read( ) 메소드로 읽을 수 없다.

→ read(byte[ ] b)나 read(byte[ ] b, int off, int len) 메소드로 전체 입력된 내용을 배열로 받고, 해당 배열을 이용해 String 객체를 생성하면 된다.

String strData = new String(byteData, 0, readByteNo-2);

//바이트 배열, 시작 인덱스, 읽은 바이트 수 -2 순서

 

 

- System.out: out은 PrintStream 타입의 필드이며, PrintStream은 OutputStream의 하위 클래스이므로 out 필드를 OutputStream 타입으로 변환해서 사용할 수 있다.

OutputStream os = System.out;

 

 

- write( ) 메소드는 아스키 코드를 문자로 콘솔에 출력한다.

 

 

- OutputStream의 write(int b) 메소드는 1바이트만 보낼 수 있으므로 2바이트로 표현되는 한글은 출력할 수 없다.

→ 한글을 바이트 배열로 얻은 후 wirte(byte[ ] b)나 write(byte[ ] b, int off, int len) 메소드로 콘솔에 출력하면 된다.

String name = "홍길동";
byte[] nameBytes = name.getBytes();
os.write(nameBytes);
os.flush();

 

 

- out은 원래 PrintStream 타입의 필드이므로 PrintStream의 print( ) 또는 println( ) 메소드를 사용하면 조금 더 쉽게 다양한 타입의 데이터를 콘솔에 출력할 수 있다.

System.out.println();

 

 

- Console 객체를 얻기 위해선 System의 정적 메소드인 console( )을 호출하면 된다.

1) 이클립스에서 실행하면 System.console( ) 메소드는 null을 리턴하므로 반드시 명령 프롬프트에서 실행해야 한다.

2) Console 클래스는 콘솔로부터 문자열을 읽을 수는 있지만, 기본 타입 값을 바로 읽을 수는 없다.

Console console = System.console();

 

 

- Scanner 클래스를 이용하면 콘솔로부터 기본 타입의 값을 바로 읽을 수 있다.

→ nextXXX( ) 메소드를 사용한다. (nextLine( ) 메소드는 String 값을 읽는다.)

Scanner scanner = new Scanner(System.in);

 


 

3. 파일 입출력

 

 

- File 클래스: 파일 크기, 속성, 이름 등의 정보를 얻어내는 기능과 파일 생성 및 삭제 기능 제공

1) 디렉토리를 생성하고 디렉토리에 존재하는 파일 리스트를 얻어내는 기능도 있다.

2) 파일의 데이터를 읽고 쓰는 기능은 지원하지 않으므로 스트림을 사용해야 한다.

File file = new File("파일 경로");

 

 

- 파일 객체를 생성했다고 해서 파일이나 디렉토리가 생성되는 것은 아니다.

1) 생성자 매개값으로 준 경로가 유효하지 않더라도 컴파일 에러나 예외가 발생하지 않는다.

2) exists( ) 메소드: 해당 경로의 파일이나 디렉토리 존재 여부를 boolean으로 리턴한다.

package ch18;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

public class FileExample {

	public static void main(String[] args) throws Exception {

		File dir = new File("/Users/kimgang/Documents/fileTest/Dir");
		File file1 = new File("/Users/kimgang/Documents/fileTest/file1.txt");
		File file2 = new File("/Users/kimgang/Documents/fileTest/file2.txt");
		File file3 = new File("/Users/kimgang/Documents/fileTest/file3.txt");

		if (dir.exists() == false) {
			dir.mkdirs();
		}
		if (file1.exists() == false) {
			file1.createNewFile();
		}
		if (file2.exists() == false) {
			file2.createNewFile();
		}
		if (file3.exists() == false) {
			file3.createNewFile();
		}

		File temp = new File("/Users/kimgang/Documents/fileTest");
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd    a    HH:mm");
		File[] contents = temp.listFiles();

		System.out.println("날짜               시간            형태           크기         이름");
		System.out.println("------------------------------------------------------------------");
		for (File file : contents) {
			System.out.print(sdf.format(new Date(file.lastModified())));
			if (file.isDirectory()) {
				System.out.print("\t<DIR>\t\t\t" + file.getName());
			} else {
				System.out.print("\t\t\t" + file.length() + "\t" + file.getName());
			}
			System.out.println();
		}
	}

}

 

 

- FileInputStream 클래스: 파일로부터 바이트 단위로 읽어들일 때 사용하는 바이트 기반 입력 스트림

→ FileInputStream 객체가 생성될 때 파일이 존재하지 않으면 FileNotFoundException을 발생시키므로 예외 처리가 필요하다.

// FileInputStream 객체 생성1
FileInputStream fis = new FileInputStream("파일경로");

// FileInputStream 객체 생성2
File file = new File("파일 경로");
FileInputStream fis = new FileInputStream(file);

 

 

- FileOutputStream: 바이트 단위로 데이터를 파일에 저장할 때 사용하는 바이트 기반 출력 스트림

→ 파일이 이미 존재할 경우, 데이터를 출력하면 파일을 덮어쓰게 되어 기존의 파일 내용이 사라진다.

// FileOutputStream 객체 생성1
FileOutputStream fos = new FileOutputStream("파일경로");

// FileOutputStream 객체 생성2
File file = new File("파일 경로");
FileOutputStream fos = new FileOutputStream(file);

 

 

- 기존의 파일 내용 끝에 데이터를 추가할 땐 FileOutputStream 생성자의 두 번째 매개값을 true로 주면 된다.

FileOutputStream fos = new FileOutputStream("파일경로", true);
FileOutputStream fos = new FileOutputStream(file, true);

 

 

- FileReader 클래스: 텍스트 파일을 프로그램으로 읽어들일 때 사용하는 문자 기반 스트림

→ 파일이 존재하지 않으면 FileNotFoundException이 발생하므로 예외 처리가 필요하다.

//전체 파일 경로로 FileReader 생성
FileReader fr = new FileReader("파일 경로");

//읽어야 할 파일이 File 객체로 이미 생성되어 있는 경우
File file = new File("파일 경로");
FileReader fr = new FileReader(file);

 

 

- FileWriter: 텍스트 데이터를 파일에 저장할 때 사용하는 문자 기반 스트림

→ 파일이 이미 존재할 경우, 데이터를 출력하면 파일을 덮어쓰게 되어 기존의 파일 내용이 사라지므로 생성자의 두 번째 매개값으로 true를 주면 된다.

//전체 파일 경로로 FileWriter 생성
FileWriter fw = new FileWriter("파일 경로");

//읽어야 할 파일이 File 객체로 이미 생성되어 있는 경우
File file = new File("파일 경로");
FileWriter fw = new FileWriter(file);

 


 

4. 보조 스트림

 

 

- 보조 스트림: 다른 스트림과 연결되어 여러 가지 편리한 기능을 제공해주는 스트림

→ 보조 스트림의 일부가 FilterInputStream, FilterOutputStream의 하위 클래스이므로 필터 스트림이라고도 한다.

 

 

- 보조 스트림은 자체적으로 입출력을 수행할 수 없어 입출력 소스와 연결되는 스트림에 연결해서 입출력을 수행한다.

1) 문자 변환, 입출력 성능 향상, 기본 데이터 타입 출력, 객체 입출력 등의 기능을 제공한다.

2) 다른 보조 스트림에 연결되어 스트림 체인을 구성할 수도 있다.

//자신이 연결될 스트림을 생성자의 매개값으로 받는다.
보조스트림 변수 = new 보조스트림(연결스트림);

 

 

- InputStreamReader: 바이트 입력 스트림에 연결되어 문자 입력 스트림인 Reader로 변환시키는 보조 스트림

Reader reader = new InputStreamReader(바이트입력스트림);

 

 

- OutputStreamWriter: 바이트 출력 스트림에 연결되어 문자 출력 스트림인 Writer로 변환시키는 보조 스트림

Writer writer = new OutputStreamWriter(바이트출력스트림);

 

 

- 프로그램의 실행 성능은 입출력이 가장 늦은 장치를 따라간다.

→ 프로그램이 입출력 소스와 직접 작업하지 않고, 중간에 메모리 버퍼와 작업함으로써 실행 성능을 향상시킬 수 있다.

 

 

- 입력 관련 Buffered: 입력 소스로부터 자신의 내부 버퍼 크기만큼 데이터를 미리 읽고 버퍼에 저장해둔다.

1) BufferedInputStream: 바이트 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림

2) BufferedReader: 문자 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림

→ readLint( ) 메소드: 캐리지리턴(\r)과 라인피드(\n)로 구분된 행 단위의 문자열을 한번에 읽을 수 있다. (Enter 입력 전까지의 문자열)

BufferedInputStream bis = new BufferedInputStream(바이트입력스트림);
BufferedReader br = new BufferedReader(문자입력스트림);

 

 

- 출력 관련 Buffered

1) BufferedOutputStream: 바이트 출력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림

2) BufferedWriter: 문자 출력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림

BufferedOutputStream bos = new BufferedOutputStream(바이트출력스트림);
BufferedWriter bw = new BufferedWriter(문자출력스트림);

 

 

- 바이트 스트림은 바이트 단위로 출력하므로 기본 데이터 타입 단위로 입출력할 수 없다.

→ DataInputStream, DataOutputStream 보조 스트림을 연결하면 가능하다.

DataInputStream dis = new DataInputStream(바이트입력스트림);
DataOutputStream dos = new DataOutputStream(바이트출력스트림);

 

 

- DataInputStream, DataOutputStream 보조 스트림은 readXXX( ), writeXXX( ) 메소드를 사용한다.

→ 데이터 타입의 크기가 모두 다르므로 출력한 순서와 동일한 순서로 읽어야 한다.

 

 

- 프린터 보조 스트림: print( ), println( ) 메소드를 가지고 있는 보조 스트림

1) PrintStream: 바이트 출력 스트림과 연결

2) PrintWriter: 문자 출력 스트림과 연결

PrintStream ps = new PrintStream(바이트출력스트림);
PrintWriter pw = new PrintWriter(문자출력스트림);

 

 

- printf( ) 메소드: 형식화된 문자열(format string)을 출력하는 메소드

1) 첫 번째 매개값으로 형식화된 문자열을, 두 번째 매개값부터 형식화된 문자열에 들어갈 값을 나열하면 된다.

2) %와 conversion(변환문자)은 필수적으로 작성하고, 그 이외의 항목은 선택적이다.

% [argument_index$] [flags] [width] [.precision] conversion
// 매개값의 순번, -/0, 전체자릿수, 소수자릿수, 변환문자 순서

 

 

- 객체는 문자가 아니므로 바이트 기반 스트림으로 출력해야 한다.

→ 객체의 데이터(필드값)를 일렬로 늘어선 연속적인 바이트로 변경해야 한다.

 

 

- 객체 직렬화(serialization): 객체의 데이터(필드값)를 일렬로 늘어선 연속적인 바이트로 변경하는 것

→ 역직렬화(deserialization): 입력 스트림으로부터 읽어들인 연속적인 바이트를 객체로 복원하는 것

 

 

- Object

1) ObjectOutputStream: 바이트 출력 스트림과 연결되어 객체를 직렬화

2) ObjectInputStream: 바이트 입력 스트림과 연결되어 객체로 역직렬화

ObjectInputStream ois = new OnjectInputStream(바이트입력스트림);
ObjectOutputStream oos = new ObjectOutputStream(바이트출력스트림);

 

 

- ObjectOutputStream의 writeObject( ) 메소드: ObjectOutputStream으로 객체를 직렬화

oos.writeObject(객체);

 

 

- ObjectInputStream의 readObject( ) 메소드: 입력 스트림에서 읽은 바이트를 역직렬화해서 객체로 생성

1) 리턴 타입이 Object이므로 객체를 원래의 타입으로 변환해야 한다.

2) 복수의 객체를 저장할 경우, 출력된 객체 순서와 동일한 순서로 객체를 읽어야 한다.

객체타입 변수 = (객체타입) ois.readObject();

 

 

- 자바는 Serializable 인터페이스를 구현한 클래스만 직렬화할 수 있도록 제한한다.

 

 

- Serializable 인터페이스: 필드나 인터페이스가 없는 빈 인터페이스

→ 객체를 역직렬화할 때 private 필드를 포함한 모든 필드를 바이트로 변환해도 좋다는 표시 역할을 한다.

 

 

- 객체를 직렬화하면 필드만 바이트로 변환되고, 생성자 및 메소드는 직렬화에 포함되지 않는다.

1) 역직렬화할 땐 필드의 값만 복원된다.

2) static 또는 transient 키워드가 붙어 있는 필드는 직렬화되지 않는다.

 

 

- 직렬화된 객체를 역직렬화할 땐 직렬화했을 때와 같은 클래스를 사용해야 한다.

→ 클래스의 이름이 같더라도 클래스의 내용이 변경되면 역직렬화에 실패한다.

 

 

- serailVersionUID: 같은 클래스임을 알려주는 식별자 역할

1) Serializable 인터페이스를 구현한 클래스를 컴파일하면 자동적으로 serialVersionUID 정적 필드가 추가된다.

2) 클래스를 재컴파일하면 serialVersionUID의 값이 달라진다.

→ 클래스의 수정이 필요하다면 클래스 작성 시 serialVersionUID를 명시적으로 선언하면 된다.

public class XXX implements Srializable {
	static final long serialVersionUID = 정수값;
}

 

 

- 부모 클래스가 Serializable 인터페이스를 구현하고 있으면 자식 클래스는 Serializable 인터페이스를 구현하지 않아도 자식 객체를 직렬화할 때 부모 필드 및 자식 필드가 모두 직렬화된다.

 

 

- 부모 클래스가 Serializable 인터페이스를 구현하고 있지 않고, 자식 클래스만 구현했다면 자식 객체를 직렬화해도 부모의 필드는 직렬화되지 않는다.

1) 부모 클래스가 Serializable 인터페이스를 구현하도록 한다.

2) 자식 클래스에서 writeObject( )와 readObject( ) 메소드를 선언해서 부모 객체의 필드를 직접 출력시킨다.

→ writeObject( ) 메소드는 직렬화될 때, readObject( ) 메소드는 역직렬화될 때 자동적으로 호출된다.

→ 접근 제한자가 private가 아니면 자동 호출되지 않으므로 반드시 private를 붙여야 한다.

→ defaultWrite(Read)Object( )는 자식 클래스에 정의된 필드들을 모두 직렬화하고 역직렬화한다.

private void writeObject(ObjectOutputStream out) throws IOException {
	out.writeXXX(부모필드);	//부모 객체의 필드값을 출력
    		:
    out.defaultWriteObject();	// 자식 객체의 필드값을 직렬화
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
	부모필드 = in.readXXX()	//부모 객체의 필드값을 읽어옴
    		:
    in.defaultReadObject();	//자식 객체의 필드값을 역직렬화

 

728x90
반응형

댓글