Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
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
Tags
more
Archives
Today
Total
관리 메뉴

forDevLife

[자바의정석] Chap 15. 입출력 I/O 본문

Java

[자바의정석] Chap 15. 입출력 I/O

JH_Lucid 2021. 3. 25. 23:54

스레드, 람다, 스트림 양이 많아 우선 가장 중요한 부분부터 진행하고, 스프링 공부하면서 필요한 부분 있으면 다시 돌아와서 공부할 예정이다. 계속 언어 공부만 하다보니 살짝 지겨운 감이 있어서, 얼른 웹 개발을 경험해보고자 하였다.

 

1. 1 입출력이란?

- I/O란 Input / Output 약자로, 컴퓨터 내/외부 장치와 프로그램 간의 데이터를 주고 받는 것을 의미한다.

 

1.2 스트림(stream)

- 다행히도, 14장의 스트림과는 전혀 다른 개념이다. 

   자바에서 입출력을 수행하려면, 두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요한데 이를 스트림이라고 정의한다.

 

- 스트림은 데이터를 운반하는데 사용되는 연결통로이다.(TV - DVD를 연결하는 입력선/출력선 역할을 한다고 보면 된다.)

- 스트림은 단방향 통신만 가능, 따라서 입/출력 동시에 처리할 수 없다.

- 따라서 입/출력 동시에 수행하려면 2개의 스트림이 필요하다. 입력 스트림 / 출력 스트림

   -. 자바 application 방향으로 들어감 : 입력 스트림

   -. 자바 application에서 외부로 나감 : 출력 스트림 

 

1.3 바이트기반 스트림 - InputStream, OutputStream - 얘네는 추상 클래스

- 스트림은 바이트단위로 데이터를 전송하며, 입출력 대상에 따라 다음과 같은 입출력 스트림이 있다.

 

1) 파일                          - 입력 : FileInputStream / 출력 : FileOutputStream

2) 메모리(byte 배열)     - 입력 : ByteArrayInputStream / 출력 : ByteArrayOutputStream

3) 프로세스(IPC)           - 입력 : pipedInputStream / 출력 : pipedOutputStream

4) 오디오장치                - 입력 : AudioInputStream / 출력 : AudioOutputStream

 

- 이들은 모두 InputStream / OutputStream의 자손들이며, 각각 읽고 쓰는데 필요한 추상 메서드를 자신에 맞게 구현해 놓았다.

 

- abstract로 int read()와 void write(int b)가 정의되고, 각 인자에 따라서 read, write 메서드가 추가로 정의되어 있다. 하지만 이 메서드들은 결국 abstract 메서드로 구현되는 것으로, abstract 메서드가 구현되어 있지 않으면 아무런 의미가 없다.

 

- 메서드는 선언부만 알고 있어도 호출이 가능, 추상 메서드를 호출하는 코드를 작성할 수 있다. 

- 실제로는 추상 클래스를 상속받아서 추상 메서드를 구현한 클래스의 인스턴스에 대해서 추상 메서드가 호출될 것이기 때문에, 추상 메서드를 호출하는 코드를 작성해도 문제가 없다. 즉 다 구현되어있는(추상 메서드를 구현하였으므로) 메서드를 호출하는 것이다.

 

- 결론적으로 read()는 반드시 구현되어야 하는 핵심 메서드이다!

 

1.4 보조 스트림

앞서 언급된 스트림 외에도 스트림의 기능을 보완하기 위한 보조스트림이 제공된다. 이는 실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능은 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다. 

따라서 스트림을 먼저 생성한 다음에 이를 이용하여 보조스트림을 생성해야 한다.

 

- 부모는 결국 InputStream, 따라서 입출력방법이 동일하다.

- 보조 스트림 중 가장 상위는 FilterInputStream

- 그 하위로 BufferedInputStream, DataInputStream, DigestInputStream, LineNumberInputStream, PushbackInputStream이 있음

 

1.5 문자 기반 스트림 - Reader, Writer

여지껏 알아본 스트림은 모두 바이트 기반 (입출력의 단위가 1 byte)이다. 

Java에서는 한 문자를 의미하는 char 형이 1이 아닌 2 byte이기 때문에, 바이트 기반으로는 문자를 처리하는 데는 어려움이 있다.

 

