자바 I/O

백종현·2023년 3월 17일
1
post-custom-banner

스트림

두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요한데, 이것을 스트림(stream)이라 정의

(1) 입출력에서 스트림은 데이터를 운반하는데 사용되는 연결통로이다.

(2) 스트림은 단방향통신만 가능하기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없다. 입력과 출력을 동시에 수행하려면 입력을 위한 입력스트림과 출력을 위한 출력스트림 2개의 스트림이 필요하다.

(3) 스트림은 먼저 보낸 데이터를 먼저 받게 되어 있으며 건너뜀 없이 연속적으로 데이터를 주고받는다. (FIFO)

(4) 스트림은 바이트단위로 데이터를 전송하며 입출력 대상에 따라 다른 입출력스트림을 사용한다.

입출력 대상(File, Memory, Console 등등...)이 존재하고, 이곳에 전달하는 통로가 Stream

큐(Queue)와 같은 FIFO(First In First Out) 구조

바이트 기반 스트림 - InputStream, OutputStream

스트림은 바이트단위로 데이터를 전송하며 입출력 대상에 따라서 다음과 같은 입출력 스트림이 있다.

FileXxxStream : 파일
ByteArrayXxxStream : 메모리(byte배열)
PipedXxxStream : 프로세스(프로세스간 통신)
AudioXxxStream : 오디오 장치

// inputStream과 OutputStream에 정의된 읽기와 쓰기를 수행하는 메서드
abstract int read()
int read(byte[] b)
int read(byte[] b, int off, int len)
abstract void write(int b)
void write(byte[] b)
void write(byte[] b, int off, int len)

Input, OutputStream의 메서드 사용법만 잘 알고 있다면, 데이터를 읽고 쓰는 것은 대상의 종류에 관계 없이 간단한 일이 될 것이다.

class IOEx1 {
	public static void main(String[] args) {
		byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
		byte[] outSrc = null;

		ByteArrayInputStream  input  = null;
		ByteArrayOutputStream output = null;

		input  = new ByteArrayInputStream(inSrc);
		output = new ByteArrayOutputStream();

		int data = 0;

		while((data = input.read())!=-1) {
			output.write(data);	// void write(int b)
		}

		outSrc = output.toByteArray(); // 스트림의 내용을 byte배열로 반환한다.

		System.out.println("Input Source  :" + Arrays.toString(inSrc));
		System.out.println("Output Source :" + Arrays.toString(outSrc));
	}
}

보조 스트림

보조스트림은 실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능은 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가

  • FilterXxxStream : 필터를 이용한 입출력 처리
  • BufferedXxxStream : 버퍼를 이용한 입출력 성능향상.
  • DataXxxStream : int, float와 같은 Primitive Type 으로 데이터를 처리하는 기능
  • SequenceXxxStream : 두개의 스트림을 하나로 연결
  • LineNumberXxxStream : 읽어온 데이터의 라인번호를 카운트 (jdk 1.1부터 LineNumberReader로 대체)
  • ObjectXxxStream : 데이터를 객체단위로 읽고 쓰는데 사용
    주로 파일을 이용하며 객체 직렬화와 관련
  • PrintStream : 버퍼를 이용하며, 추가적인 print관련 기능
    (print, printf, println 메서드)
  • PushbackXxxStream : 버퍼를 이용해서 읽어 온 데이터를 다시 되돌리는 기능 (unread, push back to buffer)

FIlterInputStream과 FilterOutputStream

모든 보조 스트림의 조상

BufferXxxStream

보조 스트림인 BufferXxxStream에 대해서 보자.

class BufferedOutputStreamEx1 {
	public static void main(String args[]) {
		try {
		     FileOutputStream fos = new FileOutputStream("123.txt");
		     // BufferedOutputStream의 버퍼 크기를 5로 한다.
		     BufferedOutputStream bos = new BufferedOutputStream(fos, 5);
		     // 파일 123.txt에  1 부터 9까지 출력한다.
		     for(int i='1'; i <= '9'; i++) {
				 // fos.write(i);
			     // bos.write(i);
		     }

		     fos.close();
		} catch (IOException e) {
		     e.printStackTrace();		
		}
	}
}

fos.write(i);를 하는 경우는 결과가 123456789,
bos.write(i);를 하는 경우는 결과가 12345가 나온다. 이유는 아래와 같다.

