본문으로 바로가기

I/O (Input/Output)

category JAVA 2019. 12. 12. 18:27

I/O와 스트림


스트림의 개념

  • I/O란 데이터의 입력(Input)과 출력(Output)을 함께 일컫는 말이다.
  • 자바의 I/O는 스트림(Stream)을 이용해서 데이터를 주고받는 구조로 되어 있다.
  • 스트림 : 데이터의 소스에서 목적지까지 데이터를 이동시키는 일종의 통로이다.
  • 노드(node) : 데이터의 소스나 목적지. ex) 키보드, 모니터, 파일, 메모리, 데이터 베이스 등
  • 노드 스트림 : 노드에 연결된 스트림
  • 스트림의 방향 
     - InputStream(입력 스트림) : 데이터를 메모리에 넣을때
     - OutputStream(출력 스트림) : 메모리에 있는 데이터를 출력할 때.
  • 스트림을 통해서 전송 되는 데이터의 단위는 기본적으로 바이트(byte)이다.
    한글과 같은 문자들은 바이트로 처리되기 어려워 문자(char) 단위의 데이터가 전송되기도 한다.
    이때는 InputStream 대신 Reader를 사용하고, OutputStream 대신 Writer를 사용한다.

사진 수정 필요 output 화살표 방향

 

(1) 노드 스트림의 종류

  • 입출력과 관련된 I/O 스트림은 java.io 패키지에 선언된다.
  • I/O 스트림은 전송 되는 데이터 타입, 노드 타입, 방향에 따라 매우 다양한 클래스가 제공된다.
  • 기준1. 전송할 데이터의 타입이 byte인지 char인지
     - byte 단위의 데이터가 이동할 경우 : **Stream
                                                     예) 문자로 구성된 텍스트, 영상과 같은 바이너리(binary)형태의 데이터
     - char 단위의 데이터가 이동할 경우 : **er
                                                     예) 2byte 이상으로 구성된 한글, 한자 등
  • 기준2. 데이터의 이동방향
     - 데이터를 읽어 들이면 InputStream 또는 Reader
     - 데이터를 출력하면 OutputStream 또는 Writer
  • 기준3. 연결되는 노드의 타입
     - 단방향 노드인 키보드와 모니터는 InputStream 또는 Reader / OutputStream 또는 Writer 만 연결 가능 하다.
     - 그 외의 노드 들은 양방향 이다.
       파일연결시 File / 다른 스레드와 연결시 Piped / 메모리를 이용할 때는 Array(ByteArray, CharArray)

 

 

 

노드 스트림


  • 모든 스트림은노드 스트림에서 시작하며 노드 스트림들의 최상위 인터페이스는 키보드에 연결되는 InputStream과 Reader, 모니터에 연결되는 OutputStream과 Writer 이다.

1. 키보드를 이용한 InputStream 과 Reader

 

(책 p.589 메서드)

 

 

 

 

(1)InputStream

 

[예시1]

System의 in을 통해 InputStream을 얻어서 사용하는 예

public class MainClass01 {
	public static void main(String[] args) {
		// 1. 글자 입력
		InputStream is=System.in;
		System.out.print("한글자 입력 : ");
		// 2. read() 메서드로 입력한 문자의 code 값을 반환 받기
		try {
			//2-1. 입력한 문자의 code 값 반환
			int code=is.read(); //read()메서드는 딱 한글자만 입력받을 수 있다.
			System.out.println("code : "+code);
			//2-2. code 값을 문자로 반환 받기
			char ch=(char)code;
			System.out.println("ch : "+ch);
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("main 메소드가 종료됩니다.");
	}
}

 

*"\r\n"은 line feed 또는 new line을 의미한다.

 

 