- 이를 보완하기 위해서 문자기반 스트림이 제공

   -. InputStream -> Reader

   -. OutputStream -> Writer

 

- 문자 기반 스트림에도 추상 메서드가 존재하며, 기존 바이트 스트림에서의 read()가 아닌 'int read(char[] cbuf, int off, int len)을 추상메서드로 사용한다. 이게 더 바람직하다고 하는데 뒤에서 배우겠쥬

 

- 마찬가지로 문자 기반 보조스트림이 존재한다.

 


2.1 InputStream과 OutputStream 

여기서부터는, 앞에서 간단히 설명된 바이트기반 / 문자기반 스트림에 대해서 더 자세히 알아보자. 먼저 바이트기반이다.

 

InputStream : 스트림의 종류에 따라서 mark(현재 위치를 표시, 후에 reset()에 의해 표시해 놓은 위치로 다시 돌아갈 수 있다.)), reset(스트림에서의 위치를 마지막으로 mark()이 호출되었던 위치로 되돌린다)를 사용하여 이미 읽은 데이터를 되돌려서 다시 읽을 수 있다.

 

OutputStream : flush(스트림의 버퍼에 있는 모든 내용을 출력소스에 씀)는 버퍼가 있는 출력스트림의 경우에만 의미가 있다.

 

- 프로그램이 종료될 때, 사용하고 닫지 않는 스트림을 JVM이 닫아주기는 하나, 무조건 close()로 닫는 것을 생활하하자.

- 하지만 ByteArrayInputStream(메모리, 즉 바이트 배열 사용) 그리고 System.in, System.out(표준 입출력)은 안닫아도 된다.

 

2.2 ByteArrayInputStream과 ByteArrayOutputStream

 

- 이는 메모리, 즉 바이트배열에 데이터를 입출력 하는데 사용되는 스트림이다. 주로 다른 곳에 입출력하기 전에 데이터를 임시로 바이트배열에 담아서 변환 등의 작업을 하는데 사용된다.

 

- read, write는 인자가 다른 메소드로 여러 개 정의되어 있으며, 이를 이용하여 한꺼번에 더 많은(인자로 전달된 배열 크기만큼) 내용을 읽고 쓸 수 있다.

 

- read()는 더 읽어올게 없으면 -1을 반환한다. input 스트림으로 한번 읽어진 데이터는 다시 사용하지 못한다. 

   즉, input = new ByteArrayInputStream(inSrc); inSrc는 바이트 배열일 경우, input.read를 통해 값들을 모두 읽으면 이 input에는 아무것도 없게 되므로, 다시 값을 넣어줘야 사용 가능해진다.

 

- read()는 읽어온 데이터의 수를 반환한다. 이를 변수로 저장하여 원하는 데이터 길이만큼만 읽어올 수 있도록 응용한다.

 

 

2.3 FileInputStream과 FileOutputStream

+ 일단 예제 879 진행 중 터미널 입력이 안되어 겁나 헤맸다.

패키지가 있을 경우, 해당 디렉토리 내에서 컴파일 후에 상위(src)로 나와서 실행 시켜야 한다. 나 같은 경우 i_o_study라는 패키지 내에 FileViewer.java를 만들었고, 실행 시 인자로 FileViewer.java를 전달할 계획이었다. 아래 처럼 실행하면 된다.

 

src로 올라온 후 > java i_o_study.FileViewer ./i_o_study/FileViewer.java 

 

 

3.1 FilterInputStream과 FilterOutputStream (여기부턴 바이트기반 보조 스트림)

 

- 이들은 Input/OutputStream의 자손이면서 모든 보조스트림의 조상이다. 자체적으로 입출력 수행이 불가능하기 때문에,

   기반(기초)스트림을 필요로 한다. 

 

- 이들의 모든 메서드는 단순히 기반스트림의 메서드를 그대로 호출할 뿐이다. 이들 자체로는 아무런 일도 안한다.

 

- 상속을 통해 원하는 작업을 수행하도록 읽고 쓰는 메서드를 오버라이딩해야 한다.

 

- 생성자 FilterInputStream(InputStream in)는 접근 제어자가 protected이기 때문에 인스턴스를 생성해서 사용할 수 없고, 상속을 통해서 오버라이딩 되어야 한다. 이건 뭔소리인지 이해가 안간다. protected이면 인스턴스 생성이 불가였나?

 