// BufferedOutputStream 내부 구조.
@Override
public synchronized void write(int b) throws IOException {
	if (count >= buf.length) {
	    flushBuffer();
    }
    buf[count++] = (byte)b;
}

속도가 왜 빨라질까?

  • 모아서 보내면 왜 빨라질까?
  • 한 바이트씩 바로바로 보내는 것이 아니라 버퍼에 담았다가 한번에 모아서 보내는 방법인데 왜 이렇게 하는 것이 빠를까?
  • 입출력 횟수가 포인트 이다.
  • 단순히 모아서 보낸다고 이점이 있는 것이 아니다 → 시스템 콜의 횟수가 줄어들었기 때문에 성능상 이점이 생기는 것이다
  • OS 레벨에 있는 시스템 콜의 횟수 자체를 줄이기 때문에 성능이 빨라지는 것이다.

파일 시스템 시스템 콜 함수를 매번 부르는 것이 아닌, 버퍼의 크기가 찼을때마다 부르기 때문에 차이가 난다. 아래 코드를 통해 눈으로 확인해보자

class BufferedOutputStreamEx1 {
	public static void main(String args[]) {

		long beforeTime = System.currentTimeMillis(); //코드 실행 전에 시간 받아오기

		try {
		     FileOutputStream fos = new FileOutputStream("123.txt");
		     BufferedOutputStream bos = new BufferedOutputStream(fos, 1000);
		     // 파일 123.txt에  1 부터 9까지 출력한다.
		     for(int i='1'; i <= 'ㅂ'; i++) {
				 for (int j = 0; j < 1000; j++) {
					 fos.write(i);
					 // bos.write(i);
				 }
		     }

		     fos.close();
		} catch (IOException e) {
		     e.printStackTrace();		
		}

		long afterTime = System.currentTimeMillis(); // 코드 실행 후에 시간 받아오기
		System.out.println((afterTime - beforeTime));
	}
}

fos.write(i);로 실행하는 경우는 42464ms,
bos.write(i);로 실행하는 경우는 301ms이다.

참조 : java.io 패키지는 데코레이터 패턴으로 만들어졌다.

  • 데코레이터 패턴이란, A 클래스에서 B 클래스를 생성자로 받아와서, B 클래스에 추가적인 기능을 덧붙여서 제공하는 패턴이다.
  • BufferedOutputStream bos = new BufferedOutputStream(fos, 5); 와 같이 사용하는 이유.

    데코레이터 패턴
    객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.

PrintStream

데이터를 적절한 문자로 출력하는 것이기 때문에 문자기반 스트림의 역할 수행.

PrintStream은 지금까지 알게 모르게 많이 사용해왔다.
System 클래스의 Static 멤버인 out과 err, 즉 System.out, System.err이 PrintStream 이다.

class PrintStreamEx1 {
	public static void main(String[] args) {
		int    i = 65;
		float f = 1234.56789f;

		Date d = new Date();

		try {
			PrintStream out = new PrintStream("test.txt");
			System.setOut(out);

		} catch (FileNotFoundException e) {
			throw new RuntimeException(e);
		}
		System.out.printf("문자 %c의 코드는 %d%n", i, i);
		System.out.printf("%d는 8진수로 %o, 16진수로 %x%n", i ,i, i);
		System.out.printf("%3d%3d%3d%n", 100, 90, 80);
		System.out.println();
		System.out.printf("123456789012345678901234567890%n");
		System.out.printf("%s%-5s%5s%n", "123", "123", "123");
		System.out.println();
		System.out.printf("%-8.1f%8.1f %e%n",f,f,f);
		System.out.println();
		System.out.printf("오늘은 %tY년 %tm월 %td일 입니다.%n", d,d,d );
		System.out.printf("지금은 %tH시 %tM분 %tS초 입니다.%n", d,d,d );
		System.out.printf("지금은 %1$tH시 %1$tM분 %1$tS초 입니다.%n", d );
	}
}

DataInputStream과 DataOutputStream

int, float와 같은 Primitive Type 으로 데이터를 처리하는 기능 제공해주는 Stream Class.

class DataInputStreamEx1 {
	public static void main(String args[]) {
		try {
			FileInputStream fis = new FileInputStream("sample.dat");
			DataInputStream dis = new DataInputStream(fis);

			System.out.println(dis.readInt());
			System.out.println(dis.readFloat());
			System.out.println(dis.readBoolean());
			dis.close();
		} catch (IOException e) {
			e.printStackTrace();		
  		}
	} // main
}