  • I/O 작업은 스레드의 블로킹을 유발하기 때문에 성능에 대한 고민이 많이 필요하다.
  • read()메서드는 겨우 1byte를 읽는데 이때마다 스레드 블로킹이 유발되어 낭비를 초래한다.
  • 이런 경우, 배열을 이용해서 여러 데이터를 한 번에 읽는 방식을 추천한다.
    이 배열을 버퍼(buffer)라고 부른다.

 

[예시2]

다음은 버퍼를 이용해서 한 번에 10개씩 데이터를 읽는 예이다.

public class MainClass13 {
	public static void main(String[] args) {
		byte[] buffer=new byte[10];
		try(InputStream input=System.in) {
			int read=-1;
			while((read=input.read(buffer))>0) {
				System.out.println("읽은 개수 : "+read+", 문자열로 : "+new String(buffer, 0, read));
			}
		}catch(IOException e) {
			e.printStackTrace();
		}
	}
}

byte[] buffer=new byte[10]; byte / 알갱이를 10개씩 담을 수 있는 빈 배열을 만들어 놓는다.

InputStream input=System.in / 키보드에 입력되는 내용을 출력받아 input 변수에 담는다.

read=input.read(buffer)>0 / 입력받은 스트림을 buffer로 10byte씩 읽어들이고 출력하기