3.2 BufferedInputStream과 BufferedOutputStream

 

- 스트림의 입출력 효율 높이기 위한 버퍼를 사용하는 보조 스트림. 

 

- 프로그램에서 입력소스로 부터 버퍼 크기만큼의 데이터를 읽어다 자신의 내부 버퍼에 저장. 프로그램에서는 BufferedInputStream에 저장된 데이터를 읽으면 되므로, 훨씬 빨라 작업 효율이 높아짐(외부 입력소스에서 읽어오는 것보다 내부 버퍼에서 읽는게 더 빠름)

 

- flush() > 버퍼의 모든 내용을 출력소스에 저장, 그리고 버퍼를 비운다.

 

- close() > flush() 호출 후, BufferedOutputStream 인스턴스가 사용하던 모든 자원을 반환.

 

- 버퍼가 가득 찼을 때만 출력소스에 출력한다. 따라서 내용이 버퍼에 남아있는 채로(출력소스에 쓰이지 않은 채로) 종료될 수 있다.

   따라서 close, flush를 통해 마지막 버퍼를 모두 출력하도록 해줘야 한다.

   > 예를 들어, 버퍼사이즈 5이고 보낼 데이터가 10개일 경우, 5개 채우고 6번째거 넣으려 할때 버퍼가 출력된다.

      0 1 2 3 4 5 6 7 8 9 에서, flush 안해주면 0 1 2 3 4 만 전달된다. 따라서 꼭 버퍼를 비워줘야 하는 것 잊지 말자.

 

- 출력의 경우, [자바 프로그램] > [BufferedOutputStream(보조 스트림)] > [FileOutputStream(기반 스트림)] > [file] 

- 입력의 경우, 뭐 위랑 반대겠지 뭐.

 

- 보조스트림을 사용한 경우에는 기반스트림의 close / flush 대신 보조의 close만 호출하면 된다.

 

3.3 DataInputStream과 DataOutputStream

- 이들도 각각 FilterInput/outputStream의 자손이다.

 

- 데이터를 읽고 쓰는데 있어서 byte단위가 아닌, 8가지 기본 자료형의 단위로 읽고 쓸 수 있는 장점이 있다.

 

- DataOutputStream이 출력하는 형식은 각 기본 자료형 값을 16진수로 표현하여 저장한다. 예로 int를 출력한다면 4byte의 16진수 출력

   > 이진 데이터로 출력되므로, vi를 통해 보면 이해하기 어렵다. 바이너리 값 - 아스키코드로 보인다.

   > 예로, 85를 write하면 85의 아스키값인 U가 입력된다. 65 Write하면 A가 입력된다. 

 

- 각 자료형의 크기가 다르므로, 출력한 데이터를 다시 읽어올 때는 출력했을 때의 순서를 염두에 두어야 한다.

  > Byte는 부호 비트가 있으므로, 범위가 -128 ~ 127이다. 따라서 이 값을 0~255로 변경해주기 위해 256을 더해줘야 한다.  

 

- readInt()시 더 이상 읽을 데이터가 없으면 EOFException 발생시킨다. 이를 이용하여 예외처리 catch블록에서 합 등을 구한다.

 

- finally는 항상 실행되는 블록이므로, 여기에서 close()를 통해 스트림을 닫아준다. close역시 IOException을 발생시킬 수 있으므로 

   try ~ catch 로 닫아줘야 한다.

 

- try - with - resoureces 문을 이용하면, close 호출안해도 자동 호출되도록 할 수 있다. 

   try ( ~ ; ~ ) { } catch를 이용하면, try () 부분에 들어 있는 in/out 스트림들이 처리 완료 시 close 호출된다.

 

3.4 SequenceInputStream (input만 있음 유의)

- 여러 개의 입력스트림을 연속적으로 연결하여 하나의 스트림으로부터 데이터를 읽는 것과 같이 처리할 수 있도록 도와준다.

- 다른 보조스트림과는 달리 FilterInputStream의 자손이 아닌 InputStream을 바로 상속받아서 구현한다.

- 생성자를 제외하고 나머지 작업은 다른 입력스트림과 다르지 않다.

- 생성자

    -. SequenceInputStream(Enumeration e) > Enumeration에 저장된 순서대로 입력 스트림을 하나의 스트림으로 연결한다.

    -. SequenceInputStream(InputStream s1, InputStream s2) > 두 개의 입력 스트림을 하나로 연결한다.

 