SequenceInputStream

여러 개의 입력스트림을 연속적으로 연결해서 하나의 스트림으로부터 데이터를 읽어오는 것과 같이 처리할 수 있도록 도와주는 역할.

class SequenceInputStreamEx {
	public static void main(String[] args) {
		byte[] arr1 = {0,1,2};
		byte[] arr2 = {3,4,5};
		byte[] arr3 = {6,7,8};
		byte[] outSrc = null;

		Vector v = new Vector();
		v.add(new ByteArrayInputStream(arr1));
		v.add(new ByteArrayInputStream(arr2));
		v.add(new ByteArrayInputStream(arr3));

		SequenceInputStream   input  = new SequenceInputStream(v.elements());
		ByteArrayOutputStream output = new ByteArrayOutputStream();

		int data = 0;

		try {
			while((data = input.read())!=-1) {
				output.write(data);	// void write(int b)
			}
		} catch(IOException e) {}

		outSrc = output.toByteArray();

		System.out.println("Input Source1  :" + Arrays.toString(arr1));
		System.out.println("Input Source2  :" + Arrays.toString(arr2));
		System.out.println("Input Source3  :" + Arrays.toString(arr3));
	    System.out.println("Output Source  :" + Arrays.toString(outSrc));
	}
}

문자기반 스트림

문자데이터를 다루는데 사용된 다는 것을 제외하고는 바이트기반 스트림과 문자기반 스트림의 사용법은 거의 같다.

Reader와 Writer

바이트기반 스트림의 조상이 InputStream과 OutputStream인 것과 같이

문자기반의 스트림에서는 Reader 와 Writer 가 그 역할을 수행한다.

Reader 와 Writer의 메서드에서는 바이트기반 스트림과 비교하여 byte 배열 대신 char 배열을 사용한다.

FileReader와 FileWriter

파일로부터 텍스트데이터를 읽고, 파일을 쓰는데 사용된다.

사용법은 FileInputStream과 FileOutputStream과 다르지 않다.

class FileReaderEx1 {
	public static void main(String args[]) {
		try {
			String fileName = "test.txt";
			FileInputStream fis = new FileInputStream(fileName);
			FileReader	    fr  = new FileReader(fileName);

			int data =0;

			// FileInputStream을 이용해서 파일내용을 읽어 화면에 출력한다.
			while((data=fis.read())!=-1) {
				System.out.print((char)data);
			}
			System.out.println();
			fis.close();

			// FileReader를 이용해서 파일내용을 읽어 화면에 출력한다.
			while((data=fr.read())!=-1) {
				System.out.print((char)data);
			}
			System.out.println();
			fr.close();				

		} catch (IOException e) {
				e.printStackTrace();		
		}
	} // main
}
  • 바이트기반 스트림과 문자기반 스트림 각각 활용하여 txt 파일을 읽어서 print 하였다.
  • 결과에서 볼 수 있듯이 문자기반 스트림을 활용하였을 때 별다른 작업을 하지 않아도 인코딩되어 문자가 정상적으로 출력되는 것을 볼 수 있다.

PipedReader와 PipedWriter

쓰레드간 데이터를 주고 받을 때 사용된다.

Piped는 다른 스트림과 달리 입력과 출력스트림을 하나의 스트림으로 연결(connect)해서 데이터를 주고 받는다는 특징이 있다.

스트림을 생성한 다음 어느 한 쪽 쓰레드에서 connect()를 호출해서 입력 스트림과 출력 스트림을 연결한다.

입출력을 마친 후에는 어느 한쪽 스트림만 닫아도 나머지 스트림은 자동으로 닫힌다.

public class PipedReaderWriter {
	public static void main(String args[]) {
		InputThread   inThread = new InputThread("InputThread");
		OutputThread outThread = new OutputThread("OutputThread");

        //PipedReader와 PipedWriter를 연결한다.
		inThread.connect(outThread.getOutput());	

		inThread.start();
		outThread.start();
	} // main
}

class InputThread extends Thread {
	PipedReader  input = new PipedReader();
	StringWriter sw    = new StringWriter();

	InputThread(String name) {
		super(name);		// Thread(String name);
	}

	public void run() {
		try {
			int data = 0;

			while((data=input.read()) != -1) {
				sw.write(data);
			}
			System.out.println(getName() + " received : " + sw.toString());
		} catch(IOException e) {}
	} // run

