i/O 스트림

gustjtmd·2022년 1월 19일
0

Java

목록 보기
38/40

I/O 스트림에 대한 이해

그냥 '스트림'과 'I/O 스트림'의 차이는?

스트림의 주제 : 데이터를 어떻게 원하는 형태로 걸러내고 가공할 것인가?
컬렉션 인스턴스에 저장된 문자열 중 길이가 5 이상인 문자열만 출력
-> '스트림'으로 해결해야 할 부분.
---------------------------------------------------------------
I/O 스트림의 주제 : 어떻게 데이터를 입력하고 출력할 것인가?
파일에 저장된 문자열을 꺼내어 컬렉션 인스턴스에 저장
-> 'I/O 스트림'으로 해결해야 할 부분.

I/O 스트림 모델의 소개

프로그램 상당 부분은 입출력과 관련이 있다. 우리가 쉽게 접할수 있는 입출력의 대상

1. 파일
2. 키보드와 모니터
3. 그래픽카드, 사운드카드
4. 프린터,팩스와 같은 출력장치
5. 인터넷으로 연결되어 있는 서버 또는 클라이언트

이렇듯 데이터의 입출력 대상은 다양한데 입출력 대상이 달라지면 코드상에서 입출력 방법도
달라지는것이 일반적이나 자바에서는 입출력 대상에 상관없이 동일한 방법으로 입출력을 진행할수
있도록 'I/O 스트림 모델'이라는 것을 정의하였다.

I/O 모델과 스트림의 이해, 그리고 파일 대상의 입력 스트림 생성

I/O 스트림은 크게 두가지로 나뉜다.

입력 스트림(Input Stream)
-> 실행 중인 자바 프로그램으로 데이터를 읽어 드리는 스트림

출력 스트림(Output Stream)
-> 실행 중인 자바 프로그램으로부터 데이터를 내보내는 스트림

----------------------------------------------------------------------

public class Write7ToFile {
    public static void main(String[] args) throws IOException {
        OutputStream out = new FileOutputStream("data.dat");    //출력 스트림 생성
        out.write(7);   //7을 저장
        out.close();
    }
}

이 문장이 실행되면 data.dat라는 파일이 생성되고 이 파일에 데이터를 저장할 수 있는 출력
스트림이 생성된다
OutputStream out = new FileOutputStream("data.dat");

만약에 스트림을 생성할 대상이 파일이 아니라면 위 문장의 오른편에서 생성하는 인스턴스의 종류가
그에 따라 달라진다.
	out.write(7);   //7을 저장
    
끝으로 데이터의 저장이 끝나면 다음 메소드 호출을 통해서 생성했던 출력 스트림을 소멸해야 한다.
스트림이 소멸되면 열려있던 파일은 닫히고 할당 되었던 메모리 자원은 다시 사용할수 읻소록
반환이 된다.
        out.close();
        


----------------------------------------------------------------------

public class Ream7FromFile {
    public static void main(String[] args) throws IOException {
        InputStream in = new FileInputStream("data.dat");   //입력 스트림 생성
        int dat = in.read();    //데이터 읽음
        in.close(); //입력 스트림 종료

        System.out.println(dat);
    }
}


7

다음과 같이 입력 스트림을 생성하였다.
InputStream in = new FileInputStream("data.dat");

출력 스트림과 마찬가지로 스트림 생성의 대상이 달라지면 위 문장에서 생성하는 인스턴스의 종류만
달라진다. 그리고 데이터를 읽어 들이는 방법은 대상에 상관없이 다음과 같다.
int dat = in.read();    //데이터 읽음

데이터를 읽어 들이고 나면 다음과 같이 입력 스트림을 종료해서 할당된 자원을 반환해야 한다.
        in.close();

입출력 스트림 관련 코드의 개선

앞서 보인 두 예제의 경우 입력 및 출력 스트림의 생성 과정에서 예외가 발생하여 스트림의 생성에
실패할수도 있다. 이러한 경우에는 스트림을 종료하는 close 메소드의 호출을 생략해야 하는데
그렇지 않으면 이로 인해 또 다른 예외가 발생할수 있다. 다음과 같이 안정적으로 작성해보자.