- Vector에 연결할 입력스트림을 저장한 다음, Vector의 elements() - Enumeration 반환 메소드를 호출하여 생성자 매개변수로 사용

 

4. 문자기반 스트림

문자데이터를 다루는데 사용되는거 말고는 바이트기반 스트림과 사용방법이 거의 같다. 추가로 설명이 필요한 부분만 정리하자.

 

4.1 Reader와 Writer

Reader / Writer 가 조상 역할을 한다. byte 대신 char 배열을 사용한다는 것 외에는 메서드가 다르지 않다.

 

+ 문자 기반 스트림이라는 것이 단순히 2 byte로 스트림을 처리하는 것만을 의미하지는 않는다. 문자 데이터를 다루는데 필요한 또 하나의

   정보는 인코딩(encoding)이다. 

 

+ 문자기반 스트림, 그리고 그 자손들은 여러 종류의 인코딩과 자바에서 사용하는 유니코드간의 변환을 자동적으로 처리해준다. 

   Reader는 특정 인코딩을 읽어서 유니코드로 변환, Writer는 유니코드를 특정 인코딩으로 변환하여 저장한다.

 

4.2 FileReader와 FileWriter

사용 방법은 FileInput/OutputStream과 다르지 않으므로 자세한 내용은 생략한다.

 

4.3 PipedReader와 PipedWriter

이들은 쓰레드 간에 데이터를 주고받을 때 사용된다. 입력 / 출력 스트림을 하나의 스트림으로 연결해서 데이터를 주고 받는다.

스트림을 생성한 다음에 어느 한쪽 쓰리데에서 connect()를 호출하여 입 / 출력을 연결한다.

 

- 여기서 쓰레드에 대해서 복습해볼 수 있다.

- InputThread, OutputThread 각각 Thread를 상속받는다. 그리고 각 클래스는 PipedReader/Writer를 인스턴스로 가진다.

- InputThread에서는 추가로 StringWriter를 가진다. 이는 그냥 메모리를 사용하는 스트림인데, 내부적으로 String Buffer를 가진다.

- inThread.start() / outThread.start()가 실행된다. 

- inThread의 run에서는, input.read()를 통해 계속해서 읽기를 시도하는 것으로 보인다. while문에서 data는 계속 0이므로, 무한 반복된다.

- output.write()를 통해 메세지가 작성되면, input.read()에서는 그때야 StringWriter로 데이터를 읽어온다.

- 더 이상 읽어올게 없다면, -1이 실행된 후 종료된다.

- 쓰레드 실행을 start가 아닌 run으로 하고나서 inThread.run()을 먼저 하면, 무한 반복 상태가 된다. 같은 쓰레드에서 inThread.run()이 데이터를 받을 때까지 while이 돌기 때문에, 정확히 말하면 while이 도는게 아니라, read로 데이터가 안들어가서, 해당 메서드 자체가 대기중인것으로 보인다.

 

 

5.1 BufferedReader와 BufferedWriter

해당 버퍼를 이용해서 입출력의 효율을 높일 수 있도록 한다. 효율이 비교할 수 없을 정도로 좋아지므로, 사용하는게 좋다.

- BufferedReader의 readline()을 사용하면, 데이터를 라인 단위로 읽을 수 있다. -> 줄 단위로 읽기 가능

- BufferedWriter는 newLine()이라는 줄바꿈 해주는 메서드를 가지고 있다.

 

5.2 InputStreamReader와 OutputStreamWriter > Reader / Writer의 자손

- 이름에서 알 수 있는 것과 같이 바이트 기반 스트림을 문자 기반 스트림으로 연결시켜주는 역할을 한다.

- 또한 바이트 기반 스트림의 데이터를 지정된 인코딩의 문자데이터로 변환하는 작업을 수행한다. 

- 생성자에서, 두번째 인자로 인코딩 방식을 지정해 줄 수 있다. 작성하지 않을 경우, OS에서 사용되는 인코딩을 사용해서 파일을 해석 / 저장

- 시스템 속성에서 sun.jnu.encoding의 값을 보면 사용하는 인코딩의 종류를 알 수 있다.

 

6.1 표준입출력 - System.in, System.out, System.err

- 표준 입출력은 콘솔을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미한다.

