본문 바로가기
DevelopmentTools/Java

[programmers-java 중급] IO

by 수짱수짱 2022. 7. 17.

<자바 IO>

I는 Input, O는 Output

  • 자바 IO
    • Byte 단위
      • InputStream 추상클래스를 상속받아 사용
        • FileInputStream  (파일로 부터 입출력받기 위한 클래스 4가지)
        • ByteArrayInputStream (배열로 부터 입출력받기  위한 클래스 4가지) 
        • DataInputStream (다양한 데이터 형을 입력받고 출력하는 클래스)
      • OutputStream 추상클래스를 상속받아 사용
        • OutputStream
        • ByteArrayOutputStream 
        • DataOutputStream (다양한 데이터 형을 입력받고 출력하는 클래스)
    • Char 단위 ( Reader, Write로 이름이 끝난다 )
      • Reader 추상클래스를 상속받아 사용
        • FileReader
        • CharReader
        • BufferedReader (한 줄 입력받는 readLine() 메소드를 가짐)
      • Writer 추상클래스를 상속받아 사용
        • FileWriter
        • CharWriter
        • PrintWriter (다양하게 한줄 출력하는 println() 메소드를 가짐)

 

어딘가로 부터 입력을 받을지, 어디에 출력을 할지 표현하는 IO 객체를 구분하는 방법은 생성자를 보고 알 수 있다.

위의 4가지 추상클래스를 받아들이는 생성자가 존재하고 있다면 다양한 입출력 방법을 제공하는 클래스이다.

어디로 입력 받을지 어디에 쓸지 쓸 클래스는 1 char, 1 byte 단위로 입력/출력하게 된다.

입출력하는 메소드가 단순하게 제공되기 때문에 특별한 방법으로 입출력을 시행하기 위해서는 이 4가지 추상메소드를 생성자에서 받아들이는 IO 클래스들을 이용해야 한다.

 

 

위에서 파란색 Class들은 어디로부터, 어디에 대상을 지정할 수 있는 Class들이다. => 장식대상 클래스 ( 어느 수도 연결된 배관 )

다양한 방식으로 입출력하는 기능을 제공하는 Class들이다. => 장식하는 클래스 ( 수도꼭지에 따라 달라지는 수압 )

 

장식하는 클래스에 대해 얘기하는 이유는 자바IO는 데코레이터 패턴(Decorator Pattern)으로 만들어졌기 때문이다.

이는 하나의 클래스를 장식하는 것처럼 생성자에서 감싸서 새로운 기능을 계속 추가 할 수 있도록 클래스를 만드는 패턴이다. IO는 클래스가 하나만 생성되서 사용되는 것이 아니므로 장식하는 클래스와 같이 여러 개의 클래스를 생성하여 생성자에 넣어서 사용하는 경우가 많다. 

 


<Byte단위 입출력>

Byte단위 입출력 클래스 이름은 InputStream, OutputStream으로 끝난다.

 

파일로부터 읽어오기 위한 객체: FileInputTstream

  • FileInputStream fis = null;
  • fis = new FileInputStream("파일경로") : try블럭 안에 작성
  • 파일경로에 파일이 없는 경우 예외(Exception)가 발생할 수 있으므로 exception 처리가 필요
    • 읽어들이기 위해 값을 담을 변수를 선언
    • int readData = -1;
    • FileInputStream의 read() 메소드는 한 바이트씩 읽을 수 있다.
      • return 타입은 정수. 정수의 4byte중에 마지막 byte에 읽어들인 한 바이트를 저장한다.
      • 정수로 return하는 이유는 끝을 나타내는 값을 표현하기 위해서이다.
      • 음수의 경우 좌측 비트는 1, 읽어들일 데이터가 있다면 항상 양수를 리턴한다.
      • 읽을 코드가 여러개라면 반복문을 통해 읽으면 된다.
    • while( (readDate = fis.read()) != -1 )  // 파일이 끝난다면 -1 을 리턴한다.
    • fos.write(readData);

파일에 쓰기 위한 객체: FileOutputStream

  • FileOutputStream fos = null;
  • fos = new FileOutputStream("작성한 파일의 저장 위치") : try블럭 안에 작성, 위치를 주지 않으면 현재 프로젝트 밑에 생성
  •  

IO의 모든 객체는 객체화하고 난 이후 반드시 닫아주어야 한다.

finally 블럭을 통해 닫아준다.close 메소드도 예외를 발생시키므로 예외 처리가 필요하다.

finally {

      try { fos.close();}

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

     try{ fis.close(); }

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

}

 


<Byte단위 입출력 심화>

한 바이트씩 읽던 것을 512byte씩 읽도록 수정한다.

 

512바이트씩 읽기위해 배열이 필요하다.

byte[] buffer = new byte[512];

