글 작성자: beaniejoy

 Java에서 중요한 부분중에 하나인 IO 입출력 관련해서 공부한 내용을 정리해보고자 한다.

 

 자바에서 입력, 출력으로 나눠서 설명하자면 

 

≫ 입력 : 파일, 키보드, 네트워크 (소스 / Source라고도 한다.)

≫ 출력 : 파일, 모니터, 네트워크 (목적지 / Destination이라고도 한다.)

 

 이렇게 나눌 수 있다. 각각에 대해 입출력을 작동시키기 위해 알맞는 Stream을 사용해 수행해야 한다. Stream은 자바 애플리케이션이 입력과 출력을 수행하도록 도와준다. 소스에서 자바 애플리케이션으로 데이터를 읽어 들이는 것을 InputStream이라고 하고, 자바 애플리케이션에서 목적지로 데이터를 출력시키는 작업을 OutputStream이라고 한다. 이것들이 어떤 용도로 어떻게 쓰이는지 알아보자.


여러가지 IO Stream이 있지만 중요하다고 생각하는 것들만 보고자 한다.

 

 

1. InputStream / OutputStream (1byte 처리, Byte Stream)

≫InputStream

- read( ) : return값은 int, 스트림 데이터를 1byte 읽어온다. 더 이상 읽을 수 없을 때는 -1을 return.

- read(byte[] b) : 스트림 데이터를 1byte 읽어 바이트 배열에 저장하고 읽은 수만큼 return.

- read(byte[] b, int start, int length) : 스트림 데이터 length 만큼 읽어서 바이트배열의 start 위치에 저장하고 읽은 수만큼 return.

 

≫OutputStream

- writer(int b) : 출력 스트림으로 b의 값을 바이트로 변환해 쓰기

- writer(byte[] b) : 바이트 배열 b를 쓰기

- writer(byte[] b, int start, int length)

 

 

2. FileInputStream / FileOutputStream (Binary File 처리할 때)

≫FileInputStream(String name)

- String name: 파일 시스템의 실제 경로

≫FileOutputStream(String name [, boolean append])

- boolean append: true면 이어서쓰는 것, false면 새로 덮어쓰기

 

1
2
3
4
5
6
7
8
9
fis = new FileInputStream("c:\\dev\\io\\2019\\12\\jdk.exe");
// destination
fos = new FileOutputStream("c:\\dev\\io\\2019\\12\\jdk-1.exe");
int readByte = 0;
// fis.read(): 한 바이트 읽어서 반환한다.
while ((readByte = fis.read()) != -1) {
    // fos.write(): 한 바이트 쓰기
    fos.write(readByte);
}
 

 위의 코드는 한 바이트씩 읽어서 쓰기를 실행하는 것이다. 물론 이것도 가능하지만 실행속도에 있어서 비효율적이다라는 단점이 있다.

그래서 이를 개선해보고자 바이트 배열로 한번 받아보자.

 

FileInOutputStreamDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package java_20191202;
 
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
 
// binary file 대상으로 읽고 쓰는 것
// 결국은 이게 제일 좋고 간편하다.
public class FileInOutputStreamDemo {
    public static void main(String[] args) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
 