	public PipedReader getInput() {
		return input;
	}

	public void connect(PipedWriter output) {
		try {
			input.connect(output);
		} catch(IOException e) {}
	} // connect
}

class OutputThread extends Thread {
	PipedWriter output = new PipedWriter();

	OutputThread(String name) {
		super(name);		// Thread(String name);
	}

	public void run() {
		try {
			String msg = "Hello";
			System.out.println(getName() + " sent : " + msg);
			output.write(msg);
			output.close();
		} catch(IOException e) {}
	} // run

	public PipedWriter getOutput() {
		return output;
	}

	public void connect(PipedReader input) {
		try {
			output.connect(input);
		} catch(IOException e) {}
	} // connect
}

StringReader와 StringWriter

CharArrayReader / CharArrayWriter와 같이 입출력 대상이 메모리인 스트림이다.

StringWriter에 출력되는 데이터는 내부 StringBuffer에 저장되며, StringWriter의 다음 과 같은 메서드를 이용해 저장된 데이터를 얻을 수 있다.

class StringReaderWriterEx {
	public static void main(String[] args) {
		String inputData = "ABCD";
		StringReader input  = new StringReader(inputData);
		StringWriter output = new StringWriter();

		int data = 0;

		try {
			while((data = input.read())!=-1) {
				output.write(data);	// void write(int b)
			}
		} catch(IOException e) {}

		System.out.println("Input Data  :" + inputData);
		System.out.println("Output Data :" + output.toString());
//		System.out.println("Output Data :" + output.getBuffer().toString());
	}
}

문자기반의 보조스트림

BufferedReader 와 BufferedWriter

버퍼를 이용해 입출력의 효율을 높일 수 있도록 해주는 보조 역할을 수행한다.

버퍼를 이용하면 입출력의 효율이 비교할 수 없을 정도로 좋아지기 때문에 사용하도록 하자!

  • BufferedReader의 readLine()을 사용하면 데이터를 라인 단위로 읽을 수 있고
  • BufferedWriter는 newLine() 이라는 줄바꿈을 해주는 메서드를 가지고 있다.
class BufferedReaderEx1 {
	public static void main(String[] args) {
		try {
			FileReader fr = new FileReader("BufferedReaderEx1.java");
			BufferedReader br = new BufferedReader(fr);

			String line = "";
			for(int i=1;(line = br.readLine())!=null;i++) { 
				//  ";"를 포함한 라인을 출력한다.
				if(line.indexOf(";")!=-1)	
					System.out.println(i+":"+line);
			}
                     
            br.close();
		} catch(IOException e) {}
	} // main
}

InputStreamReader와 OutputStreamWriter

바이트 기반 스트림을 문자 기반 스트림으로 연결시켜주는 역할을 수행한다.

추가적으로 바이트기반 스트림의 데이터를 지정된 인코딩의 문자데이터로 변환하는 작업을 수행한다.

InputStreamReader와 OutputStreamWriter는
Reader와 Writer의 자손이다.


class InputStreamReaderEx {
	public static void main(String[] args) {
		String line = "";

		try {
			InputStreamReader isr = new InputStreamReader(System.in);
			BufferedReader    br  = new BufferedReader(isr);

			System.out.println("사용중인 OS의 인코딩 :" + isr.getEncoding());

			do {
				System.out.print("문장을 입력하세요. 마치시려면 q를 입력하세요.>");
				line = br.readLine();
				System.out.println("입력하신 문장 : "+line);
			} while(!line.equalsIgnoreCase("q"));

//			br.close();   // System.in과 같은 표준입출력은 닫지 않아도 된다.
			System.out.println("프로그램을 종료합니다.");
		} catch(IOException e) {}
	} // main
}

위의 코드에서 System.in.read();처럼 한글을 읽어온다고 생각해보자. 한글이 깨지지 않을까?

표준 입출력 - System.in, System.out, System.err

표준입출력은 콘솔을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미한다.

자바에서는 표준 입출력(standard I/O)를 위해 3가지 입출력 스트림을 제공한다.

  • System.in
  • System.out
  • System.err

이 들은 자바 어플리케이션의 실행과 동시에 사용할 수 있게 자동적으로 생성되기 때문에 개발자가 별도로 스트림을 생성하는 코드를 작성하지 않아도 된다.

	// System.class 내부 코드
	private static native void registerNatives();
    static {
        registerNatives();
    }