  • InputStream은 byte 단위의 전송이 이루어지기 때문에 한글자가 2byte이상으로 이루어진 경우 처리가 어렵다.
  • 한글은 'ms949' 캐릭터셋(character set)에서는 2byte, 'utf-8'에서는 3byte를 사용한다.
    위의 예는 'utf-8'이 사용되었고, 10byte씩 읽어오므로 3글자+1byte 만큼의 데이터가 전송되었다. 맨마지막 1byte때문에 깨지는 문제가 발생했다.
  • 따라서 전송될 데이터가 문자일 경우는 char 단위의 Reader를 사용하는 것이 좋다.

 

(2)InputStreamReader

 

(책 p.593메서드)

 

 

[예시1]

public class MainClass02 {
	public static void main(String[] args) {
		//1byte 처리 스트림
		InputStream is=System.in;
		//2byte 처리 스트림
		InputStreamReader isr=new InputStreamReader(is);
		System.out.print("한글자 입력(한글 가능) : ");
		try {
			//
			int code=isr.read();
			System.out.println("code : "+code);
			//코드를 한글로 변환
			char ch=(char)code;
			System.out.println("ch : "+ch);
		} catch (IOException e) {
			e.printStackTrace();
		} //try
	}//main
}//class

 

 

2. 모니터를 이용한 OutputStream과 Writer

 

OutputStream

 

(책. p.594 outputstream의 주요 메서드)

 

[예시]

outputstream을 이용해 출력해보자

public class MainClass04 {
	public static void main(String[] args) {
		/* [OutputStream]
		 * 1byte 처리 스트림이다.
		 * 
		 * 문자데이터를  InputStream 객체로 입력 받는다면
		 * 영문자, 대소문자, 숫자, 특수문자 까지만 처리할 수 있다.(한글불가) 
		 */
		OutputStream os=System.out;
		try {
			os.write(97);
			os.write(98);
			os.write(99);
			os.write(100);
			//os.write(44032);  //1byte 처리 스트림이기 때문에 한글 처리가 불가하다.
			byte[] buffer= {65, 66, 67, 68};
			os.write(buffer);
			os.flush(); //방출(밀어내기)
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

 

 

 

 

OutputStreamWriter

Writer는 OutputStream의 byte가char로 대체된다.

 

(책. p.59 outputstreamwriter의 주요 메서드)

 

[예시]

public class MainClass05 {
	public static void main(String[] args) {
		OutputStream os=System.out;
		//2. 2byte 처리 스트림(한글 처리 가능)
		OutputStreamWriter osw=new OutputStreamWriter(os);
		try {
			osw.write(97);
			osw.write(98);
			osw.write(99);
			osw.write(44032);
			osw.write("안녕");
			osw.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
}

 

 

 

3. 메모리 기반의 입/출력 처리

ByteArrayInputStream과 byteArrayOutputStream 및

CharArrayReader와 CharArrayWriter는

각각 프로그램의 메모리에서 byte와 char 단위의 입출력을 처리하는 스트림이다.

 

[예시]

문자열 기반의 데이터가 어떻게 처리 되는지 보자

public class IO {
	public static void main(String[] args) {
		//1. 데이터 소스로 chsr[] memory를 사용한다.
        char[] memory="안녕 java world".toCharArray(); //이 string을 char 배열로 만든다.
		char[] buffer=new char[5];
		CharArrayReader cReader=new CharArrayReader(memory);
		CharArrayWriter cWriter=new CharArrayWriter();
		try {
			//2. CharArrayReader가 buffer만큼 읽은 데이터를 CharArrayWriter에게 출력한다.
            while (true) {
				int cRead=cReader.read(buffer);
				if(cRead==-1)break;
				cWriter.write(buffer, 0, cRead);
			}
			//3. CharArrayWriter에 출력된 내용을 확인하기 위해 모니터에 출력한다.
            System.out.print(Arrays.toString(cWriter.toCharArray()));
		} catch (IOException e) {
			e.printStackTrace();
		}
	} //main()
}

//결과 : [안, 녕,  , j, a, v, a,  , w, o, r, l, d]

 

 

 

4. 파일 기반의 입/출력처리

(1) File

  • file은 가장 기본적인 입/출력 장치 중 하나로 파일과 디렉토리를 다루는 클래스이다.
  • file을 통해서 파일의 크기, 속성, 이름, 경로에 대한 정보를 얻거나 생성, 삭제할 수 있다.
  • 하지만 파일에서 데이터를 읽고 쓰는 것은 스트림을 통해서만 가능하다.

(책 p.599 파일생성, 소멸 관련 메서드)

 

 

[예시1]

memo.txt 파일과 sub 디렉토리 생성해보기

public class IO {
	public static void main(String[] args) {
		//1.memo.txt 파일 만들기
		File f1=new File("C:/myFolder/memo.txt");
		//2. sub 폴더 만들기
		File f2=new File("C:/myFolder/sub");
		
		try {
			//1.memo.txt 파일 만들기
			boolean cf1=f1.createNewFile();
			//2. sub 폴더 만들기
			boolean cf2=f2.mkdir();
			if (cf1) {
				System.out.println("txt 파일이 생성되었습니다.");
			}
			if (cf2) {
				System.out.println("디렉토리가 생성되었습니다.");
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	} //main()
}

 

(책 p.600 파일의 주요 메서드)

 

[예시2]

c드라이브의 파일 리스트를 받아서 디렉토리명은 [ ]안에 작성하고 그 외에는 그냥 출력하기

public class MainClass08 {
	public static void main(String[] args) {
		File f1=new File("c:/");
		//1. File 배열에 객체 리턴 받기
		File[] files=f1.listFiles();
		//2. 반복문 돌면서 디렉토리 명이나 파일명 출력해보기
		for(File tmp:files) {
			//3. file 객체의 메소드(getName())을 이용해서 이름 얻어 오기
			String name=tmp.getName();
			if(tmp.isDirectory()) {
				System.out.println("["+name+"]");
			}else {
				System.out.println(name);
			}//if
		}//for
	}//main
}

 

 

(2) FileInputStream과 FileOutputStream을 이용한 복사

puff파일을 복사해서 puff2 파일 생성해보기

[예시]

public class IO {
	public static void main(String[] args) {
		FileInputStream fis=null;
		FileOutputStream fos=null;
		try {
			fis=new FileInputStream("C:/myFolder/puff.png");
			fos=new FileOutputStream("C:/myFolder/puff2.png");
			byte[] buffer=new byte[100];
			while(true) {
				int read=fis.read(buffer);
				if(read==-1)break;
				fos.write(buffer, 0, read);
				fos.flush();	
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
				try {
					//만약 file 객체의 경로를 잘 못 적은 경우, 객체의 참조 값이 변수에 들어가지 않아 nullpointexception이 발생할 수 있다.
					//예외를 방기하기 위해 메서드가 호출되는 경우를 null이 아닐때만 호출되도록 설정해준다.
					if(fis!=null)fis.close();
					if(fos!=null)fos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
		}
	} //main()

 

(3)FileReader와 FileWriter를 이용한 문서작성

  • FileReader와 FileWriter문자 단위의 데이터를 파일에서 읽고 쓰는 스트림이다.

 

FileReader와 FileWriter를 이용한 문서 작성 과정

 

 

[예시1]

memo.txt 파일 만들고 글써보기

public class IO {
	public static void main(String[] args) {
		File f1=new File("C:/myFolder/memo.txt");
		try {
			//1. memo.txt 파일 생성
			f1.createNewFile();
			//2. memo.txt 파일에 글쓰기
			FileWriter fw1=new FileWriter(f1);
			fw1.write("안녕");
			fw1.write("날씨가");
			fw1.write("좋다");
			fw1.append("\r\n");
			fw1.append("하나 두울");
			fw1.append("\r\n");
			fw1.append("세엣");
			fw1.close();
		} catch (IOException e) {
			e.printStackTrace();
		}	
	} //main()
}

 

[예시2]

memo.txt 파일에 작성한 글을 console 창에 출력해보기

public class IO {
	public static void main(String[] args) {
		File f1=new File("C:/myFolder/memo.txt");
		try {
			FileReader fr=new FileReader(f1);
			char[] buffer=new char[10];
			while(true) {
				int read=fr.read(buffer);
				if(read==-1)break;
				System.out.println(new String(buffer, 0, read));
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	} //main()
}

 

 

5. 다른 스레드와의 통신을 위한 스트림

아직 안 배움 (책 p.606)

 

사용전 connect()메서드를 이용해 서로 연결해주어야한다.

 

 

 

 

 

보조스트림


1. 보조 스트림의 개념과 종류

(1) 보조 스트림의 개념

  • 보조 스트림 : 노드스트림과 달리 노드에 직접 연결 되지 않고 다른 스트림과 연결되는 스트림.
  • 보조스트림은 노드와 직접 연결할 수 없고 노드 스트림을 비롯한 다른 스트림과 연결된다.
  • 보조 스트림들은 부가적인 기능을 제공하므로 필터 스트림(Filter Stream) 또는 프로세싱 스트림(Processing Stream)이라고도 부른다.
  • 보조 스트림 기능 :
    문자셋(character set) 변환, 버퍼링, 기본 데이터형의 전송, 객체 입출력 등이 있다.
  • 보조 스트림의 기능 역시 입/출력 모두 존재하며 여러개의 보조스트림을 연결해서 사용할 수 있다.
    이를 스트림 체이닝 이라고한다.(Stream Chaining)

노드 스트림과 보조 스트림
스트림 체이닝

 

 

(2) 보조 스트림의 종류

 

(책 p.608 표 참고)

 

 

 

(3) 보조스트림의 생성과 종료

  • 보조스트림 생성 : 시작을 노드 스트림으로 설정하고, 앞단계의 스트림을 생성자로 넣어주면 된다.
  • 보조스트림 종료 : 보조스트림에서만 close()를 호출하면 노드 스트림 까지 알아서 close() 처리가 완료된다.

(사진 첨부 예정)

 

 

 

2. byte 기반 스트림을 char 기반 스트림으로 변경하는 스트림

  • InputStreamReader와 OutputStreamWriter는 byte 기반의 스트림을 지정된 캐릭터셋의 char 기반 스트림으로 변경해준다.
    - 문자(char)는 2개 또는 3개의  byte를 모아서 생성된다.
  • 이처럼 일련의 byte 코드 덩어리를 문자로 또는 문자를 byte 덩어리로 해성하기 위한 과정을 각각 '디코딩'과 '인코딩'이라고 한다.
  • 이 과정에서 주어진 값을 어떻게 해석할 것인지 결정해야 하는데 이때 적용되는 기준이 '캐릭터 셋'이다.

 

 

3. 버퍼를 이용하는 성능 향상 스트림

  • 애플리 케이션의 메모리를 버퍼(buffer)라고 한다.
    '버퍼링'은 이 버퍼에 데이터를 쌓는 과정이다.
  • 에) 동영상 파일을 재생하면 먼저 파일을 애플리케이션의 메모리에 다운로드 한 후 메모리의 파일을 비디오 플레이어에서 처리한다.
  • I/O 과정도 마찬가지이다. 하드디스크에 있는 자료를 직접적으로 읽어서 처리하는 것본다는 일단 메모리에 올리 후 처리하는 것이 효율적이다.
  • BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream은 내부적으로 버퍼를 이용하기 때문에 I/O 성능을 획기적으로 개선해준다. 이들은 내부적으로 8192byte의 크기를 갖고 있다.

(사진 첨부 필요 책 P.611)