JAVA Basic) 자바 입출력
Programming/Java 기초

JAVA Basic) 자바 입출력

반응형

목차

     


     

    <자바 입출력>

    대부분의 프로그램은 자료를 입력받는 기능과 저장하거나 쓰는 출력 기능을 구현한다.

    음악, 동영상 파일을 재생하는 것도 입출력 기능에 해당되며, 채팅을 하고 SNS에 글, 사진을 피드하는 것도 입출력 기능으로 구현된다.


    <자바 입출력과 스트림>

    입출력은 프로그램의 가장 기본 기능이지만, 외부 저장 장치나 네트워크와 연동해야 하기 때문에 장치에 따라 다르게 구현해야 한다. 자바는 장치에 따라 독립적이고 효율적인 입출력 기능을 제공한다.

     

    스트림이란?

    자바에서 모든 입출력은 스트림(Stream)을 통해 이루어진다. 자료 흐름이 물의 흐름과 같다는 의미에서 사용되었다.
    입출력 장치는 매우 다양하여 장치에 따라 입출력 부분을 일일이 다르게 구현하면 프로그램 호환성이 떨어질 수 밖에 없다. 이런 문제를 해결하기 위해 자바는 입출력 장치와 무관하고 일관성 있게 프로그램을 구현할 수 있도록 일종의 가상 통로인 스트림을 제공하는 것.

    자료를 읽어 들이려는 소스(source)와 자료를 쓰려는 대상(target)에 따라 각각 다른 스트림 클래스를 제공한다.

    자바에서 입출력 기능을 사용하는 곳은 파일 디스크, 키보드, 모니터, 메모리 입출력, 네트워크 등이 있다.
    이러한 곳에서 일어나는 모든 입출력 기능을 스트림 클래스로 제공한다. 따라서 자바에서 자료를 입출력하려면 여러 스트림 클래스에 대해 알아야 하지만, 구현 방식이 서로 비슷하므로 크게 걱정할 필요가 없다.

    스트림을 세 가지 기준에 따라 분류해 생각해 보자.

     

    입 · 출력 스트림

    어떤 대상으로부터 자료를 읽어 들일 때 사용하는 스트림이 입력 스트림이다.
    예를 들어 입력 스트림은 어떤 동영상을 재생하기 위해 동영상 파일에서 자료를 읽을 때 사용한다.

    편집 화면에 사용자가 쓴 글을 파일에 저장할 때는 출력 스트림을 사용한다.

    스트림은 단방향으로 자료가 이동하기 때문에 입력과 출력을 동시에 할 수 없다. 입력 자료의 이동이 출력 자료의 이동과 한 스트림에서 동시에 일어날 수 없기 때문. 일방 통행인 길에서 차가 양방향으로 다닐 수 없는 것 처럼 말이다.
    따라서 어떤 스트림이 있다고 하면 그 스트림은 입력 스트림 또는 출력 스트림이다.

    스트림의 이름을 보면 입력용인지 출력용인지 구분이 가능하다.

    • InputStream이나 Reader로 끝나는 이름의 클래스는 입력 스트림.
    • OutputStream이나 Writer로 끝나는 이름의 클래스는 출력 스트림. 
    종류 예시
    입력 스트림 FileInputStream, FileReader, BufferInputStream, BufferReader 등
    출력 스트림 FileOutputStream, FileWriter, BufferOutputStream, BufferWriter 등

     

    바이트 · 문자 단위 스트림

    원래 자바의 스트림은 바이트(Byte)단위로 자료의 입출력이 이루어진다. 그러므로 그림, 동영상, 음악 파일 등 대부분 파일은 바이트 단위로 읽거나 쓰면 된다.
    그런데 자바에서 하나의 문자를 나타내는 Char형은 2바이트이기 때문에 1바이트만 읽으면 한글과 같은 문자는 깨져버리고 만다.
    따라서 입출력 중 가장 많이 사용하는 자료인 문자를 위해 문자 스트림을 별도로 제공한다.
    즉 읽어 들이는 자료형에 따라 바이트용과 문자용 스트림이 있다.

    바이트와 문자 단위 스트림

    • Stream으로 끝나는 이름은 경우 바이트 단위를 처리하는 스트림 클래스
    • Reader나 Writer로 끝나는 이름은 문자를 위한 스트림 클래스
    종류 예시
    바이트 스트림 FileInputStream, FileOutputStream, BufferInputStream, BufferOutputStream등
    문자 스트림 FileReader, FileWriter, BufferReader, BufferWriter 등

     

    기반 · 보조 스트림

    어떤 스트림이 자료를 직접 읽고 쓰는 기능을 제공하는지. 또는 자료를 직접 읽고 쓰는 기능은 없지만 다른 스트림에 부가적인 기능을 제공하는가에 따라 기반 스트림과 보조 스트림으로 구분할 수 있다.

    • 기반 스트림은 읽어 들일 곳(소스)나 써야할 곳(대상)에서 직접 읽고 쓸 수 있으며 입출력 대상에 직접 연결되어 생성되는 스트림이다.
    • 보조 스트림은 직접 읽고 쓰는 기능은 없으며, 항상 다른 스트림을 포함하여 생성된다.

    다음 그림처럼 기반 스트림에 보조 스트림을 더하여 기능을 추가한다.

    기반 스트림과 보조 스트림

    기반 스트림인지 보조 스트림인지는 이름으로 구분하기 어려울 수 있다.
    대부분의 기반 스트림이 소스나 대상의 이름을 가지고 있지만, 보조 스트림 중에도 이름만 봐서 바로 알 수 없는 경우도 있으므로 많이 사용하는 클래스 위주로 기억하면 된다.

    종류 예시
    기반 스트림 FileInputStream, FileOutputStream, FileReader, FileWriter등
    보조 스트림 InputStreamReader, OutputStreamWriter, BufferedInputStream, BufferedOutputStream 등

    이처럼 하나의 스트림 클래스는 세 가지 분류로 나눌 수 있다.
    예를 들어 FileInputSteam을 보면,

    • Input이므로 입력 스트림
    • Stream이므로 바이트 단위로 처리하는 스트림
    • File을 직접 읽는 기반 스트림이다.

    즉 자바 스트림 클래스 이름에서 스트림 특성을 유추할 수 있다. 그리고 각 스트림을 사용하는 메서드도 거의 같은 편이다.


    <표준 입출력>

    자바에서는 화면에 출력하고 입력받는 '표준 입출력 클래스'를 미리 정의해 두었다.
    이 클래스는 프로그램이 시작될 때 생성되므로 따로 만들 필요가 없다. 우리가 지금까지 화면 출력을 위해 사용한 System.out은 표준 출력을 위한 객체다.
    '표준 입출력'은 콘솔 화면에 입출력된다고 하여 '콘솔 입출력'이라고도 부른다.

    표준 입출력을 위한 System 클래스는 다음과 같이 세 개의 변수를 가지고 있다.

    자료형 변수 이름 설명
    static PrintStream out 표준 출력 스트림
    static InputStream in 표준 입력 스트림
    static OutputStream err 표준 오류 출력 스트림

    System.out은 표준 출력용, System.In은 표준 입력용 스트림이다.
    빨간색으로 오류 메시지를 출력할 때는 System.err를 사용한다.
    out, in, err 모두 정적(static)변수이다. 지금까지 우리가 System 클래스를 생성하지 않고도 System.out을 사용할 수 있었던 이유는 out변수가 System 클래스의 정적 변수이기 때문이었다.

    지금까지 System.out을 활용한 프로그램은 많이 실습해 보았으므로 사용자로부터 콘솔 입력을 받는 System.in을 사용해보도록 하자.

     

    System.in으로 화면에서 문자 입력받기

    System.in을 사용해 문자를 입력받는 프로그램을 구현해 보자.
    입출력에 관련한 코드를 구현하면 예외 처리를 해야 한다.

    그럼 다음과 같이 하나의 문자를 입력받는 코드를 구현해보자.

    package stream.inputstream;
    
    import java.io.IOException;
    
    public class SystemInTest1 {
    	public static void main(String[] args) {
    		System.out.println("알파벳 하나를 쓰고 [Enter]를 누르세요.");
    		
    		int i;
    		try {
    			i = System.in.read();
    			System.out.println(i); // i는 int형이므로 알파벳을 정수로 출력
    			System.out.println((char)i); //정수를 char형으로 변환
    		} catch(IOException e) {
    			System.out.println(e);
    		}
    	}
    }

    A를 입력하고 Enter를 눌렀다.

    프로그램을 실행하면 입력을 받기 위해 커서가 기다린다.

    • A라고 알파벳을 입력하고 Enter를 누르면 입력한 값이 변수 i로 들어간다.
    • i는 4바이트(int형)지만 System.in은 바이트 단위로 읽어 들이는 InputStream이므로 1바이트만 읽는다.
    • 읽어 들인 1바이트를 출력하면 문자에 대한 값, 즉 아스키 값을 출력 => 65
    • 문자(char형)으로 변환하여 출력하면 입력한 A가 출력된다.

    읽어 들일 때 사용한 InputStream의 read()메서드는 한 바이트만을 읽어 들인다.

    이번에는 알파벳 여러개를 쓰고 Enter(\n)을 눌러 입력받는 예제를 보자.

    package stream.inputstream;
    
    import java.io.IOException;
    
    public class SystemInTest2 {
    	public static void main(String[] args) {
    		System.out.println("알파벳 하나를 쓰고 [Enter]를 누르세요.");
    		
    		int i;
    		try {
    			//while문에서 read()메서드로 한 바이트씩 반복해서 읽음
    			while((i = System.in.read()) != '\n') { // '\n'대신 -1을 입력해도 결과는 같음
    				System.out.print((char)i);
    			}
    		} catch(IOException e) {
    			System.out.println(e);
    		}
    	}
    }

    문자 여러개를 입력함

    while문에서 read() 메서드를 이용해 한 바이트씩 읽어들인다.

    • Enter에 해당하는 '\n'값이 입력되기 전까지 read()를 반복하고, \n이 입력되면 읽어들인 내용을 화면에 출력한다.

     

    그 외 입력 클래스

    Scanner 클래스

    Scanner 클래스는 jvav.util 패키지에 있는 입력 클래스.
    Scanner 클래스는 문자뿐 아니라 정수, 실수 등 다른 자료형도 읽을 수 있다. 또한 콘솔 화면뿐 아니라 파일이나 문자열을 생성자의 매개변수로 받아 자료를 읽어 올 수 있다. 여러 대상에서 자료를 읽는 Scanner클래스의 생성자는 굉장히 다양하다.

    대표 생성자를 살펴보자.

    생성자 설명
    Scanner(File source) 파일을 매개변수로 받아 Scanner를 생성한다.
    Scanner(InputStream source) 바이트 스트림을 매개변수로 받아 Scanner를 생성.
    Scanner(String source) String을 매개변수로 받아 Scanner를 생성.

    Scanner scanner = new Scanner(System.in)처럼 사용하면 표준 입력으로부터 자료를 읽어 들이는 기능을 사용할 수 있다.
    Scanner 클래스는 다양한 메서드를 활용할 수 있기 때문에 자주 사용되는 클래스다.

    Scanner 클래스에서 제공하는 메서드는.

    메서드 설명
    boolean nextBoolean() boolean 자료를 읽는다.
    byte nextByte() 한 바이트 자료를 읽는다.
    short nextShort() short 자료형을 읽는다.
    int nextInt() int 자료형을 읽는다.
    long nextLong() long 자료형을 읽는다.
    float nextFloat() float 자료형을 읽는다.
    double nextDouble() double 자료형을 읽는다.
    String nextLine() 문자열 String을 읽는다.

     

    Scanner 클래스를 활용해 표준 입력에서 다양한 자료를 읽어 온 후 출력하는 예제를 보자.

    package stream.others;
    
    import java.util.Scanner;
    
    public class ScannerTest {
    	public static void main(String[] args) {
    		Scanner scanner = new Scanner(System.in);
    		//문자열을 읽는 nextLine() 메서드로 이름, 직업 입력받음
    		System.out.println("이름: ");
    		String name = scanner.nextLine();
    		System.out.println("직업: ");
    		String job = scanner.nextLine();
    		
    		//int형을 읽는 nextInt() 메서드로 사번을 입력받음
    		System.out.println("사번: ");
    		int num = scanner.nextInt();
    
    		System.out.println("====입력 결과====");
    		System.out.println(name);
    		System.out.println(job);
    		System.out.println(num);
    	}
    }

    • 표준 입력을 매개변수로 Scanner클래스를 생성했다.
    • 이름과 직업은 문자열이므로 nextLine() 메서드로 입력받고, 사번은 정수이므로 nextInt() 메서드를 사용한다.
    • 입력받은 자료를 그대로 출력하여 잘 입력되었는지 확인할 수 있다.

    표준 입력 System.in을 사용하면 바이트 단위 자료만 처리할 수 있어 한글과 같은 경우에는 보조 스트림을 추가로 사용해야 하는데, Scanner는 다양한 자료형을 입력할 수 있어 자주 활용된다.

     

    Console 클래스

    System.in을 사용하지 않고 간단히 콘솔 내용을 읽을 수 있는 Console 클래스도 있다.
    직접 콘솔 창에서 자료를 입력받을 때 이 클래슬ㄹ 사용하는데, 이클립스와는 연동되지 않는다.

    사용하는 메서드는 다음과 같다.

    메서드 설명
    String readLine() 문자열을 읽는다.
    char[] readPassword() 사용자에게 문자열을 보여주지 않고 읽는다.
    Reader reader() Reader 클래스를 반환한다.
    PrintWriter writer() PrintWriter 클래스를 반환한다.

    Console 클래스를 활용해 직접 cmd창에서 자료를 입력받는 실습을 해보자.

    package stream.others;
    
    import java.io.Console;
    
    public class ConsoleTest {
    	public static void main(String[] args) {
    		Console console = System.console(); //콘솔 클래스 반환
    		
    		System.out.println("이름:");
    		String name = console.readLine();
    		System.out.println("직업:");
    		String job = console.readLine();
    		
    		System.out.println("비밀번호:");
    		char[] password = console.readPassword();
    		String strPW = new String(password);
    		
    		System.out.println(name);
    		System.out.println(job);
    		System.out.println(strPW);
    	}
    }

    이 예제를 실행하기 위해 명령 프롬프트(cmd) 창을 띄우고 프로젝트 폴더로 이동한다.
    (cd : 해당 디렉토리로 이동하는 명령어, dir : 해당 디렉토리에 있는 파일, 폴더를 보여줌)
    거기에서 다시 클래스가 컴파일 되어있는 bin 폴더로 이동한 후 java stream.others.ConsoleTest라고 입력한다.
    주의해야 할 점은 명령 프롬프트 창에서 실행할 때 패키지의 상위 폴더에서 패키지 이름까지 포함한 전체 클래스 이름을 써야 한다.

    cmd에서 실행한 자바

    • readLine() 메서드를 사용해 이름과 직업을 직접 문자열로 입력받는다.
    • readPassword() 메서드를 사용해 비밀번호를 char[]배열로 입력받는다. 비밀번호를 입력할 때는 창에 입력한 문자가 나타나지 않는다.
    • Console클래스는 Scanner와 마찬가지로 한글도 읽을 수 있다. 
      다만 이클립스와 같은 IDE(통합 개발 환경)에서는 Console 클래스가 연동되지 않는 경우가 있어 Scanner 클래스를 더 많이 사용한다.

    <바이트 단위 스트림>

    이제부터 다양한 스트림의 종류와 사용 방법에 대해 자세히 알아보도록 하자.
    여기에서 설명하는 스트림은 입출력 기능을 구현하는 데 기본으로 알아야 하는 클래스와 메서드다. 이들 클래스를 모두 외워야 하는 것은 아니지만, 기본 사용법을 익히고 나중에 프로그램을 개발할 때 원하는 기능의 클래스를 잘 찾아서 사용할 수 있으면 된다.

    바이트 입출력 스트림부터 살펴보자.

    InputStream

    바이트 단위로 읽는 스트림 중 최상위 스트림.

    InputStream은 추상 메서드를 포함한 추상 클래스로서 하위 스트림 클래스가 상속받아 각 클래스 역할에 맞게 추상 메서드 기능을 구현한다.

    주로 사용하는 하위 클래스는 다음과 같다.

    스트림 클래스 설명
    FileInputStream 파일에서 바이트 단위로 자료를 읽는다.
    ByteArrayInputStream Byte 배열 메모리에서 바이트 단위로 자료를 읽는다.
    FileInputStream 기반 스트림에서 자료를 읽을 때 추가 기능을 제공하는 보조 스트림의 상위 클래스.

    InputStream은 바이트 자료를 읽기 위해 다음 메서드를 제공한다.

    메서드 설명
    int read() 입력 스트림으로부터 한 바이트의 자료를 읽는다.
    읽은 자료의 바이트 수를 반환한다.
    int tead(byte b[]) 입력 스트림으로부터 b[] 크기의 자료를 b[]에 읽는다.
    읽은 자료의 바이트 수를 반환한다.
    int read(byte b[], int off, int len) 입력 스트림으로부터 b[] 크기의 자료를 b[]의 off변수 위치부터 저장하여 len만큼 읽는다.
    읽은 자료의바이트 수를 반환한다.
    void close() 입력 스트림과 연결된 대상 리소스를 닫는다.
    (ex : FileInputStream인 경우 파일 닫음)

    read() 메서드의 반환형은 int이다. 한 바이트를 읽어 int에 저장한다.
    한 바이트만 읽는 데 반환형이 int인 이유는 더 이상 읽어 들일 자료가 없는 경우에 정수 -1을 반환하기 때문이다.
    파일에서 자료를 읽는 경우 파일의 끝에 도달하면 -1이 반환된다.

    그러면 InputStream 중 가장 많이 사용하는 FileInputStream 클래스를 보자. 

     

    FileInputStream

    FileInputStream은 파일에서 바이트 단위로 자료를 읽어 들일 때 사용하는 스트림 클래스다.
    스트림을 사용하기 위해서는 먼저 스트림 클래스를 생성해야 한다.

    FileInputStream의 생성자를 살펴보자.

    생성자 설명
    FileInputStream(String name) 파일 이름 name(경로 포함)을 매개변수로 받아 입력 스트림을 생성한다.
    FileInputStream(File f) File 클래스 정보를 매개변수로 받아 입력 스트림을 생성.


    FileInputStream(String name)생성자로 스트림을 생성하여 파일로부터 자료를 읽는 예제를 보자.

    package stream.inputstream;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    
    public class FileInputStreamTest {
    	public static void main(String[] args) {
    		FileInputStream fis = null;
    		
    		try {
    			fis = new FileInputStream("input.txt"); //input.txt 파일 입력 스트림 생성
    			System.out.println(fis.read());
    			System.out.println(fis.read());
    			System.out.println(fis.read());
    		} catch(IOException e) {
    			System.out.println(e);
    		} finally {
    			try {
    				fis.close(); //열린 스트림은 finally 블록에서 닫음
    			} catch (IOException e) {
    				System.out.println(e);
    			//스트림이 null인 경우	
    			} catch(NullPointerException e) {
    				System.out.println(e);
    			}
    		}
    		System.out.println("end");
    	}
    }

    첫 예제이므로 모든 예외 처리를 try-catch문으로 구현했다.
    try-catch문을 보면 파일 스트림을 생성하고 활용할 때 어떤 예외가 발생하고 어떻게 처리해야 하는지 알 수 있다.

    • FileInputStream("input.txt")생성자로 input.txt파일에 입력 스트림을 생성하려고 한다. (파일이 존재하지 않은 상태)
    • FileInputStream은 읽으려는 파일이 없으면 FileNotFoundException예외가 발생한다. 따라서 스트림을 생성하려다가 IOException(FileNotFoundException의 상위 예외 클래스)이 발생해 catch된다.
    • 그리고 finally 블록에서 열려있는 스트림을 close하기 위해 close()를 호출하는데, 스트림이 생성되지 않았으므로 NullPointerException은 처리하지 않을 때 컴파일 오류가 발생하는 예외가 아니므로 어떤 예외 클래스로 처리해야 할지 모르는 경우 최상위 예외 클래스 Exception을 사용하면 된다.

    수행 결과에서 알 수 있는 또하나의 사실은 예외 처리가 되어 프로그램이 중단되거나 멈춘 것이 아니라 "end" 부분까지 출력되었다는 점이다. 프로그램 수행을 중단시키지 않는 예외 처리의 중요성을 알 수 있는 부분이다.

     

    파일에서 자료 읽기

    이제 실제 파일에서 자료를 읽어보자.

    FileInputStream("input.txt")와 같이 쓰면 제일 먼저 input.txt 파일을 프로젝트 폴더에서 찾는다. 
    따라서 임의로 파일을 만들어 주자.
    [New File] 메뉴를 선택하고 그중 [File] 메뉴를 누르면 아래처럼 창이 나타난다.
    현재 프로젝트 폴더를 선택하고 input.txt파일을 생성한다.

     이렇게 생성된 파일은 다음처럼 볼 수 있다. input.txt파일을 직접 열고 다음처럭 작성 후 저장하자.

    프로젝트에 생성된 input.txt 파일 / input.txt에 ABC 문자를 작성하자

    ABC 세 개의 문자를 적었다. 이제 FileInputStreamTest.java를 다시 run하여 출력 결과를 보자.

    ABC 세 개의 아스키 코드를 출력

    input.txt에 적힌 ABC를 읽어 들여 알파벳의 아스키 코드 값을 출력했다. System.out의 read() 메서드는 한 바이트씩 자료를 읽어 들이기 때문이다.
    이를 A,B,C로 화면에 출력하려면 출력문을 다음처럼 char 자료형으로 변환해야 한다.

    System.out.println((char)fis.read());

    이렇게 문자로 변환하여 출력하면 아스키 코드 값에 해당하는 문자가 출력된다.

     

    파일 끝까지 읽기

    바로 전 예제에서는 input.txt에 문자가 세 개 포함된 것을 알고 있기 때문에 read() 메서드를 세번 호출해 파일에서 문자를 읽어 들였다.
    하지만 파일에 내용이 얼마큼 있는지 모르는 경우에는 파일의 끝에 도달할 때까지 반복해서 읽어야 할 것이다.

    다음은 input.txt 파일을 끝까지 읽는 방식으로 FileInputStreamTest1.java를 바꾼 예제이다. 
    try-with-resources문으로 구현해보자.

    package stream.inputstream;
    
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    
    public class FileInputStreamTest2 {
    
    	public static void main(String[] args) {
    		
    		try(FileInputStream fis = new FileInputStream("input.txt")){
    			int i;
    			//i 값이 -1이 아닌동안 read()메서드로 한 바이트씩 반복해 읽는다.
    			while((i = fis.read()) != -1) { 
    				System.out.print((char)i);
    			}
    			System.out.println();
    			System.out.println("end");
    		} catch(FileNotFoundException e) {
    			System.out.println(e);
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    	}
    }

    • read()메서드로 파일을 읽는 경우 파일의 끝에 도달하면 -1을 반환한다.
    • while문을 보면 read()메서드를 사용해 한 바이트씩 읽어 들이고 있다.
      읽어 들여 저장한 i값이 -1이 아닌동안 while문을 반복 수행한다.

     

    int read(byte[] b) 메서드로 읽기

    자료를 read() 메서드로 한 바이트씩 읽는 것보다 배열을 사용해 한번에 많이 읽으면 처리 속도가 훨씬 빠르다.
    read(byte[] b) 메서드는 선언한 바이트 배열의 크기만큼 한꺼번에 자료를 읽는다. 그리고 읽어들인 자료의 수를 반환한다.

    바이트 배열을 생성하고 배열을 사용해 자료를 읽어보자.
    이전 input.txt파일과 유사하게 input2.txt파일을 만들고 A~Z까지 알파벳을 적는다. 실제로는 더 큰 배열을 사용하지만, 테스트를 위해 10바이트 크기의 배열을 만들어 사용한다.

    package stream.inputstream;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    
    public class FileInputStreamTest3 {
    	public static void main(String[] args) {
    		try(FileInputStream fis = new FileInputStream("input2.txt")) {
    			byte[] bs = new byte[10];
    			int i;
    			while((i = fis.read(bs)) != -1) {
    				for(byte b : bs) {
    					System.out.print((char)b);
    				}
    				System.out.println(": " + i + "바이트 읽음");
    			}
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    		System.out.println("end");
    	}
    }

    A~Z까지 읽고 QRST가 더 출력됨.

    • byte[] bs = new byte[10]; 에서 크기 10인 바이트 배열 bs를 생성함.
    • while문에서 파일을 읽어 들이는 부분에 배열 bs를 매개변수로 넣는다.
      그리고 읽어들인 반환 값이 -1이 아닐동안, 즉 파일의 끝에 도달할 때 까지 읽는다.
    • 향상된 for문을 사용해 bs 배열에 들어있는 자료를 출력하고 몇 바이트를 읽었는지 출력한다.  

    배열 크기는 10이고 26개의 알파벳을 읽으므로 반복할 때 마다 읽는 알파벳 개수는 10, 10, 6이다.
    그런데 출력 화면을 보면 마지막에 6바이트를 읽었는데 QRST가 더 출력되었다. 이것은 두 번째 읽을 때 K~T 10개의 알파벳을 저장했고, 마지막으로 U~Z까지 6개 를 저장해 나머지 4개로 남아있는 것.
    따라서 6개만 읽었는데 bs 전체를 출력하면 다음처럼 출력되는 것이다.

    그럼 어떻게 출력해야할까?
    read(byte[] b)메서드의 반환 값은 읽어 들인 자료의 바이트 수이다. 이를 사용해 전체 배열을 출력하는 것이 아닌 바이트 수만큼, 즉 i개수만큼 출력하도록 코드를 바꾸면 된다.

    for(byte b : bs){
    	System.out.println((char)b);
    }

    를 다음처럼 바꾼다.

    for(int k = 0; k < i; k++){
    	System.out.println((char)bs[k]);
    }

    위 처럼 for문을 바꾸면 출력 결과는 다음처럼 나온다.

    깰끔쓰

    이처럼 메서드의 반환 값은 프로그래밍할 때 유용하게 쓰인다.
    그러므로 JavaDoc등에서 메서드를 찾아 사용할 때 매개변수 뿐 아니라 반환 값의 의미도 잘 확인해야 한다.

     

    OutputStream

    바이트 단위로 쓰는 스트림 중 최상위 스트림이다.

    자료의 출력 대상에 따라 다른 스트림을 제공한다.

    스트림 클래스 설명
    FileOutputStream 바이트 단위로 파일에 자료를 쓴다.
    ByteArrayOutputStream Byte 배열에 바이트 단위로 자료를 쓴다.
    FilterOutputStream 기반 스트림에서 자료를 쓸 때 추가 기능을 제공하는 보조 스트림의 상위 클래스


    OutputStream에서 제공하는 메서드는 다음과 같다.

    메서드 설명
    void write(int b) 한 바이트를 출력한다.
    void write(byte[] b) b[] 배열에 있는 자료를 출력한다.
    void write(byte[] b, int off, int len) b[] 배열에 있는 자료 off 위치부터 len개수 만큼 자료를 출력한다.
    void flush() 출력을 위해 잠시 자료가 머무르는 출력 버퍼를 강제로 비워 자료를 출력한다.
    void close() 출력 스트림과 연결된 대상 리소스를 닫는다.
    출력 버퍼가 비워진다.
    (예 : FileOutputStream인 경우 파일을 닫는다)


    OutputStream을 상속받은 클래스 중 가장 많이 사용하는 FileOutputStream을 활용하는 메서드 예제를 보자.

     

    FileOutputStream

    파일에 바이트 단위 자료를 출력하기 위해 사용하는 스트림.

    FileOutputStream을 생성하는 생성자는 다음과 같다.

    생성자 설명
    FileOutputStream(String name) 파일 이름 name(경로 포함)을 매개변수로 받아 출력 스트림을 생성한다.
    FileOutputStream(String name, boolean append) 파일 이름 name(경로 포함)을 매개변수로 받아 입력 스트림을 생성한다.
    append 값이 true면 파일 스트림을 닫고, 다시 생성할 때 파일의 끝에 이어서 쓴다.
    디폴트 값은 false.
    FileOutputStream(File file) File 클래스 정보를 매개변수로 받아 출력 스트림을 생성한다.
    FileOutputStream(File file, boolean append) File 클래스 정보를 매개변수로 받아 출력 스트림을 생성.
    append 값이 true면 파일 스트림을 닫고, 다시 생성할 때 파일의 끝에 이어서 쓴다.
    디폴트 값은 false.

    생성자 매개변수로 전달한 파일이 경로에 없으면 FileOutputStream은 파일을 새로 생성한다.
    FileOutputStream을 사용해 파일에 자료를 쓸 때 기존 파일의 내용이 있떠라도 처음부터 새로 쓸지(overwrite), 또는 기존 내용 맨 뒤에 연결해서 쓸지(append) 여부를 fileOutputStream 생성자의 매개변수로 전달한다. 이 값이 append변수.
    스트림 생성자에서 append값은 디폴트가 false다. 기존에 쓰여있는 내용이 있더라도 새로(overwrite)쓴다.
    기존 파일 내용에 이어서 써야한다면 append 값을 반드시 ture로 지정해야한다.

     

    write() 메서드 사용하기

    다음은 FileOutputStream을 생성하고 write()메서드를 활용해 파일에 정수 값을 저장하는 예제이다.

    package stream.ouputStream;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    public class FileOutputStreamTest {
    	public static void main(String[] args) {
    		try (FileOutputStream fos = new FileOutputStream("output.txt")){
    			//FileOutputStream은 파일에 숫자(ASCII)를 쓰면 해당하는 알파벳으로 변환된다.
    			fos.write(65);
    			fos.write(66);
    			fos.write(67);
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    		System.out.println("출력이 완료되었습니다.");
    	}
    }

    FileOutputStream fos = new FileOutputStream("output.txt");에서 output.txt라는 파일 이름으로 클래스를 생성한다.

    write() 메서드에 따라 파일에 값을 출력하고(쓰고) 스트림을 닫는다.

    이클립스에서 현재 프로젝트를 선택하고 refresh(F5)하면 생성된 output.txt파일이 보인다.
    생성된 파일을 열어 확인하면 다음과 같다.

     

    출력한 아스키 코드 값(65, 66, 67)에 해당하는 문자 A, B, C가 output.txt 파일에 쓰여졌다.
    FileOutputStream은 숫자를 해당 아스키 코드 값의 문자로 변환하여 저장한다.

    앞에서 실행한 FileOutputStreamTest를 한 번 더 실행하고 output.txt 파일을 확인하면 변화가 없는 것 처럼 보일 것이다.
    기존 ABC는 없어지고 새로운 ABC가 overwrite된 것이다.

    만약 기존 자료에 이어서 출력하길 원한다면 생성자의 두 번째 매개변수에 true를 입력하면 된다.

    FileOutputStream fos = new FileOutputStream("output.txt", ture);

    두 번째 매개변수 true 입력

    true 매개변수를 추가하고 실행하면 위 처럼 이어서 쓰이는 것을 확인할 수 있다.

     

    write(byte[] b) 메서드 사용하기

    출력도 입력과 마찬가지로 여러 자료를 한꺼번에 출력하면 효율적이고 실행 시간도 줄어든다.
    따라서 바이트 배열을 활용해 출력할 수 있다.

    write(byte[] b) 메서드는 바이트 배열에 있는 자료를 한꺼번에 출력한다. 코드를 보자.

    package stream.ouputStream;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    public class FileOutputStreamTest2 {
    	public static void main(String[] args) {
    		try (FileOutputStream fos = new FileOutputStream("output2.txt", true)) {
    			byte[] bs = new byte[26];
    			byte data = 65; //A의 아스키 값
    			//A~Z 배열에 넣기
    			for(int i = 0; i < bs.length; i++) {
    				bs[i] = data;
    				data++;
    			}
    			fos.write(bs); //배열은 한꺼번에 출력
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    		System.out.println("출력 완료");
    	}
    }

    출력 성공

    • byte[] bs = new byte[26]; : 26개 크기의 바이트 배열을 만들었다.
    • 그리고 for문으로 A~Z의 아스키 코드 값을 넣어 출력할 바이트 배열을 만들었다.
    • for문 밖에서 fos.write(bs);를 호출해 전체 바이트 배열을 한꺼번에 출력한다.

    output2.txt파일을 확인하면 A~Z까지 출력되어 쓰여있음을 확인할 수 있다.

    A~Z까지 출력된 모습

    바이트 배열을 사용해 파일 출력 스트림을 생성할 때도 생성자 두 번째 매개변수에 true라고 쓰면 이미 쓰인 자료에 연결된다.

    한 번 더 실행하면 append된다("output2.txt",true)

     

    write(byte[] b, int off, int len) 메서드 사용하기

    write(byte[] b, int off, int len) 메서드는 배열의 전체 자료를 출력하지 않고 배열의 off위치 부터 len 길이만큼 출력한다.
    예를 들어 앞 예제에서 만든 bs 배열을 사용한다고 했을 때 write(bs, 2, 10)라고 쓰면,
    bs배열의 두 번째 인덱스, 즉 세 번째 위치 부터 10개 바이트 자료만 출력한다.

    즉, 배열 자료 중 일부를 출력할 때 사용할 수 있다. 예제를 보자.

    package stream.ouputStream;
    
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    public class FileOutputStreamTest3 {
    	public static void main(String[] args) {
    		try (FileOutputStream fos = new FileOutputStream("output3.txt", true)) {
    			byte[] bs = new byte[26];
    			byte data = 65;
    			for(int i = 0; i < bs.length; i++) {
    				bs[i] = data;
    				data++;
    			}
    			//배열의 세 번째 위치부터 10개 바이트 출력
    			fos.write(bs, 2, 10); 
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    		System.out.println("출력 완료");
    	}
    }

    • FileOutputStream2 예제에서 write() 메서드만 변경했다.
    • fos.write(bs, 2, 10)로 변경하여 두 번째 인덱스(세 번째 위치)부터 10개를 출력한다.

    output3.txt를 확인하면.

    배열에 저장된 자료 중 세 번째인 C부터 L까지 10개 바이트가 정상적으로 출력되었다.

    flush(), close() 메서드

    출력 스트림에서 flush() 메서드의 기능은 강제로 자료를 출력하는 것이다.

    write() 메서드로 값을 썼다고 해도 바로 파일이나 네트워크로 전송되지 않고 출력을 위한 자료가 쌓이는 출력 버퍼에 어느정도 자료가 모여야 출력이 된다.
    따라서 자료의 양이 출력할 만큼 많지 않으면 write()메서드로 출력했어도 파일에 쓰이지 않거나 전송되지 않을 수 있는데, 이런 경우에 flush()메서드를 호출한다.

    출력 스트림의 close() 메서드 안에서 flush()메서드를 호출하여 출력 버퍼가 비워지며 남아있는 자료가 모두 출력된다.

    ※ 바로바로 전송되는 채팅 메시지와 같은 경우는 flush() 메서드를 호출하는 것이 좋다.


    <문자 단위 스트림>

     

    Reader

    문자 단위로 읽는 스트림 중 최상위 스트림.
    다음 하위 클래스를 주로 사용한다.

    스트림 클래스 설명
    FileReader 파일에서 문자 단위로 읽는 스트림 클래스
    InputStreamReader 바이트 단위로 읽은 자료를 문자로 변환해주는 보조 스트림 클래스
    BufferedReader 문자로 읽을 때 배열을 제공하여 한꺼번에 읽을 수 있는 기능을 제공해 주는 보조 스트림


    다음과 같이 자료를 읽는 메서드를 제공한다.

    메서드 설명
    int read() 파일로부터 한 문자를 읽는다. 읽은 값을 반환한다.
    int read(char[] buf) 파일로부터 buf 배열에 문자를 읽는다.
    int read(char[] buf, int off, int len) 파일로부터 buf 배열의 off 위치부터 len 개수만큼 문자를 읽는다.
    void close() 스트림과 연결된 파일 리소스를 닫는다.

     

    그럼 Reader 중 가장 많이 사용하는 FileReader에 대해 살펴보자.

     

    FileReader

    FileReader를 생성하는 데 사용하는 생성자는 다음과 같다.
    FileInputStream과 마찬가지로 읽으려는 파일이 없으면 FileNotFoundException이 발생한다.

    생성자 설명
    FileReader(String name) 파일 이름 name(경로 포함)을 매개변수로 받아 입력 스트림을 생성
    FileReader(File file) File 클래스 정보를 매개변수로 받아 입력 스트림을 생성

     

    Reader 스트림을 활용하지 않고 바이트 단위로 문자르 ㄹ읽을 때 문자가 어떻게 되는지 알아보기 위해 먼저 앞에서 사용한 FileInputStream으로 자료를 읽어 보도록 하겠다.

    현재 프로젝트에 reader.txt파일을 만들고 오른쪽과 같이 한글로 '안녕하세요'라고 적은 후 바이트 단위로 읽었을 때 어떻게 되는지 확인해 보자.

    앞에서 만든 FileInputStreamTest2예제에서 스트림을 생성할 때 한글을 적은 reader.txt파일을 매개변수로 입력하여 생성한다.

    public class FileInputStreamTest2 {
    	public static void main(String[] args) {
    		//FileInputStream을 생성할 때 reader.txt파일을 매개변수로.
    		try(FileInputStream fis = new FileInputStream("reader.txt")){
    			int i;
    			while((i = fis.read()) != -1) {
    				System.out.print((char)i);
    			}
    			System.out.println();
    			System.out.println("end");
    		} catch(FileNotFoundException e) {
    			System.out.println(e);
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    	}
    }

    글씨가 깨졌다!

    예제를 실행하여 출력 결과를 보면 한글 문자가 모두 깨진 것을 볼 수 있다.

    한글을 바이트 단위로 읽어와서 무슨 글자인지 알 수 없다. 따라서 문자를 입출력할 때는 문자 스트림을 사용해야 한다.

    이제 FileReader로 reader.txt를 다시 읽어 보겠다.

    package stream.reader;
    
    import java.io.FileReader;
    import java.io.IOException;
    
    public class FileReaderTest {
    	public static void main(String[] args) {
    		
    		try (FileReader fr = new FileReader("reader.txt")) {
    			int i;
    			while((i = fr.read()) != -1) {
    				System.out.print((char)i);
    			} 
    		}catch(IOException e) {
    				System.out.println(e);
    		}
    	}
    }

    깰끔쓰

    문자 스트림 FileReader로 읽으면 한글이 제대로 읽히는 것을 알 수 있다. 이처럼 Reader클래스는 문자를 처리할 때 사용하는 클래스다.
    나머지 read(char[] buf)메서드read(char[] buf, int off, int len)메서드의 내용은 FileInputStream과 유사하다.

    Writer

    문자 단위로 출력하는 스트림 중 최상위 스트림.

    다음 하위 클래스를 주로 사용한다.

    스트림 클래스 설명
    FileWriter 파일에 문자 단위로 출력하는 스트림 클래스
    OutputStreamWriter 파일에 바이트 단위로 출력한 자료를 문자로 변환해주는 보조 스트림
    BufferedWriter 문자로 쓸 때 배열을 제공하여 한꺼번에 쓸 수 있는 기능을 제공해주는 보조 스트림


    다음과 같이 자료를 읽는 메서드를 제공한다.

    메서드 설명
    void write(int c) 한 문자를 파일에 출력한다.
    void write(char[] buf) 문자 배열 buf의 내용을 파일에 출력한다.
    void write(char[] buf, int off, int len) 문자 배열 buf의 off위치에서부터 len 개수의 문자를 파일에 출력한다.
    void write(String str) 문자열 str을 파일에 출력한다.
    void write(String str, int off, int len) 문자열 str의 off번째 문자부터 len 개수만큼 파일에 출력한다.
    void flush()  파일에 출력하기 전에 자료가 있는 공간(출력 버퍼)을 비워 출력한다.
    void close() 파일과 연결된 스트림을 닫는다.
    출력 버퍼도 비워진다.

    Writer 스트림 중 가장 많이 사용하는 FileWriter 스트림 클래스로 자료를 출력해보자.

    FileWriter

    다른 스트림 클래스와 마찬가지로 생성자를 사용해 스트림을 생성한다.
    FIleOutputStream과 마찬가지로 출력 파일이 존재하지 않으면 파일을 생성한다.

    생성자는 다음과 같다.

    생성자 설명
    FileWriter(String name) 파일 이름 name(경로 포함)을 매개변수로 받아 출력 스트림을 생성한다.
    FileWriter(String name, boolean append) 파일 이름 name(경로 포함)을 매개변수로 받아 출력 스트림을 생성한다.
    append 값이 true면 파일 스트림을 닫고 다시 생성할 때 파일 끝에 이어서 쓴다.
    append의 기본 값은 false.
    FileWriter(File file) File 클래스 정보를 매개변수로 받아 출력 스트림을 생성한다.
    FileWriter(File file, boolean append) File 클래스 정보를 매개변수로 받아 출력 스트림을 생성한다.
    append 값이 true면 파일 스트림을 닫고 다시 생성할 때 파일 끝에 이어서 쓴다.
    append의 기본 값은 false.

     

    Writer에서 제공하는 여러가지 메서드와 FileWriter를 사용해 실습해 보자.

    package stream.writer;
    
    import java.io.FileWriter;
    import java.io.IOException;
    
    public class FileWriterTest {
    	public static void main(String[] args) {
    		try (FileWriter fw = new FileWriter("writer.txt")) {
    			fw.write('A'); //문자 하나 출력
    			
    			char buf[] = {'B', 'C', 'D', 'E', 'F', 'G'};
    			fw.write(buf); //문자 배열 출력
    
    			fw.write("안녕하세요"); //문자열 출력
    			fw.write(buf, 1, 2); //문자 배열 일부 출력
    			fw.write("65"); //숫자 그대로 출력
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    		System.out.println("출력 완료");
    	}
    }

    출력쓰

    예제에서 여러 write() 메서드를 사용했다.

    • 문자 하나를 출력하는 경우.
    • 문자 배열을 생성하여 전체를 출력하는 것과
    • 배열 일부만 출력하는 경우.
    • 문자열 전체를 출력하는 경우.
    • 숫자 그대로를 출력하기 위해 ""를 사용한 경우.

    write.txt파일을 확인해 보면,

    정상적으로 잘 출력됨


    <보조 스트림>

     

    보조 스트림이란?

    보조 스트림은 입출력 대상이 되는 파일이나 네트워크에 직접 쓰거나 읽는 기능은 없다.
    말 그대로 보조 기능을 추가하는 스트림이다. 이 보조 기능은 여러 스트림에 적용할 수 있다.

    보조 스트림은 다른 말로 Wrapper 스트림이라고도 한다.
    다른 스트림을 감싸고 있다는 의미.
    스스로는 입출력 기능이 없기 때문에 생성자의 매개변수로 다른 스트림을 받게 되면 자신이 감싸고 있는 스트림이 읽거나 쓰는 기능을 수행할 때 보조 기능을 추가한다. 

    ※ 보조 스트림 처럼 다양한 기능을 제공하는 클래스를 디자인 패턴에서 '데코레이터(decorator)'라고 한다.

    FilterInputStream과 FilterOutputStream

    FilterInputStream과 FilterOutputStream은 보조 스트림의 상위 클래스.
    모든 보조 스트림은 FilterInputStrean이나 FilterOutputStream을 상속받는다. 또한 앞에서 설명했듯이 보조 스트림은 자료 입출력을 직접 할 수 없기 때문에 다른 기반 스트림을 포함한다.

    FilterInputStream과 FilterOutputStream의 생성자는 다음과 같다.

    생성자 설명
    protected FilterInputStream(InputStream in) 생성자의 매개변수로 InputStream을 받는다.
    public FilterOutputStream(OutputStream out) 생성자의 매개변수로 OutputStream을 받는다.

    두 클래스 모두 다른 생성자는 제공하지 않는다. 
    따라서 이들 클래스를 상속받은 보조 클래스도 상위 클래스에 디폴트 생성자가 없으므로 다른 스트림을 매개변수로 받아 상위 클래스를 호출해야 한다.

    FilterInputStream과 FilterOutputStream을 직접 생성하여 사용하는 경우는 거의 없고 이를 상속한 하위 클래스를 프로그램에서 많이 사용한다.

    우리가 보조 스트림을 배울 때 기억할 사항은 보조 스트림(c)의 생성자에 항상 기반 스트림(a)만 매개변수로 전달되는 것은 아니라는 점이다.
    때로는 또 다른 보조 스트림(b)을 매개변수로 전달받을 수도 있다. 이때 전달되는 또 다른 보조 스트림(b)은 내부적으로 기반 스트림(a)을 포함하고 있다.
    이런 경우 다음 그림처럼 하나의 기반 스트림에 여러 보조 스트림 기능이 추가된다.

     

    그럼 주로 사용하는 보조 스트림을 중심으로 살펴보자.

    InputStreamReader와 OutputStreamWriter

    바이트 단위로 자료를 읽으면 한글 같은 문자는 깨진다. 그래서 문자는 Reader나 Writer에서 상속받은 스트림을 사용해 자료를 읽거나 써야한다.
    하지만 바이트 자료만 입력되는 스트림도 있다. 대표적으로 표준 입출력 System.in 스트림이다. 그리고 네트워크에서 소켓이나 인터넷이 연결되었을 때 읽거나 쓰는 스트림은 바이트 단위인 InputStream과 OutStream이다.
    이렇게 생성된 바이트 스트림을 문자로 변환해 주는 보조 스트림이 InputStreamReader와 OutputStreamWriter이다.

    보조 스트림은 입출력 기능이 없으므로 다른 입출력 스트림을 포함한다.

    InputStreamReader의 생성자를 살펴보면 다음과 같다.

    생성자 설명
    InputStreamReader(InputStream in) InputStream 클래스를 생성자의 매개변수로 받아 Reader를 생성한다.
    InputStreamReader(InputStream in, Charset cs) InputStream과 Charset클래스를 매개변수로 받아 Reader를 생성한다.
    InputStreamReader(InputStream in, CharsetDecoder dec) InputStream과 CharsetDecoder를 매개변수로 받아 Reader를 생성한다.
    InputStreamReader(InputStream in, String charsetName) InputStream과 String으로 문자 세트 이름을 받아 Reader를 생성한다.

    InputStreamReader 생성자의 매개변수로 바이트 스트림과 문자 세트를 매개변수로 지정할 수 있다.

    문자 세트란 문자를 표현하는 인코딩 방식이다. 바이트 자료가 문자로 변환될 때 지정된 문자 세트가 적용된다.
    적용할 문자 세트를 명시하지 않으면 시스템이 기본으로 사용하는 문자 세트가 적용된다.

    ※ 문자 세트는 각 문자가 가지고 있는 고유 값이 어떤 값으로 이루어졌는가에 따라 다르다.
    대표적으로 자바에서 사용하는 UTF-16 문자 세트가 있는데 이는 유니코드를 나타내는 문자 세트 중 하나이다.

    InputStreamReader의 모든 생성자는 InputStream, 즉 바이트 단위로 읽어 들이는 스트림을 매개변수로 받는다.
    생성자에서 매개변수로 받은 InputStream이 자료를 읽으면 InputStreamReader가 읽은 바이트 자료를 문자로 변환해 준다.

    그럼 InputStream인 FileInputStream을 사용해 InputStreamReader가 해주는 문자 변환 기능을 보자.

    package stream.decorator;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStreamReader;
    
    public class InputStreamReaderTest {
    	public static void main(String[] args) {
    		try (InputStreamReader isr = new InputStreamReader(new FileInputStream("reader.txt"))) {
    			int i;
    			//파일의 끝인 -1이 반환될 때까지 보조 스트림으로 자료를 읽음.
    			while ((i = isr.read()) != -1) {
    				System.out.print((char)i);
    			}
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    	}
    }

    한글이 잘 나온다.

    InputStreamReader(보조 스트림)이 FileInputStream(기반 스트림)을 매개변수로 받아 생성된다.

    while문에서 파일의 끝 -1이 반환도리 때까지 보조 스트림으로 읽어 들인다.

    FileInputStream은 바이트 단위로 자료를 읽기 때문에 reader.txt에 쓰여있는 한글 '안녕하세요'를 읽을 수 없다.
    InputStreamReader는 파일 스트림이 바이트 단위로 읽어들인 내용을 문자로 변환해 주는 역할을 한다.

    사실 파일에서 문자를 읽는 경우 위와 같이 InputStreamReader로 변환할 필요 없이 FileReader로 바로 읽으면 되지만, 여기에서는 우리가 사용해본 InputStream을 쓰기 위해 FileInputStream을 사용한 것이다.

    표준 입출력 스트림 System.in과 out은 모두 바이트 스트림이다. 특히 System.in은 콘솔 화면에서 한글을 읽으려면 InputStreamReader를 사용해야 한다.
    Scanner클래스는 이런 변환이 필요 없어 콘솔 입력에 많이 쓰인다.

    네트워크에서 쓰이는 클래스는 스트림을 생성하면 InputStream이나 OutputStream으로 생성된다. 예를 들어 채팅 프로그램을 만든다고 할 때 바이트 단위로 사용하면 영어로만 채팅을 해야하기 때문에, 이럴 때 읽어 들인 자료를 InputStreamReader나 OutputStreamWriter를 활용해 문자로 변환하여 사용한다.

    Buffered 스트림

    입출력이 한 바이트나 문자 단위로 이루어지면 그만큼 프로그램 수행 속도가 느려진다.
    Buffered 스트림은 내부적으로 8,192바이트 크기의 배열을 가지고 있으며 이미 생성된 스트림에 배열 기능을 추가해 더 빠르게 입출력을 실행할 수 있는 버퍼링 기능을 제공한다.
    물론 한 바이트나 한 문자 단위로 처리할 때보다 빠르게 처리할 수 있다.

    버퍼링을 기능을 제공하는 스트림 클래스는 다음과 같다.

    스트림 클래스 설명
    BufferedInputStream 바이트 단위로 읽는 스트림에 버퍼링 기능을 제공한다.
    BufferedOutputStream 바이트 단위로 출력하는 스트림에 버퍼링 기능을 제공한다.
    BufferedReader 문자 단위로 읽는 스트림에 버퍼링 기능을 제공한다.
    BufferedWriter 문자 단위로 출력하는 스트림에 버퍼링 기능을 제공한다.


    버퍼링 기능을 제공하는 스트림 역시 보조 스트림으로 다른 스트림을 포함하여 수행된다.
    BufferedInputStream의 생성자를 보면 다음과 같다.

    생성자 설명
    BufferedInputStream(InputStream in) InputStream 클래스를 생성자의 매개변수로 받아 BufferedInputStream을 생성한다.
    BufferedInputStream(InputStream in, int size) InputStream 클래스와 버퍼 크기를 생성자의 매개변수로 받아 BufferedInputStream을 생성한다.

    BufferedInputStream은 보조 스트림으로 생성자의 매개변수로 다른 InputStream을 가져야 한다.
    BufferedOutputStream은 OutputStream을 생성자의 매개변수로 가져야하고,
    BufferReader는 Reader를 BufferWriter는 Writer 클래스를 생성자의 매개변수로 받는다.
    Buffered 스트림이 포함할 스트림이 입력 스트림인지 출력 스트림인지, 문자용인지 바이트용인지에 따라 그에 맞는 스트림을 사용한다.

    그러면 Buffered 스트림을 사용할 때와 그렇지 않은 경우의 수행 시간을 비교해보자.

    FileInputStream과 FileOutputStream을 사용해 파일을 복사할 때 걸리는 시간과,
    BufferedInputStream과 BufferedOutputStream을 사용했을 때 걸리는 시간을 확인해 보자.
    테스트를 위해 용량이 5MB정도 되는 복사할 파일을 구하여 현재 프로젝트 폴더에 넣는다. 그리고 이를 copy라는 이름으로 복사하여 생성한다.

    6메가의 A.EXE파일을 프로젝트에 넣었다.

    package stream.decorator;
    
    import java.io.BufferedInputStream;
    import java.io.BufferedOutputStream;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    public class BufferedStreamTest {
    	public static void main(String[] args) {
    		long milliseconds = 0;
    		try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("A.EXE"));
    				BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("Copy.EXE"))) {
    			milliseconds = System.currentTimeMillis();
    			int i;
    			while ((i = bis.read()) != -1) {
    				bos.write(i);
    			}
    			milliseconds = System.currentTimeMillis() - milliseconds;
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    		System.out.println("Buffered 클래스로 파일을 복사하는 데 " + milliseconds + " milliseconds가 소요되었습니다.");
    	}
    }

    한 바이트씩 읽고 쓰는 데 57417밀리초 소요

    • FileInputStream과 FileOutputStream을 사용해 A.EXE파일에서 한 바이트씩 읽어 Copy.EXE파일에 쓰고있다.
    • 전체를 복사하는 데 걸린 시간은 약 57.4초다.

    FileInputStream은 바이트 단위로 자료를 읽는 스트림이다. 한 바이트를 읽어서 변수 i에 저장하면 그 값을 다시 FileOutStream을 통해 저장한다. 한 바이트씩 읽고 쓰는 과정의 시간이 여러 바이트를 한꺼번에 읽고 쓰는 것 보다 당연히 오래 걸린다.

    이번에는 보조 스트림 BufferedInputStream과 BufferedOutputStream을 사용해 파일을 복사해 보자.

    package stream.decorator;
    
    import java.io.BufferedInputStream;
    import java.io.BufferedOutputStream;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    public class BufferedStreamTest {
    	public static void main(String[] args) {
    		long milliseconds = 0;
    		try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("A.EXE"));
    				BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("Copy.EXE"))) {
    			milliseconds = System.currentTimeMillis();
    			int i;
    			while ((i = bis.read()) != -1) {
    				bos.write(i);
    			}
    			milliseconds = System.currentTimeMillis() - milliseconds;
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    		System.out.println("Buffered 클래스로 파일을 복사하는 데 " + milliseconds + " milliseconds가 소요되었습니다.");
    	}
    }

    Buffered 클래스로 읽고 쓰는 데 116밀리초 소요

    수행 시간이 굉장히 단축된 것을 알 수 있다.

    Buffered 스트림은 멤버 변수로 8,192바이트 배열을 가지고 있다. 즉 한 번 자료를 읽을 때 8KB정보를 한번에 읽고 쓸 수 있으므로 1바이트 씩 읽고 쓸 때보다 훨씬 빠른 수행을 보장한다.
    배열의 크기는 Buffered 스트림 생성자 매개변수로 지정할 수도 있다.

    DataInputStream과 DataOutputStream

    지금까찌 살펴본 스트림은 사람이 읽고 쓰는 텍스트 형식의 자료를 다루었다.
    이제 배울 DataInputStream과 DataOutputStream은 메모리에 저장된 0, 1상태를 그대로 읽거나 쓴다. 그래서 자료형의 크기가 그대로 보존된다.

    두 스트림은 다음과 같은 생성자를 제공한다.

    생성자 설명
    DataInputStream(InputStream in) InputStream을 생성자의 매개변수로 받아 DataInputStream을 생성한다.
    DataOutputStream(OutputStream out)  OutputStream을 생성자의 매개변수로 받아 DataOutputStream을 생성한다.


    'DataInputStream'은 다음과 같이 각 자료형별 메서드를 제공하여 자료형에 따라 읽거나 쓸 때 사용할 수 있다.

    메서드 설명
    byte readByte() 1바이트를 읽어 반환한다.
    boolean readBoolean() 읽은 자료가 0이 아니면 true, 0이면 false를 반환한다.
    char readChar() 한 문자를 읽어 반환한다.
    short readShort() 2바이트를 읽어 정수 값을 반환한다.
    int readInt() 4바이트를 읽어 정수 값을 반환한다.
    long readLong() 8바이트를 읽어 정수 값을 반환한다.
    float readFloat() 4바이트를 읽어 실수 값을 반환한다.
    double readDouble() 8바이트를 읽어 실수 값을 반환한다.
    String readUTF() 수정된 UTF-8 인코딩 기반으로 문자열을 읽어 반환한다.


    'DataOutputStream'은 각 자료형별로 read()에 대응되는 write()메서드를 제공한다.

    메서드 설명
    void writeByte(int v) 1바이트의 자료를 출력한다.
    void writeBoolean(boolean v) 1바이트 값을 출력한다.
    void writeChar(int v) 2바이트를 출력한다.
    void writeShort(int v) 2바이트를 출력한다.
    void writeInt(int v) 4바이트를 출력한다.
    void writeLong(long v) 8바이트를 출력한다.
    void writeFloat(float v) 4바이트를 출력한다.
    void writeDouble(double v) 8바이트를 출력한다.
    void writeUTF(String str) 수정된 UTF-8 인코딩 기반으로 문자열을 출력한다.

    자료형을 그대로 읽고 쓰는 스트림이기 때문에 같은 정수라도 자료형에 따라 다르게 처리한다.
    즉 writeByte(100)은 1바이트로 쓰인 100을 의미하지만, writeInt(100)은 4바이트로 쓰인 100을 의미한다.
    따라서 자료를 쓸 때 사용한 메서드와 같은 자료형의 메서드로 읽어야한다.

    즉 정수 100을 쓰는 데 writeInt(100)을 쓰고 readByte()로 읽으면 서로 사용한 메모리 크기가 달라 같은 값을 가져올 수 없다.
    또 파일이든 네트워크든 자료를 쓸 때 사용한 메서드 순서대로 읽어야 한다.

    파일에 여러 자료형 값을 저장하는 예제.
    자료형을 유지하여 저장하기 위해 DataInputStream과 DataOutputStream을 보조 스트림으로 사용한다.

    package stream.decorator;
    
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    
    public class DataStreamTest {
    	public static void main(String[] args) {
    		try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"))) {
    			//각 자료형에 맞게 자료를 씀.
    			dos.writeByte(100);
    			dos.writeChar('A');
    			dos.writeInt(10);
    			dos.writeFloat(3.14f);
    			dos.writeUTF("Test");
    		} catch(IOException e) {
    			System.out.println(e);
    		}
    		try (DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"))) {
    			//자료형에 맞게 자료를 읽어 출력, 파일에 쓴 순서와 같은 순서, 같은 메서드로 읽어야 함
    			System.out.println(dis.readByte());
    			System.out.println(dis.readChar());
    			System.out.println(dis.readInt());
    			System.out.println(dis.readFloat());
    			System.out.println(dis.readUTF());
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    	}
    }

    • 파일 스트림을 만들고 여기에 DataInputStream과 DataOutputStream 기능을 추가했다.
    • 기반 스트림에서 쓸 수 없던 각 자료형의 자료를 그대로 읽고 쓸 수 있다.
    • 파일에 쓴 것과 동일한 순서, 동일한 메서드로 읽어야 한다.

    <직렬화>

     

    직렬화와 역직렬화

    클래스의 인스턴스가 생성되면 인스턴스의 상태, 즉 인스턴스 변수 값은 마치 생명체처럼 계속 변화한다.

    그런데 인스턴스의 어느 순간 상태를 그대로 저장하거나 네트워크를 통해 전송할 일이 있을 수 있다.
    이를 '직렬화(serialization)'라고 한다.

    그리고 저장된 내용이나 전송받은 내용을 다시 복원하는 것을 '역직렬화(deserialization)'라고 한다.

    다시 말해 직렬화란 인스턴스 내용을 연속 스트림으로 만드는 것이다. 스트림으로 만들어야 파일에 쓸 수도 있고 네트워크로 전송할 수도 있다.
    따라서 직렬과 과정에서 하는 일은 인스턴스 변수 값을 스트림으로 만드는 것.

    복잡한 과정일 수 있지만 자바에서는 보조 스트림인 ObjectInputStream과 ObjectOutputStream을 사용해 좀 더 쉽게 구현할 수 있따.

    생성자를 보면 다음과 같다.

    생성자 설명
    ObjectInputStream(InputStream in) InputStream을 생성자의 매개변수로 받아 ObjectInputStream을 생성한다.
    ObjectOutputStream(OutputStream out) OutputStream을 생성자의 매개변수로 받아 ObjectOutputStream을 생성한다.

    저장할 파일이나 전송할 네트워크 등의 기반 스트림을 매개변수로 받아서 인스턴스 변수 값을 저장하거나 전송한다.

    그러면 직렬화에 사용할 Person 클래스를 하나 만들어 인스턴스로 생성한 후 파일에 썼다가 복원하는 예제를 살펴보자.

    • Person 클래스의 생성자로 두 인스턴스를 생성한다.
    • 이를 serial.out 파일에 저장한다(직렬화).
    • 그러고 나서 serial.out파일에서 저장된 내용을 일거 원래 인스턴스 상태로 복원한다(역직렬화).
    package stream.serialization;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    
    class Person {
    	
    	String name;
    	String job;
    	
    	public Person() {}
    	
    	public Person(String name, String job) {
    		this.name = name;
    		this.job = job;
    	}
    	
    	public String toString() {
    		return name + ", " + job;
    	}
    }
    
    public class SerializationTest {
    	public static void main(String[] args) throws ClassNotFoundException {
    		Person personAhn = new Person("안재용", "대표이사");
    		Person personKim = new Person("김철수", "상무이사");
    		
    		try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serial.out"))) {
    			//personAhn과 Kim의 값을 파일에 씀(직렬화).
    			oos.writeObject(personAhn);
    			oos.writeObject(personKim);
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    		try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serial.out"))){
    			//personAhn과 Kim의 값을 파일에서 읽어 들임(역직렬화).
    			Person p1 = (Person)ois.readObject();
    			Person p2 = (Person)ois.readObject();
    			System.out.println(p1);
    			System.out.println(p2);
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    	}
    }
    • 먼저 직렬화에 사용할 Person 클래스를 하나 만들었다. 이 클래스는 이름과 직업을 생성자의 매개변수로 받아 생성한다.
    • writeObject() 메서드를 호출하면 personAhn과 personKim 값이 파일에 쓰이는데 이때 serial.out 파일을 열어 보면 우리가 읽을 수 없는 내용으로 저장이 되어있다.

    writeObject()를 호출하여 쓰인 serial.out파일의 내용

    • 다시 원래 상태로 복원할 때는 readObject()를 사용해 저장된 순서로 읽어들이는데 이때 readObject() 메서드의 반환 값이 Object이므로 원래 자료형인 Person으로 형 변환을 한다.
    • 역직렬화를 할 때 클래스 정보가 존재하지 않을 수 있으므로 ClassNotFoundException도 처리해야 한다.
      (여기서는 main()함수의 throws 부분에 추가)

    Serializable 인터페이스

    위 프로그램을 실행하면 출력 화면처럼 오류가 난다.

    오류가 발생한다.

    직렬화는 인스턴스 내용이 외부로 유출되는 것이므로 프로그래머가 직렬화를 하겠다는 의도를 표시해야한다.
    따라서 Person 클래스마커 인터페이스(marker interface)Serializable 인터페이스를 다음과 같이 추가한다.

    class Person implements Serializable { //직렬화하겠다는 의도 표시
    	...
        String name;
        String job;
        ...
    }

    Serializable 인터페이스는 이 클래스를 직렬화하겠다는 의미로만 해석하면 된다.
    그리고 다시 프로그램을 실행하면 출력화면이 다음과 같다.

    처음 생성한 클래스 내용이 읽히고 복원되었다.

    transient 예약어

    직렬화 대상이 되는 클래스는 모든 인스턴스 변수가 직렬화되고 복원된다.

    그런데 직렬화가 될 수 없는 클래스(Socket 클래스는 직렬화될 수 없음)가 인스턴스 변수로 있다거나 직렬화하고 싶지 않은 변수가 있을 수 있다. 그럴 때 transient 예약어를 사용한다.
    그러면 해당 변수는 직렬화 되고, 복원되는 과정에서 제외시킨다.

    transient 예약어를 사용한 변수 정보는 그 자료형의 기본 값으로 저장된다. 따라서 객체 자료형인 경우에 null 값이 된다.

    String name;
    transient String job;

    앞의 예제에서 Person의 job변수를 오른쪽과 같이 바꾸고 실행해 출력 결과를 보면 다음과 같다.

    trasient job의 내용은 저장되지 않음.

    job 내용이 저장되지 않았음을 알 수 있다.

    SerialVersionUID를 사용한 버전 관리

    객체를 역직렬화할 때, 직렬화할 때의 클래스와 상태가 다르면 오류가 발생한다. 그 사이 클래스가 수정되었다거나 변경되었다면 역직렬화할 수 없기 때문이다.
    따라서 직렬화할 때 자동으로 serialVersionUID를 생성해 정보를 저장한다. 그리고 역직렬화를 할 때 serialVersionUID를 비교하는데 막약 클래스 내용이 변경되었다면 클래스 버전이 맞지 않는다는 오류가 발생한다.

    그런데 작은 변경에도 클래스 버전이 계속 바뀐다면, 네트워크로 서로 객체를 공유해서 일하는 경우에는 클래스를 새로 배포해야 하는 번거로움이 있다.

    이런 경우에 클래스의 버전 관리를 개발자가 할 수 있다.
    자바에서 제공하는 자바 설치 경로의 bin\serialver.exe를 사용하면 다음과 같이 serialVersionUID가 생성되는데, 이 정보를 클래스 파일에 적어주면 된다.

    이클립스에서는 이 기능을 자동으로 제공한다.

    cmd를 열어서 막 복잡하게 버전 정보를 복사할 필요 없이 이클립스는 자동으로 제공한다. 

    이 중 두 번째 Add generated serial version ID를 선택하면 다음과 같이 직렬화를 위한 버전 번호가 자동으로 생성된다.

    버전 정보가 생성됨.

    만약 직렬화의 대상이 되는 클래스 정보가 바뀌고 이를 공유해야 하는 경우에 버전 정보를 변경하면 된다.

    Externalizable 인터페이스

    직렬화하는 데 사용하는 또 다른 인터페이스는 Externalizable이다.
    Serializable 인터페이스는 자료를 읽고 쓰는 데 필요한 부분을 프로그래머가 따로 구현하지 않는다. 하지만 Externalizable 인터페이스는 프로그래머가 구현해야 할 메서드가 있다.

    객체의 직렬화와 역직렬화를 프로그래머가 직접 세밀하게 제어하고자 할 때, 메서드에 그 내용을 구현한다.

    name속성을 가진 Dog 클래스에 Externalizable을 구현하면 다음과 같다.

    package stream.serialization;
    
    import java.io.Externalizable;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInput;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutput;
    import java.io.ObjectOutputStream;
    
    class Dog implements Externalizable {
    	String name;
    	
    	public Dog() {}
    	
    	//Externalizable 인터페이스의 메서드 구현
    	@Override
    	public void writeExternal(ObjectOutput out) throws IOException {
    		out.writeUTF(name);
    	}
    	@Override
    	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    		name = in.readUTF();
    	}
    	
    	public String toString() {
    		return name;
    	}
    }
    
    public class ExternalizableTest {
    	public static void main(String[] args) throws IOException, ClassNotFoundException {
    		Dog myDog = new Dog();
    		myDog.name = "멍멍이";
    		
    		try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("external.out"))) {
    			oos.writeObject(myDog);
    		} catch (IOException e) {
    			System.out.println(e);
    		}
    		
    		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("external.out"));
    		
    		Dog dog = (Dog)ois.readObject();
    		System.out.println(dog);
    	}
    }

    • Externalizable 인터페이스를 구현하면 writeExternal()메서드와 readExternal()메서드를 구현해야 한다.
    • 또한 복원할 때 디폴트 생성자가 호출되므로, 디폴트 생성자를 추가해줘야 한다.
    • 읽고 쓰는 내용은 프로그래머가 직접 구현한다.

    <그 외 입출력 클래스>

     

    File 클래스

    File 클래스는 말 그대로 파일이라는 개념을 추상화한 클래스다.
    파일에 대한 입출력은 스트림을 사용하여 수행한다. 따라서 File 클래스에 별도의 입출력 기능은 없지만 파일 자체의 경로나 정보를 알 수 있고 파일을 생성할 수도 있다.

    File클래스의 주요 생성자는 다음과 같다.

    생성자 설명
    File(String pathname) pathname을 매개변수로 받아 파일을 생성한다.


    다음 예제를 통해 FIle 클래스에서 제공하는 여러 메서드 기능을 살펴보자.

    package stream.others;
    
    import java.io.File;
    import java.io.IOException;
    
    public class FileTest {
    	public static void main(String[] args) throws IOException {
    		//해당 경로에 File 클래스 생성. 아직 파일이 생성된 것이 아님.
    		File file = new File("C:\\Users\\My Pc\\Desktop\\newFile.txt");
    		
    		// 실제 파일 생성
    		file.createNewFile();
    		
    		//파일 속성을 살펴보는 메서드를 호출하여 출력
    		System.out.println(file.isFile());
    		System.out.println(file.isDirectory());
    		System.out.println(file.getName());
    		System.out.println(file.getAbsolutePath());
    		System.out.println(file.getPath());
    		System.out.println(file.canRead());
    		System.out.println(file.canWrite());
    		
    		file.delete(); //파일 삭제
    	}
    }

    • File 클래스를 생성했다고 실제 파일시 생성되는 것은 아니다.
      createNewFile()메서드를 활용해 파일을 생성한다.
    • File클래스가 제공하는 메서드로 생성한 파일의 속성을 살필 수 있다.
    • 마지막에 delete()메서드를 사용해 파일을 삭제한다.

    이렇게 생성한 파일은 FileInputStream과 같은 파일 입출력 기능을 제공하는 클래스의 생성자 매개변수로 사용할 수 있다.

    RandomAccessFile 클래스

    RandomAccessFile은 입출력 클래스 중 유일하게 파일 입출력을 동시에 할 수 있는 클래스다.
    또한 지금까지 공부한 스트림은 처음부터 차례로 자료를 읽었지만 RandomAccessFile은 임의의 위치로 이동하여 자료를 읽을 수 있다.
    RandomAccessFile에는 파일 포인터가 있는데, 현재 이 파일의 어느 위치에서 읽고 쓰는지 그 위치를 가리키는 속성이다.
    (LP판을 읽는 턴테이블의 바늘이라고 생각하면 된다)

    스트림을 생성하지 않고 간단하게 파일에 자료를 쓰거나 읽을 때 사용하면 유용하다.

    파일 포인터가 이동하는 위치가 파일 자료를 읽거나 쓰는 위치이므로 포인터의 위치를 잘 생각하며 구현해야 한다.

     RandomAccessFile의 생성자는 다음과 같다.

    생성자 설명
    RandomAccessFile(File file, String mode) 입출력 할 File과 입출력 mode를 매개변수로 받는다.
    mode에는
    읽기 전용 "r", 읽고 쓰기 기능 "rw"를 사용할 수 있다.
    RandomAccessFIle(String file, String mode) 입출력을 할 파일 이름을 문자열로 받고, 입출력 mode를 매개변수로 받는다.
    mode에는
    읽기 전용 "r", 읽고 쓰기 기능 "rw"를 사용할 수 있다.

    RandomAccessFile은 임의의 위치에 읽거나 쓰는 기능 외에도 다양한 자료형 값을 읽거나 쓸 수 있다.
    이 원리를 이해하기 위해 RandomAccessFile이 어떤 인터페이스를 구현했는지 JavaDoc에서 보자.

    RandomAccessFile 클래스가 구현한 인터페이스들

    구현한 인터페이스를 살펴보면 DataInput, DataOutput 인터페이스가 있다.
    이런 인터페이스를 구현하면 RandomAccessFile 클래스는 DataInputStream 및 DataOutputStream과 같이 다양한 자료형을 다루는 메서드를 사용할 수 있다.

    RandomAccessFile을 활용해 여러 자료형 값을 읽고 쓰는 예제를 살펴보자.

    package stream.others;
    
    import java.io.IOException;
    import java.io.RandomAccessFile;
    
    public class RandomAccessFileTest {
    	public static void main(String[] args) throws IOException {
    		RandomAccessFile rf = new RandomAccessFile("random", "rw");
    		rf.writeInt(100);
    		System.out.println("파일 포인터의 위치 : " + rf.getFilePointer());
    		
    		rf.writeDouble(3.14);
    		System.out.println("파일 포인터의 위치 : " + rf.getFilePointer());
    		
    		rf.writeUTF("안녕하세요");
    		System.out.println("파일 포인터의 위치 : " + rf.getFilePointer());
    
    		int i = rf.readInt();
    		double d = rf.readDouble();
    		String str = rf.readUTF();
    		
    		System.out.println("파일 포인터의 위치 : " + rf.getFilePointer());
    		System.out.println(i);
    		System.out.println(d);
    		System.out.println(str);
    	}
    }

    파일 포인터의 위치가 맨 처음으로 옮겨지지 않아 오류 발생

    예제를 설명하기 전에 먼저 파일 포인터에 대해 알아보자.

    파일에 자료를 읽거나 쓰면 파일 포인터가 이동한다. 처음 RandomAccessFile 클래스를 생성하면 파일 포인터의 위치는 맨 앞, 즉 0의 위치를 가리킨다.
    int 크기는 4바이트이므로 파일 포인터 위치는 4로 이동한다. 다시말해 다음 자료를 읽거나 써야 할 위치로 계속 이동하는 것이 파일 포인터.

    RandomAccessFile을 생성할 때 rw모드를 사용했다. 따라서 이 파일은 읽고 쓰기가 모두 가능하다.

    RandomAccessFile의 다양한 자료형 메서드로 파일에 자료를 출력했다. 그리고 각 각 출력 후의 포인터 위치를 확인했다.

    각 메서드가 호출됨으로써 이동한 파일 포인터 값은 다음과 같다.

    rf.writeInt(100); // int 4바이트
    rf.writeDouble(3.14); //double 8바이트
    
    //수정된 UTF-8, 한글(3바이트) * 5 + null문자(2바이트) = 17바이트
    rf.writeUTF("안녕하세요");

    쓰기가 끝난 후 파일 포인터는 4+8+17='29'
    그런데 여기에서 read() 메서드를 바로 호출하면 오류가 발생한다.
    파일 포인터 위치가 29에 있기 때문이다.

    우리가 읽어야 할 위치는 맨 처음인 0부터이다.
    따라서 파일 포인터의 위치를 이동해주는 seek() 메서드를 활용해 맨 처음으로 이동시켜야 한다.

    다음처럼 파일을 읽기 전에 파일 포인터를 이동하는 코드를 추가한다.

    package stream.others;
    
    import java.io.IOException;
    import java.io.RandomAccessFile;
    
    public class RandomAccessFileTest {
    	public static void main(String[] args) throws IOException {
    		RandomAccessFile rf = new RandomAccessFile("random", "rw");
    		rf.writeInt(100);
    		System.out.println("파일 포인터의 위치 : " + rf.getFilePointer());
    		
    		rf.writeDouble(3.14);
    		System.out.println("파일 포인터의 위치 : " + rf.getFilePointer());
    		
    		rf.writeUTF("안녕하세요");
    		System.out.println("파일 포인터의 위치 : " + rf.getFilePointer());
    
    		rf.seek(0);
    		//파일 포인터를 0으로 옮기고 위치를 출력.
    		System.out.println("파일 포인터의 위치 : " + rf.getFilePointer());
    		
    		int i = rf.readInt();
    		double d = rf.readDouble();
    		String str = rf.readUTF();
    		
    		//읽기를 마치고 파일 포인터의 위치 출력
    		System.out.println("파일 포인터의 위치 : " + rf.getFilePointer());
    		System.out.println(i);
    		System.out.println(d);
    		System.out.println(str);
    	}
    }

    • 이제 자료를 읽기 전에 포인터를 맨 처음으로 이동시켜 처음부터 차례로 값을 읽어 올 수 있다.
    • 자료를 읽어 올 때는 저장할 때 사용한 자료형에 대응하는 메서드를 사용해 읽어야 한다.

     

    반응형