또한, read메소드에 byte 배열을 인자로 준다. => 최대 512바이트의 데이터를 읽는다. 단, 무조건 512바이트만큼 읽는다는 것은 아니다. 마지막엔 읽을 것이 없으므로 read메소드는 -1을 반환할 것이다.

long startTime = System.currentTimeMillis();

byte[] buffer = new byte[512];
int readCount = -1;   // 읽어들이는 데이터의 수를 저장함.

while( (readCount = fis.read(buffer) ) != -1) {
	fos.write(buffer, 0, readCount); // 읽어들이는 수 만큼 write해라.
}


.
.
.


long endTime = System.currentTimeMillis();
System.out.println(endTime-startTime); // 수행시간

한 바이트씩 읽어들이는 것보다 속도가 빨라짐을 알 수 있다. 이를 위해 수행시간을 측정한다.

=> Q. 왜 속도가 빨라질까?

A. 1 byte씩 읽는 경우에 512byte씩 읽어서 1byte만 전달하는 것이기 때문이다.

즉, 1 byte를 2번씩 읽는다면 512byte를 2번씩 읽어서 1byte씩만 전달하는 것이다.

따라서, 512 byte씩 os가 읽기때문에 파일을 읽을땐 512 배수만큼 배열의 크기를 설정하는 것이 성능에 효율적이다.

 

* System.currentTimeMillis();는 현재 시간을 롱타입으로 반환하는 함수이다.


<다양한 타입의 출력>

다양한 타입을 파일에 저장하는 Class를 작성 => DataOutputStream (장식하는 클래스)

 

  • try-wirth-resource 문법
    • 자바 io객체 인스턴스를 만들고난 후 사용자가 close 메소드를 호출하지 않아도 예외가 발생하지 않았다면 자동으로 객체가 close가 되도록 하는 방법(문법)
    • try ( IO객체 선언 ) { ... }
    • catch ( Exception e ) { ... }
    • try 뒤의 괄호안에서 만든 stream 객체는 예외가 발생하지 않는다면 별도로 close할 필요가 없다.
  • DataOutputStream 이라는 IO 객체를 이용한다. (DataOutputStream Class)
    • 다양한 타입을 저장하고 읽을 수 있는 객체
    • DataOutputStream 클래스의 생성자는 OutputStream을 매개변수로 갖는다. 즉, OutputStream의 자식이라면 무엇이든 받아들일 수 있다는 것이다.
    • OutputStream을 받아들이는 생성자가 있다는 것은 DataOutputStream이 바로 장식의 역할(다양한 메소드 제공)을 한다는 것이다.
    • 대신, 장식이기 때문에 대상을 지정할 수는 없다. 따라서 어디에 쓸지 지정할 수 있는 Class를 함께 사용하면 된다.
import java.io.DataOutputStream;
import java.io.FileOutputStream;

try(DataOutputStream out = new DataOutputStream(new FileOutputStream("data.txt"));){ // 여러가지 데이터 타입을 받아서 파일에 저장할 수 있게 된다.
	out.writeInt(100); // int값으로 저장 (4byte 저장)
    	out.writeBoolean(true); // boolean값으로 저장 (1byte 저장)
    	out.writeDouble(50.5); // double값으로 저장 (8byte 저장)
}
catch (Exception e){
	e.printStackTrace();
}

data.txt에는 4 + 1 + 8 으로 총 13 byte가 파일에 저장된다.

 


<다양한 타입의 입력>

다양한 타입의 데이터를 읽어낼 수 있는 DataInputStream (장식하는 클래스)

=> InputStream의 자식은 전부 읽을 수 있다.

 

try-with-resource 문법을 통해 객체를 선언하고 선언한 객체를 사용하자.

 

import java.io.DataInputStream;
import java.io.FileInputStream;


try(DataInputStream in = new DataInputStream(new FileInputStream("data.txt"));) { // 다른 inputStream도 들어갈 수 있다 = inputstream의 자식은 다 들어갈 수 있다. 지금은 파일을 읽을 수 있는 객체가 필요하므로 FileInputStream을 사용한다.
	
    // 저장된 순서대로 읽어야 하므로 정수->불리언->더블로 읽는다.
    int i = in.readInt();
    boolean b = in.readBoolean();
    double d = in.readDouble();
    
    System.out.println(i);
    System.out.println(b);
    System.out.println(d);
}
catch (Exception e) {
	e.printStackTrace();
}

<Char단위 입출력(Console)>

Char 단위 입출력 클래스는 Reader, Write로 이름이 끝난다.

 

char 단위 입출력 클래스를 이용해서 키보드로 부터 입력받아 콘솔에 출력

  • System.in
    • 키보드로 부터 입력을 받을 수 있음 
    • InputStream 타입
      • 따라서, BufferedReader 생성자에 바로 들어갈 수 없다.
  • BufferedReader
    • 한줄씩 입력 받기 위한 클래스
    • 생성자가 Reader 객체만 받아들일 수 있다.
    • System.in의 타입은 InputStream이다.
    • 단, 생성자가 InputStream 객체를 받아들일 수 없다.
      • 이를 해결하기 위해 InputStreamReader 클래스를 이용하자.
      • InputStreamReader 클래스는 Reader를 상속받으며 생성자가 InputStream을 받아들인다.