- 이들은 자바 어플리케이션의 실행과 동시에 사용할 수 있게 자동적으로 생성되기 때문에 개발자가 별도롤 스트림을 생성하는 코드를 작성안해도 사용 가능하다.

  -. System.in > 콘솔로부터 데이터를 입력받는데 사용

  -. System.out > 콘솔로 데이터를 출력하는데 사용

  -. System.err > 콘솔로 데이터를 출력하는데 사용

 

- in, out, err는 System 클래스에 선언된 클래스 변수(static변수)이다. 

   - public final static InputStream in = nullInputStream();  > 선언으로 봐서는 in 타입은 InputStream,

      실제로는 버퍼 사용(BufferedInputStream) 

   - public final static PrintStream out/err = nullPrintStream(); > 선언으로 봐서는 out, err 타입은 PrintStream,

     실제로는 버퍼 사용(BufferedOutputStream)

 

- System.in.read()가 호출되면, 코드의 진행을 멈추고 콘솔에 커서가 깜빡이며 입력을 기다린다. 

- 이 때 문제는, 줄이동 및 캐리지 리턴(커서를 현재 라인의 첫 번째로 이동시킴)도 사용자 입력으로 간주된다는 점이다.

- 따라서, System.in에 BufferedReader를 이용해서 readline()을 통해 라인 단위로 데이터를 입력 받는다.

   > InputStreamReader isr = new InputStreamReader(System.in);

      BufferedReader br = new BufferedReader(isr);

      line = br.readLine();

 

6.2 표준입출력의 대상 변경 - setOut(), setErr(), setIn()

- 이들을 사용해서, 입출력을 콘솔 이외에 다른 입출력 대상으로 변경하는 것이 가능하다.

   static void setOut(PrintStream out)      > System.out의 출력을 지정된 PrintStream으로 변경

   static void setErr(PrintStream err)      > System.err의 출력을 지정된 PrintStream으로 변경

   static void setIn(InputStream in)      > System.in의 입력을 지정된 InputStream으로 변경

 

- 그러나 JDK1.5부터 Scanner 클래스가 제공되면서 System.in으로부터 데이터를 입력받아 작업하는 것이 편리해졌다.

 

 

6.3 RandomAccessFile

- 자바에서는 입력과 출력이 각각 분리되어 별도로 작업을 하도록 설계되어 있다. 

- 하지만 RandomAccessFile만은 하나의 클래스로 파일에 대한 입력과 출력을 모두 할 수 있도록 되어 있다.

- DataInput / DataOutput interface를 모두 구현했기 때문에 읽기 쓰기가 모두 가능하다.

- DataInputStream / DataOutputStream은 위의 인터페이스에 정의된 메서드를 사용한다.

    > 따라서 RandomAccessFile을 사용하여 기본자료형 단위로 데이터를 읽고 쓸 수 있다.

 

- 가장 큰 장점은 파일 어느 위치에나 읽기 / 쓰기가 가능하다는 것이다. 

    > 다른 입출력 클래스들은 입출력소스에 순차적으로 읽/쓰 하기 때문에 제한적임.

- 이를 가능하게 하기 위해, 내부적으로 '파일 포인터' 사용, 입출력 시에 작업이 수행되는 곳이 파일 포인터 위치한 곳이다.

- 읽기 / 쓰기 수행할 때 마다 작업이 수행된 다음 위치!로 이동하게 된다. 

   > 처음 시작은 0, int를 저장하면 0 1 2 3 만큼에 저장되고 포인터는 4를 가리킨다. 

- 임의의 위치에 있는 내용에 대해서 작업하고자 한다면, 파일 포인터를 원하는 위치로 옮기고 작업을 해야 한다.

 

- 현재 작업중인 파일에서 파일 포인터 위치 > getFilePointer(), 맨 처음은 0이다.

- 파일 포인터 위치 옮기기 > seek(long pos) / skipBytes(int n)

+ 사실 모든 입출력에 사용되는 클래스들은 포인터를 내부적으로 가지고 있으나, RandomAccessFile 만이 작업자가 컨트롤 가능하다.

 

- 파일을 rw모드로 연 후에, write 하고 read 할 경우 이 점을 유의한다.

   > write후에 fp는 마지막을 가리키고 있다. 따라서 바로 read할 경우, seek(0)을 통해 fp의 위치를 이동해주자.

 

 

Comments