        // try부분은 외워야 한다.
        try {
            // source
            fis = new FileInputStream("c:\\dev\\io\\2019\\12\\jdk.exe");
            // destination
            fos = new FileOutputStream("c:\\dev\\io\\2019\\12\\jdk-1.exe");
            // 10240을 잡는게 일반적이다.
            byte[] readBytes = new byte[1024 * 10]; // 8-10kb
            int readByteCount = 0;
            // fis.read(readBytes): 1024바이트 읽어서 readBytes에 저장하고
            // 읽은 바이트수를 반환한다.
         
            while ((readByteCount = fis.read(readBytes)) != -1) {
                // fos.write(readBytes, 0, readByteCount): readBytes 바이트
                // 배열에 있는 데이터를 출력하되, 처음(0)부터, readByteCount만큼 출력
                fos.write(readBytes, 0, readByteCount);
                // fos.write(readBytes)로 해도된다.
            }
            
          
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // IOException이 FileNotFoundException 보다 상위이기 때문에
        // 위에다 catch하면 얘가 다 잡아버림
        finally {
            try {
                if (fis != null) {
                    fis.close();
                }
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
 

 바이트 단위로 받았던 것에서 바이트 배열로 받고 쓰는 것은 상당한 시간 개선이 있다. 바이트 배열 덩어리로 처리하기 때문에 바이트 단위였던 것보다 실행시간 효율이 좋다.

 

 

3. BufferedInputStream / BufferedOutputStream

≫BufferedinputStream

≫BufferedOutputStream

 

 위 두 개는 Stream Chaining으로 쓰이는데 1byte 바이트스트림에서는 Chaining안하고 그냥 쓰는 것이 좋다. 사실 Stream Chaining하는 것이 Stream 성능개선을 목적으로 쓰이는데 위의 바이트배열로 받고 처리하는 것과 비교했을 때 실행시간에서 차이가 별로 없다. 따라서 1byte를 처리할 때(주로 binary file받을 때) Input/OutputStream으로 해서 바이트배열 처리하는 것이 좋다.

 

 

4. FileReader / FileWirter (2byte, Character Stream, Text File 처리할 때)

≫FileReader(String name)

- read( ) : 문자 입력 스트림에서 한 개의 문자를 읽어온다.

- read(char[] c) : 문자를 하나씩 읽어 char[]에 저장하고 읽은 수만큼 return한다.

- name: 파일시스템 실제 파일 경로

≫FileWriter(String name)

- write(int c) : c의 값을 char로 변환해 쓰기

- write(char[] c) : 문자 배열 c를 쓰기 한다.

- writer(char[] c, int start, int length) : 문자 배열 b를 start부터 length만큼 쓰기한다.

 

FileReaderWriterDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package java_20191203;
 
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
 
public class FileReaderWriterDemo {
    public static void main(String[] args) {
        FileReader fr = null;
        FileWriter fw = null;
 
        // 이 클래스는 Stream chaining해주는 것이 좋다.
        try {
            fr = new FileReader("C:\\dev\\io\\2019\\12\\test.txt");
            fw = new FileWriter("C:\\dev\\io\\2019\\12\\test-copy.txt");
            int readChar = 0;
            while ((readChar = fr.read()) != -1) {
                System.out.print((char) readChar);// 모니터에 호출
                fw.write(readChar);// 파일에 호출
            }
            // new char[2048] 같이 큰 숫자를 받을 때 주의해야 할 것이
            // 큰 뭉텅이를 한번에 받아오는 것이기에 마지막에 빈 공백도
            // 같이 출력할 수 있다. => readCharCount를 이용해서 읽은 만큼만 write하자
            char[] readChars = new char[10];
            int readCharCount = 0;
            while ((readCharCount = fr.read(readChars)) != -1) {
                // char[] => String으로 바꿔주는 생성자
                System.out.print(new String(readChars, 0, readCharCount));        
                // 중요
                // 이 과정에서 마지막에 10개보다 적은 수의 char를 받아오기에
                // 전에 읽었던 char들이 남아있을 수 있어서 주의해야함
                // 그래서 readCharCount만큼 write해줘야함
                fw.write(readChars, 0, readCharCount);// 이걸 주로 사용
                // fw.write(new String(readChars, 0, readCharCount));
                // String 자체를 이용해서 write도 할 수있다.
            }
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                if (fr != null)
                    fr.close();
                if (fw != null)
                    fw.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
 

 text file을 받아서 쓰고싶을 때는 FileReader/Writer를 사용한다. 여기서도 char배열을 이용해 덩어리로 처리하는 방식을 적용했는데 문제가 하나 있다. 문자열을 가지고 char배열에 넣었을 때 전에 있었던 문자열 길이가 더 길면 뒷부분에 쓰레기 문자들이 남게 된다. 그렇기에 write(char[] c, int start, int length)를 이용해 읽어 들인 갯수만큼만 인식해서 쓰기 처리하는 것이 좋다.

 

 그런데 text file을 읽고 쓰는 것은 이 방법보다 Stream Chaining해서 사용하는 것이 좋다. 

 

 

5. BufferedReader / BufferedWriter / PrintWriter

≫BufferedReader

Reader 객체를 매개변수로 받아 Chaining한다.

≫BufferedWriter

Writer 객체를 매개변수로 받아 Chaining한다. readLine을 통해 한 줄 단위로 받아낸다. but newLine을 통해 구분시켜줘야 한다는 점과 마지막에 flush를 통해 읽은 것들을 비워내야 한다는 점은 유의해야 한다. 

≫PrintWriter

OutputStream과 Writer객체 둘다 매개변수로 받을 수 있다. flush 수행을 생성자로 받을 수 있고 println 메서드를 통해 개행처리를 따로 하지 않아도 된다.

 

BufferedReaderWriterDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package java_20191203;
 
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
 
public class BufferedReaderWriterDemo {
    public static void main(String[] args) {
        FileReader fr = null;
        FileWriter fw = null;
        BufferedReader br = null;
        BufferedWriter bw = null;
 
        try {
            fr = new FileReader("C:\\dev\\io\\2019\\12\\test.txt");
            br = new BufferedReader(fr); // chaining
 
            fw = new FileWriter("C:\\dev\\io\\2019\\12\\test-copy2.txt");
            bw = new BufferedWriter(fw);
 
            // 한 줄씩 받기
            String readLine = null;
            while ((readLine = br.readLine()) != null) {
                System.out.println(readLine);
                bw.write(readLine);
                bw.newLine(); // 개행을 해주는 역할
                // linux에서는 개행이 \r, 여기서는 \n
                // PrintWriter에서는 println()으로 개행까지 같이 작업가능
            }
            bw.flush(); // 혹시나 남아있을 수 있기에
 
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                if (fr != null)
                    fr.close();
                if (br != null)
                    br.close();
                // if (fw != null)
                // fw.close();
                if (bw != null)
                    bw.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
 

 

 BufferedReader(Writer)를 이용해 Stream Chaining을 하면 readLine 메서드를 이용해 한 줄 단위로 받을 수 있다. 이 장점을 이용해 간편하게 코드를 구성할 수 있다. 하지만 조금 부족한 것이 개행을 알리는 newLine을 해줘야 한다는 것과 한 줄 단위로 받기 때문에 한 줄을 못채우고 끝난 맨 마지막 부분이 쓰기가 안될 수도 있다. 그렇기에 반복문을 빠져나오면 flush를 통해서 이러한 것들을 다 없애야 한다.

1
2
3
4
5
6
7
pw = new PrintWriter(bw, true); // true => autoFlush
// 한 줄씩 받기
String readLine = null;
while ((readLine = br.readLine()) != null) {
    pw.println(readLine);
    // 개행까지 작업해준다.
}
 

 이 때 PrintWriter를 한번 더 Chaining해서 autoflush 기능을 true로 설정하면 println 메서드 하나로 위의 BufferedWriter에서 해줘야 하는 기능들을 한번에 해결할 수 있다.

 

 

6. InputStreamReader / OutputStreamWriter (1byte > 2byte 변환)

≫InputStreamReader

≫OutputStreamWriter

 Input(Output)Stream을 매개변수로 받아서 2byte 문자스트림으로 바꿔준다. (1byte → 2byte로)

 

 네트워크나 키보드(System.in)에서 입력을 1byte 스트림으로 받는데 여기에 text 스트림도 내포되어 있어서 text스트림으로 바꿔주는 것이 좋다. 그때 InputStreamReader를 이용해서 변환을 시켜준다.

 

InputStreamReaderDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package java_20191203;
 
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
 
public class InputStreamReaderDemo {
    public static void main(String[] args) {
        System.out.print("입력하세요>");
        InputStream in = System.in// Source가 키보드인 경우
        InputStreamReader isr = null;
        BufferedReader br = null;
        FileWriter fw = null;
        BufferedWriter bw = null;
        PrintWriter pw = null;
        
        isr = new InputStreamReader(in);
        br = new BufferedReader(isr);
        
        try {
            fw = new FileWriter("C:\\dev\\io\\2019\\12\\test-copy2.txt");
            bw = new BufferedWriter(fw);
            pw = new PrintWriter(bw, true); // true => autoFlush    
            
            String readLine = null;
            // 키보드 입력을 기다리다 엔터를 치면 읽는다.
            while ((readLine = br.readLine()) != null) {
                if (readLine.equals("exit"))
                    break;
                System.out.println(readLine);
                System.out.print("입력하세요>");
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
 
            try {
                if (in != null)
                    in.close();
                if (br != null)
                    br.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}
r

 

 

7. File Class

 

FileDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package java_20191202;
 
// mkdir, length(), renameTo을 주로 사용
 
public class FileDemo {
    public static void main(String[] args) {
 
        File f1 = new File("c:\\dev\\io\\2019\\12");
        // 해당 경로에 폴더가 생김
        boolean isSuccess = f1.mkdirs();
        // mkdir는 디렉토리 한개만 생성
        System.out.println(isSuccess);
 
        File f2 = new File(f1, "jdk-11.0.3_windows-x64_bin.exe");
        // 기본적으로 byte 단위
        // 반환값이 20억을 넘어갈 수도 있다.
        long fileSize = f2.length() / (1024 * 1024);
        System.out.println(fileSize); // MB 단위
        // 1970년 1월 1일부터 최종 수정날짜까지
        // milliseconds 단위로 계산해서 출력
        long lastModified = f2.lastModified();
 
        // 디렉토리 안에 모든 파일, 디렉토리 출력하기
        File f3 = new File("c:\\");
        // list(): c 드라이브에 있는 모든 파일과 디렉토리를 String[] 배열로 반환
        String[] list = f3.list();
 
        for (String temp : list) {
            File f4 = new File(f3, temp);
            if (f4.isDirectory()) {
                System.out.println("디렉토리: " + temp);
            } else if (f4.isFile()) {
                System.out.println("파일: " + temp);
            } else {
                System.out.println("?: " + temp);
            }
        }
        // 파일이름 바꾸기
        File f5 = new File(f1, "jdk.exe");
        f2.renameTo(f5);
 
        File f6 = new File(f1, "a.txt");
        try {
            f6.createNewFile();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
 
        System.out.println(f6.getName());// 파일이름(확장자까지)
        System.out.println(f6.getPath());// 전체경로
        System.out.println(f6.getParent());// 상위디렉토리
 
        // a.txt => System.currentTimeMillis().txt 파일로
        String extension = f6.getName().substring(f6.getName().lastIndexOf("."));
        File f7 = new File(f1, System.currentTimeMillis() + extension);
        f6.renameTo(f7);
       f7.delete(); // 삭제할 때
    }
}
 

- canRead( ) : 읽을 수 있는 파일 - true / 아니면 - false

- canWrite( ) : 쓸수 있는 파일 - true / 아니면 - false

- delete( ) : 파일을 지우고 성공 - true / 실패 - false

- exists( ) : 파일이나 디렉토리가 존재하면 - true / 아니면 - false

- getName( ) : 확장자까지 포함한 파일이름

- isDirectory( ) : 디렉토리이면 - true / 아니면 - false

- isFile( ) : 파일이면 - true / 아니면 - false

- lastModified( ) : 1970년 1월 1일부터 최종 수정날짜까지 밀리세컨드 초로 반환한다.

- length( ) : 파일의 크리를 바이트로 반환

- list( ) : 특정디렉토리의 모든 파일과 그 안의 디렉토리들을 스트링 배열로 반환

- mkdir( ) : 디렉토리 생성하면 - true / 실패하면 - false

- renameTo(File dest) : dest 파일객체로 이름을 바꾸면 - true / 실패하면 - false

 

 

 

※정리

 

입력

1) 키보드: (System.in) InputStream > InputStreamReader > BufferedReader

2) 네트워크: 키보드와 같다. BufferedReader

3) 파일: binary - FileInputStream (binary배열로)

             text - FileReader 

 

출력

1) 모니터: System.out으로 출력

2) 파일: binary - FileOutputStream 

           text - FileWriter > BufferedWriter > PrintWriter

3) 네트워크: OutputStream > OutputStreamWriter > BufferedWriter


InputStream / OutputStream(byte 배열 이용해서 쓴다.)

BufferedInputStream / BufferedOutputStream(잘 안쓴다. flush()를 해야 한다.)

PrintStream (Buffered를 안쓰니 잘 안씀, flush()할 필요가 없다.)

 

FileReader / FileWriter (텍스트 파일 받을 때)

BufferedReader / BufferedWriter(newLine(), flush()를 해야한다.) (이걸 많이 쓴다. )

PrintWriter(println()이라는 좋은 메서드 존재, newLine, flush를 동시에 해결해준다.)

InputStreamReader / OutputStreamWriter (1byte → 2byte 처리, 네트워크, 키보드 받을 때)