import java.io.BufferedReader;


public class CharIOExam01 {

        public static void main(String[] args) {
        
            BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
            // 한줄씩 입력받기 위해 BufferedReader 객체 사용
            // 키보드로부터 입력받기 위해 System.in이 들어가야하고 이를 넣기 위해 InputStreamReader 객체를 통해 System.in을 Reader타입으로 바꿔준다. 
        
        
     	   // 키보드로부터 입력을 받아서 콘솔에 출력하자.
            String line = null;  // try문 밖에서도 사용하기 위해 변수의 scope을 생각하여 선언을 바깥쪽에 한다.
            try{
            	line = br.readLine(); // 키보드로 부터 입력을 받은 값을 line 변수에 넣어준다. 
           	// readLine 메소드는 예외가 발생할 수 있으므로 예외처리를 해주어야 한다.
            }
            catch(IOException e){
            	e.printStackTrace();
            }
            
            System.out.println(line);
        
        }
        
    }

br 객체는 키보드로 부터 한줄씩 입력받을 수 있는 객체가 된다.

반복문을 이용하여 여러줄 입력을 받을 수도 있다.

데코레이터 패턴을 이용하여 키보드가 아닌 파일로부터 입력을 받을 수도 있다.

입력 받은 부분을 콘솔이 아니라 파일이나 ArrayList와 같은 자료구조에 저장 할수도 있다.

 

  • 데코레이터 패턴(Decorator Pattern)
    • 객체에 추가적인 기능을 동적으로 첨가하는 방식
    • 서브클래스를 만드는 것을 통해 기능을 유연하게 확장할 수 있는 방법
    • 근원이 되는 부분과 기능을 갖는 부분을 끼워서 사용하는 방식

<Char단위 입출력(File)>

파일에서 입력하여 파일에서 출력하자.

  • FileReader
    • 파일에서 읽기 위한 클래스
  • BufferedReader
    • 한줄씩 읽어 들이기 위한 클래스
  • 두 개의 객체가 같이 생성되어야 한다.

 

  • System.out의 타입은 PrintWriter 객체이다.
    • PrintWrite는 다양한 방법으로 출력하는 메소드를 가지기 때문에 해당 객체로 출력을 하는 것이 편리하다.
    • PrintWrite 생성자가 발전되어서 생성자 자체가 File을 받아들이는 부분도 제공하기 때문에 FileWrite를 사용하지 않아도 PrintWriter 하나로 사용할 수 있게 되었다.

 

   import java.io.BufferedReader;
   import java.io.FileReader;
   import java.io.FileWriter;
   import java.io.IOException;
   import java.io.PrintWriter; 
   
   
    public class CharIOExam02 {
    
        public static void main(String[] args) {
        	
        // try블럭 안에 선언된 부분이 들어가면 외부에서 사용할 수 없으므로 선언과 예외처리 부분을 분리한다.
		
        BufferedReader br = null;
        PrintWriter pw = null;
    
		try{
              br = new BufferedReader(new FileReader("src/javaIO/exam/CharIOExam02.java"));  // FileReader가 생성될 때 예외처리 필요
              pw = new PrintWriter(new FileWriter("test.txt")); // 파일에서부터 받아들일 객체
              
              String line = null;
              while( (line = br.readLine()) != null ) {   // 파일이 끝일때 readLine 메소드는 null을 반환
              	pw.println(line); // 파일에 다시 읽은 값을 출력
              }
        	}
        catch(Exception e){
        		e.printStackTrace();
            }
        finally{
        		pw.close();   // 닫지않는다면 text.txt에 저장되지 않을 수도 있다.
                try {
                	br.close();
                }
                catch (IOException e) {
                	e.printStackTrace();
                }
            }
        }
    }
  • 항상 IO는 열어주었으면 닫아주어야 한다. !! 
  • finally 부문에서 pw 객체를 close 하지 않는다면 내용이 text.txt에 저장되지 않을 수도 있으므로 유의해야 한다.
  • System.out의 타입은 PrintWriter 객체이다.
    • PrintWrite는 다양한 방법으로 출력하는 메소드를 가지기 때문에 해당 객체로 출력을 하는 것이 편리하다.
    • 참고: PrintWrite 생성자가 발전되어서 생성자 자체가 File을 받아들이는 부분도 제공하기 때문에 FileWrite를 사용하지 않아도 PrintWriter 하나로 사용할 수 있게 되었다.
  • IO는 어디에서 읽을지, 쓸지. 어떤 방식으로 읽을지, 쓸지에 따라서 여러가지 종류의 객체를 조합하여 사용할 수 있다.
  • 따라서, 다양한 방법으로 이용할 수 있다.