이와 같이 native 메소드를 통해 생성되는 것을 볼 수 있다.

RandomAccessFile

자바는 기본적으로 입력과 출력이 각각 분리되어 별도로 작업을 하도록 설계되어 있는데,

RandomAccessFile 만은 하나의 클래스로 파일에 대한 입력과 출력을 모두 할 수 있도록 되어 있다.

InputStream이나 OutputStream으로부터 상속받지 않고, DataInput 인터페이스와 DataOutput 인터페이스를 모두 구현했기 때문에 읽기와 쓰기가 모두 가능하다.

RandomAccessFile의 큰 장점은 파일의 어느 위치에나 읽기/쓰기가 가능하다는 것이다.

class RandomAccessFileEx1 {
	public static void main(String[] args) {
		try {
			RandomAccessFile raf = new RandomAccessFile("test.dat", "rw");
			System.out.println("파일 포인터의 위치: " + raf.getFilePointer());
			raf.writeInt(100);
			System.out.println("파일 포인터의 위치: " + raf.getFilePointer() + "," +raf.readInt());
			raf.writeLong(100L);
			System.out.println("파일 포인터의 위치: " + raf.getFilePointer());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
class RandomAccessFileEx2 {
	public static void main(String args[]) {
//				      번호, 국어, 영어, 수학					
		int[] score = {	1, 100,  90,  90,
					    2,  70,  90, 100,
						3, 100, 100, 100, 
						4,  70,  60,  80, 
						5,  70,  90, 100
					   }; 

		try {
		      RandomAccessFile raf = new RandomAccessFile("score2.dat", "rw");

		      for(int i=0; i<score.length;i++) {
		             raf.writeInt(score[i]);				
		      }
			  raf.seek(0); // 포인터를 처음으로 초기화.
		      while(true) {
			     System.out.println(raf.readInt());
		      }
		} catch (EOFException eof) {
		       // readInt()를 호출했을 때 더 이상 읽을 내용이 없으면 EOFException이 발생한다.
		} catch (IOException e) {
		       e.printStackTrace();		
		}
	} // main
}

직렬화

**직렬화(serialization)**란 객체를 데이터 스트림으로 만드는 것을 뜻한다.

객체에 저장된 데이터를 스트림에 쓰기(write)위해 연속적인(serial) 데이터로 변환하는 것을 의미한다.

반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)이라 한다.

ObjectInputStream, ObjectOutputStream

직렬화(스트림에 객체를 출력)에는 ObjectOutputStream을 사용

역직렬화(스트림으로부터 객체를 입력)에는 ObjectInputStream을 사용

직렬화가 가능한 클래스 만들기 - Serializable, transient

transient 제어자를 붙여서 직렬화 대상에서 제외할 수 있다. 제외시킨 후 역직렬화하면 값은 null이 된다.

Serializable 인터페이스를 아무런 내용이 없는 빈 인터페이스지만, 직렬화를 고려하여 작성한 클래스인지를 판단하는 기준이 된다.

public class SerialEx1 {
	public static void main(String[] args) {
		try {
			String fileName = "UserInfo.ser";
			FileOutputStream     fos = new FileOutputStream(fileName);
			BufferedOutputStream bos = new BufferedOutputStream(fos);

			ObjectOutputStream out = new ObjectOutputStream(bos);
			
			UserInfo u1 = new UserInfo("JavaMan","1234",30);
			UserInfo u2 = new UserInfo("JavaWoman","4321",26);

			ArrayList<UserInfo> list = new ArrayList<>();
			list.add(u1);
			list.add(u2);

			// 객체를 직렬화한다.
			out.writeObject(u1);
			out.writeObject(u2);
			out.writeObject(list);
			out.close();
			System.out.println("직렬화가 잘 끝났습니다.");
		} catch(IOException e) {
			e.printStackTrace();
		}
	} // main
} // class

직렬화 가능한 클래스의 버전관리

  • 직렬화된 객체를 역직렬화할 때는 직렬화 했을 때와 같은 클래스를 사용해야한다.
  • 클래스 이름이 같아도 클래스의 내용이 변경됐다면 역직렬화는 실패하고 에러가 발생한다.

참조 : https://five-cosmos-fb9.notion.site/I-O-af9b3036338c43a8bf9fa6a521cda242, 자바의 정석

profile
노력하는 사람
post-custom-banner

0개의 댓글