공부/whiteship-java

13주차: I/O

chulphan 2021. 2. 25. 19:01

[백기선님께서 주최하시는 자바 기초 스터디]

github.com/whiteship/live-study

 

whiteship/live-study

온라인 스터디. Contribute to whiteship/live-study development by creating an account on GitHub.

github.com

 

 

 

13주차 주제는 I/O 이다.

 

 

▶ 스트림(Stream) / 버퍼(Buffer) / 채널(Channel) 기반의 I/O

■ 스트림

스트림은 바이트(Byte) 들의 연속적인 흐름이다.

스트림 객체에서 바이트를 읽거나 스트림 객체에다 바이트를 쓸 수 있다

스트림은 파일, 네트워크 등에 연결되어 바이트를 읽어내거나 바이트를 쓸 수 있는 것이다

하나의 스트림은 하나의 방향만 가능하다. 즉, 입력 또는 출력만 할 수 있으며 두 가지 동시에 하고 싶다면 입력 스트림과 출력 스트림 두 개가 필요하다

 

버퍼/채널 IO 를 알기 이전에 Java의 nio 에 대해서 간단히 알아보자. 왜냐하면 이 둘은 nio 가 나타나며 생겨난 용어이기 때문이다

 

¿ java nio??

Java New Input Output 의 줄임말로 Java 1.4에서 소개 된 IO API 이다.

기존에 존재하던 I/O API와 다른 방식으로 작업하기 위한 방법을 제공하는데, 버퍼(Buffer)에 기원한 채널을 기반으로 한 I/O 연산들에 접근한다. 특히 JDK7 에서는 NIO 시스템을 확장해서 파일 시스템과 파일을 조작하는 것들에 대한 지원을 가능하게 했다

또, NIO 는 자바 프로그래머들에게 커스텀 native 코드 없이도 빠른 속도의 I/O 를 구현할 수 있도록 했다고 한다

 

■ Buffer IO

java.io 패키지 내에 Buffers 클래스가 정의되어 있으며, 모든 버퍼에 대한 핵심적인 기능인 limit, capacity, 현재 위치 등을 정의한다

java nio 의 버퍼는 java nio 의 채널들과 상호작용 하는데에 사용된다

이 버퍼는 메모리 블록 내부에 데이터를 쓸 수 있고, 이후에 다시 읽을 수도 있다.

메모리 블록은 nio 의 버퍼 객체로 감싸져서 메모리 블록과 쉽게 작업할 수 있는 메소드들을 제공한다

 

■ Channel IO

java.nio 에서의 채널은 개체와 바이트 버퍼(Byte buffer) 들 간에 데이터를 효율적으로 전송하는데에 사용된다

채널은 개체로 부터 데이터를 읽어들이고 읽어들인 데이터를 소비하기 위해서 버퍼 블록 내부에 위치 시킨다

채널은 I/O 메커니즘에 접근하기 위한 게이트웨이(java.nio 가 제공한) 처럼 행동한다

일반적으로 채널들은 플랫폼에 무관한 연산적 특징을 제공하기 위한 운영체제 파일 명세서와 1:1 관계를 가진다.

 

▶ InputStream 과 OutputStream

■ InputStream

Byte Stream 의 추상 클래스이며 스트림에 대한 입력을 나타내며 파일, 이미지 즉, 바이너리 파일을 읽어 들이는데 사용된다.

InputStream 은 소스로 부터 하나씩 데이터를 읽어들인다.

 

InputStream 클래스에는 소스로 부터 데이터를 읽어들이는 데에 필요한 몇 가지 메소드가 정의되어있다

  • read(): 읽어들인 바이트의 값을 포함하는 정수 값(int) 을 반환한다. 한 번에 하나의 바이트를 읽어들이며 주어진 소스로 부터 더 이상 읽을 데이터가 없으면 -1 을 반환한다. 이를 통해서 파일을 모두 읽었는지 아닌지 판단할 수 있다
  • read(byte[]): 인자로 전달된 byte 배열의 크기만큼 스트림을 읽어들인다. 이 메소드는 실제로 얼마만큼의 바이트를 읽어들였는지에 대한 값을 반환한다.
  • read(byte[], int offset, int length): 이 배열은 read(byte[]) 배열과 거의 동일하지만 차이점은 스트림의 offset 부터 시작하며 length 로 주어진 값 만큼만 읽어낸다
  • readAllBytes(): Java9 부터 포함된 메소드이며, InputStream 내에 유효한 모든 바이트들을 읽어들여서 읽어들인 바이트들을 모두 포함하는 바이트 배열을 반환한다. 이 메소드는 FileInputStream 내에 있는 바이트 배열을 통해 파일에 있는 모든 바이트들을 한꺼번에 읽을 때 유용한다

■ OutputStream

InputStream 과 마찬가지로 Byte Stream 의 추상 클래스이며 바이너리 데이터를 목적지에 써나가는 데에 사용된다

OutputStream 은 목적지에 데이터를 한 번에 하나씩 써나간다

 