public class Write7ToFile2 {
    public static void main(String[] args) throws IOException {

        OutputStream out = null;

        try{
            out = new FileOutputStream("data.dat");
            out.write(7);
        }
        finally {
            if(out != null) //출력 스트림 생성에 성공했다면,
                out.close();
        }
    }
}

----------------------------------------------------------------------
public class Read7FromFile2 {
    public static void main(String[] args) throws IOException {
        InputStream in = null;

        try{
            in = new FileInputStream("data.dat");
            int dat = in.read();
            System.out.println(dat);
        }finally {
            if(in != null)  //입력 스트림 생성에 성공했다면,
                in.close();
        }
    }
}


이전 예제와 내용상으로는 차이가 없다. 다만 안정적인 close 메소드의 호출을 보장하기 위한
코드가 추가되었을 뿐이다. 
try-with-resources문을 적용하여 더 간결한 구조를 만들어보자.

public class Write7ToFile3 {
    public static void main(String[] args) {
        try(OutputStream out = new FileOutputStream("data.dat")){
            out.write(7);
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

------------------------------------------------------------------------

public class Read7FromFile3 {
    public static void main(String[] args) {
        try(InputStream in = new FileInputStream("data.dat")){
            int dat = in.read();
            System.out.println(dat);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

바이트 단위 입출 및 출력 스트림

가장 기본적인 데이터의 입출력 단위는 바이트이고, 바이트 단위로 데이터를 입력 및 출력하는
스트림을 가리켜 '바이트 스트림'이라 한다. 그리고 파일을 대상으로 하는 바이트 스트림의
생성 방법은 다음과 같다.

InputStream in = new FIleInputStream("data.dat")
-> 파일을 대상으로 하는 바이트 단위의 입력 스트림 생성
OutPutStream out = new FileOutputStream("data.dat")
-> 파일을 대상으로 하는 바이트 단위의 출력 스트림 생성


바이트 스트림을 생성해서 파일을 복사하는 코드의 예

public class BytesFileCopier {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("대상 파일 : ");
        String src = sc.nextLine();

        System.out.print("사본 이름 : ");
        String dst = sc.nextLine();

        try(InputStream in = new FileInputStream(src);
            OutputStream out = new FileOutputStream(dst)){
            int data;
            while(true){
                data = in.read();   //파일로부터 1바이트를 읽는다.
                if(data == -1)  // 더 이상 읽어 들일 데이터가 없다면,
                    break;  //끝내기
                out.write(data);    //파일에 1바이트를 쓴다
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}


대상 파일 : BytesFileCopier.java
사본 이름 : BytesFileCopier2.java

파일을 복사하기 위해서는 입력 스트림과 출력 스트림을 함께 생성해야 한다. 따라서 위의 코드는
하나의 try-with-resources문을 통해서 두개의 스트림을 생성하였다.

while(true){
                data = in.read();   //파일로부터 1바이트를 읽는다.
                if(data == -1)  // 더 이상 읽어 들일 데이터가 없다면,
                    break;  //끝내기
                out.write(data);    //파일에 1바이트를 쓴다
            }while문에서 호출하는 read 메소드는 다음과 같다.
public int read() throws IOException

이 메소드는 파일로부터 읽어 들인 1바이트의 유효한 데이터에 3바이트의 0을 채워서 4바이트
int형 데이터로 반환한다. 반면 스트림의 끝에 도달해서 더 이상 읽어들일 데이터가 없는 경우
-1을 반환한다.
그리고 while문에서 호출하는 write 메소드도 read 메소드와 유사하게 인자로 전달되는 int형
데이터의 첫번째 바이트만을 파일에 저장한다.

즉 위의 코드는 파일의 크기에 상관없이 1바이트씩 읽고 쓰는 복사 프로그램이다 따라서
크기가 어느정도 되는 파일을 복사할 경우 제법 오랜 시간이 걸린다.

보다 빠른 속도의 파일 복사 프로그램

바이트 스트림이라 하여 1바이트씩만 읽고 써야 하는 것은 아니다.

public class BufferedFileCopier {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("대상 파일 : ");
        String src = sc.nextLine();

        System.out.print("사본 이름 : ");
        String dst = sc.nextLine();

        try(InputStream in = new FileInputStream(src);
            OutputStream out = new FileOutputStream(dst)){
            byte buf[] = new byte[1024];
            int len;

            while(true) {
                len = in.read(buf);  //buf 바이트를 읽어들인다.
                if(len == -1)
                    break;
                out.write(buf,0,len);   //len 바이트만큼 데이터를 저장한다.
            }
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

위 코드에서 호출한 read write메소드는 각각 다음과 같다 역시 끝에 도달하면 -1 반환

public int read(byte[] b) throws IOException
-> 파일에 저장된 데이터를 b로 전달된 배열에 저장

public void write(byte[], int off, int len) throws IOException
-> b로 전달된 배열의 데이터를 인덱스 off에서 부터 len 바이트만큼 파일에 저장

그리고 위의 메소드 호출에 사용된 배열은 다음과 같다
byte buf[] = new byte[1024];

따라서 1킬로바이트 단위로 데이터를 읽고 쓰게되어 그만큼 속도가 빨라지게 된다.

필터 스트림의 이해와 활용

바이트 단위로 데이터를 읽고 쓸줄은 알지만.

우리는 다음 요구 사항을 만족하는 프로그램을 작성할수 있다,
"파일로부터 4바이트의 데이터를 읽어 들인다."

그러나 다음 요구 사항을 만족하는 프로그램은 아직 작성할줄 모른다
"파일로부터 int형 데이터 하나를 읽어 들인다."

내용만 놓고 보면 둘 다 4바이트의 데이터를 읽어 들이는 형태이지만 결과는 다르다.
4바이트의 데이터를 읽어 들이는 방법은 다음과 같다.

InputStream in = new FileInputStream("data.dat");
byte buf[] = new byte[4];	//4바이트의 공간을 마련하여
in.read(buf);	//4바이트를 읽어들인다.

이는 1바이트 데이터 4개를 읽어들이는 형태이다(비록 한번의 read 메소드 호출로 4바이트를
읽어 들이더라도) 그리고 이렇게 읽어들인 결과는 코드상에서 int형 데이터로 활용하지 못한다.
int형 데이터 하나를 읽어 들이려면 다음의 단계를 거쳐야한다.

단계 1. 파일로부터 1바이트 데이터 4개를 읽어들인다.
단계 2. 읽어 들인 1바이트 데이터 4개를 하나의 int형 데이터로 조합한다.

이 중에서 두번째 단계의 일을 하는 스트림을 가리켜 '필터 스트림'이라 한다.
이러한 필터 스트림은 입력 또는 출력 스트림에 덧붙여서 데이터를 조합, 가공 및 분리하는 역할.

'필터 입력 스트림' : 입력 스트림에 연결되는 필터 스트림
'필터 출력 스트림' : 출력 스트림에 연결되는 필터 스트림

예를 들어 파일로부터 int370 하나를 읽어 들이기 위해서는 다음 그림으 ㅣ형태로
스트림을 구성해야 한다. 이와같이 구성하면 파일로부터 4개의 1바이트 데이터를
읽어서 필터 입력스트림에 전달한다. 그러면 필터 입력 스트림은 이를 하나의 int형 데이터로
조합해서 반환한다.

엮으로 파일에 int형 데이터 370 하나를 저장하는 경우도 마찬가지이다.
필터 출력 스트림에 int 정수 370을 전달한다. 그러면 필터 출력 스트림은
이를 4개의 1바이트 데이터로 구분해서 출력 스트림에 전달한다.

OutputStream out = new FileOutputStream("data.dat); //출력 스트림 생성
DataOutputStream fOut = new DataOutputStream(out);//필터 스트림 생성 및 연결

필터 스트림의 종류에는 여러 가지가 있다. 일단 기본 자료형 데이터의 입출력에 필요한
다음 두 필터 스트림을 대상으로 필터 스트림에 대해 얘기하겠다.

DataInputStream	'기본 자료형 데이터의 입력을 위한 필터 스트림'
DataOutputStream '기본 자료형 데이터의 출력을 위한 필터 스트림'

기본 자료형 데이터를 파일에 저장하는 코드.
---------------------------------------------------------------------

public class DataFilterOutputStream {
    public static void main(String[] args) {
        try(DataOutputStream out =
                    new DataOutputStream(new FileOutputStream("data.dat"))){
            out.writeInt(370);  //int형 데이터 저장
            out.writeDouble(3.14); //double형 데이터 저장
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

출력하는 코드

public class DataFilterInputStream {
    public static void main(String[] args) {
        try(DataInputStream in = new
                DataInputStream(new FileInputStream("data.dat"))){
            int num1 = in.readInt();    //int형 데이터 꺼냄
            double num2 = in.readDouble();  //double형 데이터 꺼냄

            System.out.println(num1);
            System.out.println(num2);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

370
3.14

--------------------------------------------------------------------------

위 코드에서 다음과 같이 하나의 문장에서 파일 출력 스트림을 생성하고 여기에 필터 스트림을
연결하였다.

DataOutputStream out =
               new DataOutputStream(new FileOutputStream("data.dat"))
               
저장 코드를 실행하면 data.dat에 값이 저장된다.

그후 출력하는 코드를 통해 값을 출력하였는데
이떄 호출한 메소드는 다음과 같다

public final int readInt() throws IOException
public final double readDouble() throws IOException

이렇듯 자료형을 결정해서 데이터를 입출력하는 경우 순서를 지키는것이 중요하다.

버퍼링 기능을 제공하는 필터 스트림

필터 스트림 중에서 상대적으로 사용 빈도수가 높은 두 필터 스트림
BufferedInputStream - 버퍼링 기능을 제공하는 버퍼 입력 스트림
BufferedOutputStream - 버퍼링 기능을 제공하는 버퍼 출력 스트림

다음 코드에서도 1바이트 단위로 데이터를 읽고 쓴다. 그러나 이번에는
입출력 스트림에 버퍼링 기능을 제공하는 필터 스트림을 연결하였다.

public class BufferedStreamFileCopier {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("대상 파일 : ");
        String src = sc.nextLine();

        System.out.print("사본 이름 : ");
        String dst = sc.nextLine();

        try(BufferedInputStream in =
                new BufferedInputStream(new FileInputStream(src));
            BufferedOutputStream out =
                    new BufferedOutputStream(new FileOutputStream(dst))){
            int data;
            while(true){
                data = in.read();
                if(data == -1)
                    break;
                out.write(data);
            }
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}


대상 이름 : BufferedStreamFileCopier.java
사본 이름 : BufferedStreamFileCopier2.java

---------------------------------------------------------------------------

위 코드에서 다음과 같이 입출력 스트림을 생성하고 이를 버퍼스트림에 연결하였다.

BufferedInputStream in =
                new BufferedInputStream(new FileInputStream(src));
            
BufferedOutputStream out =
                    new BufferedOutputStream(new FileOutputStream(dst))
                    
버퍼 스트림을 연결하면 어떠한 일이 일어나는 것일까?

위 그림에서 보이듯이 버퍼 입력 스트림은 내부에 '버퍼(메모리 공간)'을 갖는다.
그리고 입력 스트림으로부터 많은 양의 데이터를 가져가 해당 버퍼를 채운다
때문에 프로그래머가 read 메소드를 호출할때 파일에 저장된 데이터를 반환하는 것이 아니라
버퍼 스트림의 버퍼에 저장된 데이터를 반환된다. 이것이 성능 향상의 핵심

파일에서 데이터를 읽을 때만 속도가 저하되는 것은 아니다. 쓸때도 마찬가지로 속도가 저하된다.
따라서 버퍼 출력 스트림 내부에 데이터를 저장해 두었따가 한 번에 파일에 쓰면 그만큼 속도
저하를 줄일수 있다.

버퍼링 기능에 대한 대책 flush 메소드의 호출

기본적으로 버퍼링은 성능 향상에 도움을 주지만 다음과 같은 상황이 발생할수 있어서 주의가 필요

"버퍼 스트림에 저장된 데이터가 파일에 저장되지 않은 상태에서 컴퓨터가 다운"

이때 프로그램상 write 메소드 호출을 통해 데이터를 저장했음에도 불구하고 실제 파일에는
저장되지 않는 일이 발생할 수 있다. 때문에 버퍼가 차지 않아도 파일에 저장해야할 중요한 
데이터가 있다면 
public void flust() throws IOException
의 호출을 통해서 버퍼를 비우라고(파일을 데이터로 보내라고)명령할수 있다.

그러나 이 메소드를 빈번히 호출하는 것은 버퍼링을 통한 성능 향상에 방해가 되는 일이기
때문에 제한적으로 호출하는것이 좋다.

파일에 기본 자료형 데디어틀 렂장하고 버퍼링 기능도 추가하기

파일을 대상으로 버퍼링 기능을 갖는 스트림을 생성하여 기본 자료형 데이터를 저장하려면
어떻게 스트림을 생성해야 할까? 
'버퍼링 기능의 필터 스트림''기본 자료형 데이터의 저장을 위한 필터 스트림'이
동시에 필요하다. 따라서 다음과 같이 스트림을 생성해야 한다.

OutputStream out = new FileOutputStream("data.dat");
BufferedOutputStream bfOut= new BufferedOutputStream(out);
DataOutputStream dfOut = new DataOutputStream(bfOut);

버퍼 스트림은 기본 입출력 스트림에 연결되어 입출력의 효율을 향상시키는 필터 스트림이다.
따라서 위의 그림과 같은 구조로 스트림을 연결해야한다(순서를 바꾸면 컴파일 되더라도 예외발생)

위 그림에 해당하는 스트림 생성하여 기본 자료형 저장하는 코드

public class BufferedDataOutputStream {
    public static void main(String[] args) {
        try(DataOutputStream out = 
                new DataOutputStream(
                        new BufferedOutputStream(
                                new FileOutputStream("data.dat")))){
            out.writeInt(370);
            out.writeDouble(3.14);
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}


버퍼링 기능이 존재하는 스트림을 생성해서 값을 꺼내는 코드

public class BufferedDataInputStream {
    public static void main(String[] args) {
        try(DataInputStream in =
                new DataInputStream(
                        new BufferedInputStream(
                                new FileInputStream("data.dat")))){
            int num1 = in.readInt();
            double num2 = in.readDouble();

            System.out.println(num1);
            System.out.println(num2);
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}


370
3.14

문자 스트림의 이해와 활용

바이트 스트림과 문자 스트림의 차이

입출력 과정에서 "데이터의 변화 없이" 바이트 단위로 데이터를 입출력하는것이 입출력의 기본이다.
그러나 문자를 입출력할 때에는 데이터 수정이 필요하다 그래서 자바는 '문자 스트림' 이라는
것을 별도로 지원한다.

문자를 입출력 할 때에는 왜 데이터의 수정이 필요할까?

자바는 모든 문자를 '유니코드'를 기준으로 표현한다.
그러나 운영체제의 문자 표현방식은 한글 윈도우의 경우 '코드페이지949'라는 인코딩 방식을
기준으로 문자를 표현하기 때문에 자바 프로그램에서 파일에 문자를 저장하고
해당 파일을 메모장과 같은 프로그램으로 확인을 하려면

"유니코드로 표현된 문자를 해당 운영체제의 문자 표현 방식으로 바꾸어서 저장한다"

그런데 이러한 작업을 프로그래머가 직접 하기엔 번거로우니 문자 스트림을 사용하면
이를 대신 진행 해준다.

FileReader&FileWriter

앞서 설명한 바이트 스트림의 생성과 관련된 클래스가 상속하는 클래스는 각각 다음과 같다.

InputStream - '바이트 입력 스트림의 상위 클래스'
OutputStream - '바이트 출력 스트림의 상위 클래스'

이에 대응하는 문자 스트림의 생성과 관련된 클래스가 상속하는 클래스는 다음과 같다

Reader - '문자 입력 스트림의 상위 클래스'
Writer - '문자 출력 스트림의 상위 클래스'

앞서 소개한 파일을 대상으로 하는 바이트 입출력 스트림을 생성하는 클래스

FileInputStream - '파일 대상 바이트 입력 스트림 생성'
FileOutputStream - '파일 대상 바이트 출력 스트림 생성'

이에 대응하는 파일을 대상으로 하는 문자 입출력 스트림 생성하는 클래스

FileReader - '파일 대상 문자 입력 스트림 생성'
FileWriter - '파일 대상 문자 출력 스트림 생성'

관련 문제

Q1 문자만 저장되어 있는 파일을 복사하려 한다 이떄 필요한 스트림은?
A 바이트 스트림 

Q2 자바 프로그램에서 문자 하나를 파일에 저장했다가 다시 읽어들이려 한다 이때 필요한 스트림은?
A 바이트 스트림 (불필요한 인코딩 제한)

Q3 운영체제상 만든 텍스트 파일의 내용을 자바 프로그램에서 읽어서 출력 이때 필요한 스트림은?
A 문자 스트림
운영체제상에서 만든 텍스트 파일의 내용을 읽어서 출력하는 코드

public class TextReader {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("읽을 파일 : ");
        String src = sc.nextLine();

        try(Reader in = new FileReader(src)){   //문자 입력 스트림 생성
            int ch;

            while(true){
                ch = in.read(); //문자를 하나씩 읽는다.
                if(ch == -1)    //더 이상 읽을 문자가 없다면,
                    break;
                System.out.print((char)ch); //문자를 하나씩 출력
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

출력:
메모장으로 작성된 내용들

--------------------------------------------------------------------------
위 코드에서 다음 메소드 호출을 통해서 문자를 읽어 들였다.

public int read() throws IOexception
-> 하나의 문자를 반환 반환할 문자 없으면 -1 반환

반환형이 int이므로 char형으로 형 변환이 필요하다
하지 않으면 정수가 출력된다.
운영 체제상에서 확인할 수 있는 형태로 알파벳을 파일에 저장하는 코드

public class TextWriter {
    public static void main(String[] args) {
        try(Writer out = new FileWriter("data.txt")){   //문자 출력 스트림 생성
            for(int ch = (int)'A'; ch<(int)('Z'+1); ch++)
                out.write(ch);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

ABCDEFGHIJKLMNOPQRSTUVWXYZ

-----------------------------------------------------------------------
		for(int ch = (int)'A'; ch<(int)('Z'+1); ch++)
                out.write(ch);
                
알파벳 유니코드 값은 A부터 Z까지 1씩 증가한다. 다음과 같이 그 값을 1씩 증가시켜서
저장을 하면 출력 스트림을 통해서 알파벳 A부터 Z까지 저장이 된다

물론 문자 출력 스트림을 통해서 저장했으니 문자들은 해당 운영체제으 인코딩 방식으로
바뀌어 저장이 된다.
               

BufferedReader & BufferedWriter

바이트 스트림에는 필터 스트림을 연결할 수 있었다. 대표적인 필터 스트림

BufferedInputStream - '바이트 기반 버퍼 입력 스트림'
BufferedOutputStream - '바이트 기반 버퍼 출력 스트림'

문자 스트림에도 필터 스트림 연결 가능하다 문자 스트림에 버퍼링 기능을 제공하는 필터 스트림

BufferedReader - '문자 기반 버퍼 입력 스트림'
BufferedWriter - '문자 기반 버퍼 출력 스트림'

위의 두 클래스에는 다음과 같이 문자열 단위로 입출력을 진행할수 있는 메소드가 정의되어있다.

public String readLine() throws IOExceptipon	//BufferedReader의 메소드

public void write(String s, int off, int len) throws IOException
-> 문자열 s를 인덱스 off에서부터 len개의 문자까지 저장 //BufferedWriter의 메소드
파일을 생성해서 두 개의 문자열을 저장하기 코드

public class StringWriter {
    public static void main(String[] args) {
        String ks = "공부에 있어서 돈이 꼭 필요한 것은 아니다.";
        String es = "Life is long if you know how to use it.";
        
        try(BufferedWriter bw = 
                new BufferedWriter(new FileWriter("String.txt"))){
            bw.write(ks,0,ks.length());
            bw.newLine();   //줄 바꿈 문자를 삽입
            bw.write(es,0,es.length());
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

파일로부터 문자열을 읽어서 출력하는 코드

public class StringReader {
    public static void main(String[] args) {
        try(BufferedReader br = new BufferedReader(new FileReader("String.txt"))){
            String str;
            while(true){
                str = br.readLine();    //한 문장 읽어 들이기
                if(str == null)
                    break;
                System.out.println(str);
            }
        }
    }
}


 공부에 있어서 돈이 꼭 필요한 것은 아니다
 Life is long if you know how to use it
 
 ---------------------------------------------------------------------------
 bw.newLine() //줄 바꿈 문자를 삽입
 
 운영체제별로 줄 바꿈을 표시하는 방법에는 차이가 있다. 그런데 위의 메소드를 호출하면 
 해당 운영체제에서 정의하고 있는 줄 바꿈 표시 문자를 삽입해준다.
 따라서 위 코드의 실행을 통해 생성된 파일을 메모장과 같은 프로그램으로 열어보면
 두 개의 문자열이 행이 달리하여 저장된다.

IO 스트림 기반의 인스턴스 저장

ObjectInputStrema & ObjectOutputStream

바이트 스트림을 통해서 인스턴스를 통째로 저장하고 꺼내는 것도 가능하다. 
이렇듯 인스턴스를 통째로 저장하는것을 '객체 직렬화'라 하고
역으로 저장된 인스턴스를 꺼내는것을 '객체 역 직렬화'라 한다.
인스턴스의 저장은 당연히 바이트 스트림을 통해서 이뤄진다. 
바이트 기반의 기본 입출력 스트림에 다음 스트림을 연결하면 인스턴스 통째로 입출력 가능하다.

ObjectInputStream - '인스턴스를 입력하는 스트림'
ObjectOutputStream - '인스턴스를 출력하는 스트림'

위의 두 스트림은 사용방법이 필터 스트림과 유사하다. 그러나 이들은 필터 스트림으로 구분하지
않는다. 이유는 필터 스트림이 상속하는 다음 두 클래스를 상속하지 않기 때문이다.

FilterInputStream - '필터 입력 스트림이 상속하는 클래스'
FilterOutputStream - '필터 출력 스트림이 상속하는 클래스'

그러나 코드의 작성 방법은 필터 스트림을 생성해서 연결할때와 같으니 어렵지 않게 사용 가능하다
그러나 다음 사실은 별도로 기억을 해야한다

"입출력의 대상이 되는 인스턴스 클래스는 java.io.Serializable을 구현해야 한다"

Serializable는 마커인터페이스로 직렬화 가능함을 표시하는 인터페이스다
따라서 구현해야 할 메소드는 없다.
파일에 인스턴스를 저장하는 코드

public class SBox implements java.io.Serializable{
    String s;
    public SBox(String s){this.s = s;}
    public String get(){return s;}
}

Serializable 인터페이스를 구현했으니 위 클래스의 인스턴스는 바이트 스트림을 통한 입출력이
가능하다. 

----------------------------------------------------------------------------

다음 코드를 실행하면 Object.bin이라는 파일이 생성되고
이 파일에 두 개의 SBox 인스턴스가 저장된다

public class ObjectOutput {
    public static void main(String[] args) {
        SBox box1 = new SBox("Robot");
        SBox box2 = new SBox("Strwaberry");

        try(ObjectOutputStream oo =
                new ObjectOutputStream(new FileOutputStream("Ojbect bin"))){
            oo.writeObject(box1);   //box1이 참조하는 인스턴스 저장
            oo.writeObject(box2);   //box2가 참조하는 인스턴스 저장
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

인스턴스의 저장을 위해 호출한 메소드는 다음과 같다.

public final void writeObject (Ojbect obj) throws IOException

-------------------------------------------------------------------------

인스턴스의 복원 코드

public class ObjectInput {
    public static void main(String[] args) {
        try(ObjectInputStream oi = 
                new ObjectInputStream(new FileInputStream("Object.bin"))){
            SBox box1 = (SBox)oi.readObject();  //인스턴스 복원
            System.out.println(box1.get());
            SBox box2 = (SBox)oi.readObject();  //인스턴스 복원
            System.out.println(box2.get());
        }
        catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

Robot
Straberry

출력 내용을 통해서 저장했던 인스턴스가 정상적으로 복원되었음을 알 수 있다.
인스턴스를 읽어들일때 호출한 메소드는 다음과 같다

public final Object readObject() throws IOException, ClassNotFoundException

이 메소드는 반환형이 Object이다. 따라서 저장된 인스턴스의 형에 맞게 명시적으로
형 변환을 진행할 필요가 있다.

transient

다음 코드를 관찰해보자.

public class SBox implements java.io.Serializable{
    String s;
    public SBox(String s){this.s = s;}
    public String get(){return s;}
}

위 클래스의 인스턴스 변수 s는 다른 인스턴스를 참조하는 참조변수이다.
그리고 이전 코드의 실행 결과를 통해서 다음 사실을 알 수 있다.

"인스턴스를 저장하면 인스턴스 변수가 참조하는 인스턴스까지 함께 저장이 된다"

즉 위 클래스의 인스턴스를 저장하면 s가 참조하는 인스턴스까지 함께 저장이 이뤄진다.
이렇듯 함께 저장이 되려면 해당 인스턴스의 클래스도 Serializable을 구현하고 있어야 한다.
그런데 String 클래스는 다음과 같이 Serializable을 구현하고 있다.

public final class String extends Object implements Serializable

그래서 이전 코드에서 SBox 인스턴스를 저장하고 복원할때 참조변수 s가 참조하는 인스턴스까지
함께 저장되고 또 복원되었던 것이다.
그런데 만약에 참조변수 s가 참조하는 인스턴스의 저장을 원치 않는다음 다음과 같이
transient 선언을 추가하면 된다.

public class SBox implements java.io.Serializable{
    transient String s;	//이 참조변수가 참조하는 대상은 저장하지 않겠다는 선언
    public SBox(String s){this.s = s;}
    public String get(){return s;}
}

이렇듯 인스턴스 변수앞에 transient 선언을 추가하면 이 변수가 참조하는 인스턴스는
저장되지 않는다. 그리고 복원했을때 이 참조변수는 null로 초기화 된다.

코드 확인

public class SBox implements java.io.Serializable{
    transient String s;
    public SBox(String s){this.s = s;}
    public String get(){return s;}
}


-------------------------------------------------------------------------

public class ObjectOutput {
    public static void main(String[] args) {
        SBox box1 = new SBox("Robot");
        SBox box2 = new SBox("Strwaberry");

        try(ObjectOutputStream oo =
                new ObjectOutputStream(new FileOutputStream("Ojbect bin"))){
            oo.writeObject(box1);   //box1이 참조하는 인스턴스 저장
            oo.writeObject(box2);   //box2가 참조하는 인스턴스 저장
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}


public class ObjectInput {
    public static void main(String[] args) {
        try(ObjectInputStream oi =
                new ObjectInputStream(new FileInputStream("Ojbect bin"))){
            SBox box1 = (SBox)oi.readObject();  //인스턴스 복원
            System.out.println(box1.get());
            SBox box2 = (SBox)oi.readObject();  //인스턴스 복원
            System.out.println(box2.get());
        }
        catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

출력
null
null

---------------------------------------------------------------------------

이렇듯 인스턴스 변수 앞에 transient 선언을 추가하면 이 변수가 참조하는 인스턴스는
저장되지 않는다. 그리고 복원했을때 이 참조 변수는 null로 초기화된다
이러한 transient 선언은 다음과 같이 기본 자료형 변수에도 할 수 있다.
그러면 인스턴스 저장시 해당 변수가 갖고 있는 값은 저장되지 않는다
그리고 복원을 하면 이 변수는 0으로 초기화 된다.

	public class IBox implements java.io.Serializable{
    	transient int n;	//이 변수의 값은 저장 대상에서 제외한다.
        public IBox(int n){this.n = n;}
        public int get(){return n;}
    }
profile
반갑습니다

0개의 댓글