OutputStream 클래스에도 목적지에 데이터를 써나가는데 필요한 몇 가지 메소드가 정의되어있다

  • write(byte): OutputStream 에 하나의 바이트를 쓰는데 사용된다. 
  • write(byte[]): 한 번에 OutputStream 바이트의 배열을 쓸 수 있게 해준다
  • write(byte[], int offset, int length): read(byte[], int offset, int length) 와 비슷하며 차이점은 이 메소드는 쓰기 위해 사용한다는 것이다
  • flush(): 이 메소드는 OutputStream 객체에 쓰여진 모든 데이터를 목적지에 써나간다. 이 메소드를 쓰기 전 까지 OutputStream 객체는 실제 목적지에 쓰여진게 아니라 메모리의 어딘가에 머물고 있다. 그리고 이 메소드를 호출하면 메모리 어딘가에 머물고 있는 데이터를 목적지에 실제적으로 쓰여진다

 

※ 주의점

InputStream 과 OutputStream 객체를 통해 작업을 끝낸 뒤에는 반드시 close 메소드를 호출해줘야 한다.

일일히 close 메소드를 호출하기 귀찮으면 try-with-resources 를 통해 이 클래스의 객체들을 활용해주자

 

 

▶ Byte와 Character 스트림

■ 바이트 스트림(Byte Stream)

바이너리 데이터를 읽고 쓰기 위해서 사용되는 스트림.

모든 ByteStream 클래스들은 추상클래스인 InputStream, OutputStream 클래스에 의해서 파생되며 각각 입력, 출력에 대한 역할을 한다

바이트 스크림 클래스의 모든 부분 클래스들의 이름에는 InputStream/OutputStream 을 포함한다

 

■ 문자 스트림(Character Stream)

문자 단위로 입출력하는 클래스이다. 자바는 기본적으로 문자를 처리할 때 유니코드를 사용한다

유니코드를 처리하기 위해서 Reader와 Writer 라는 추상 클래스를 사용한다

문자 스트림 클래스의 모든 부분 클래스들의 이름에는 Reader/Writer 를 포함한다

 

▶ 표준 스트림(System.in, System.out, System.err)

표준 스트림은 JVM이 시작할 때 Java 런타임에 의해 초기화 되므로 어떠한 표준 스트림도 사용자가 객체화 할 필요가 없다

 

■ System.in

일반적으로 콘솔 프로그램의 키보드 입력에 연결된 InputStream 이다. Java 애플리케이션을 시작하고 나서 CLI 콘솔에 타이핑을 하는 동안에 그 입력들이 System.in 을 통해 읽혀지고 읽어들인 것들은 애플리케이션 내에서 사용할 수 있다

 

■ System.out

문자들을 쓸 수 있는 PrintStream 이다. 일반적으로 이 표준 스트림은 우리가 쓴 데이터를 CLI 콘솔 또는 터미널에 출력한다.

대부분 콘솔로만 구현된 프로그램에 사용된다

 

■ System.err

System.out 과 유사하지만 에러 텍스트를 표시하는 데에만 사용한다

 

▶ 파일 읽고 쓰기

여기서는 FileInputStream, FileOutputStream 을 통해서 하나의 파일을 복사하는 코드를 작성해볼 것이다

 

코드를 작성하는데 사용한 파일은 인텔리제이 설치 파일이고 대략 858MB 이다

 

두 가지 방법으로 작성해 볼 것인데, 한 가지 방법은 read(), write(byte) 메소드를 사용하는 것이고

다른 하나는 바이트의 배열을 통해 읽어들이고 읽어들인 바이트 배열을 써나가는 메소드를 사용해볼 것이다

 

1)  read(), write(byte) 를 사용한 파일 복사

800MB가 넘는 파일을 하나의 바이트를 읽고 쓰고 읽고 쓰고를 해서 그런지 30분이 넘는 시간동안 진행해도 복사가 완료되지 않았다.

더 기다릴 수 없어서 강제 종료 하고 진행된 용량을 보니 반 정도 완료 됐었다

 

그에 비해 두 번째 방법으로 진행하면 엄청나게 빠른 속도로 진행된 것을 볼 수 있다

 

2) read(byte[]), write(byte[]) 를 사용한 파일 복사

1024 크기만큼으로 바이트 배열을 선언해주고 코드를 돌려보았다

 

그리고 결과는....

5367ms, 즉 5초 정도로 굉장히 빨라졌다.

 

크기가 작은 파일에 대해서는 1번 방법을 쓰나 2번 방법을 쓰나 큰 차이는 없을 것 같다

 

하지만 크기가 조금이라도 큰 파일에 대해서 하나 읽고 하나 쓰고 하나 읽고 하나 쓰고 하는 1번 방식을 사용한다면 작업 시간에 큰 차이가 날 것이다

 

그래서 사용하는 파일의 크기에 따라서 적절한 방법을 사용해야 될 것 같다

 

참고)

어서와 Java는 처음이지! JDK8로 배우는 자바 프로그래밍 천인국[저], 인피니트북스

http://tutorials.jenkov.com/java-io/overview.html

http://tutorials.jenkov.com/java-io/inputstream.html 

http://tutorials.jenkov.com/java-io/streams.html 

https://www.baeldung.com/java-inputstream-to-outputstream 

'공부 > whiteship-java' 카테고리의 다른 글

15주차: 람다식  (0) 2021.03.06
14주차: 제네릭  (0) 2021.02.27
12주차: 애노테이션  (0) 2021.02.06
11주차: enum  (0) 2021.01.30
10주차: 멀티쓰레드 프로그래밍  (0) 2021.01.22