Java - 기본기

Java 입출력(I/O), 스트림(Stream), 버퍼(Buffer) 개념 및 사용법

TerianP 2021. 9. 26.
728x90

최근 백준에서 문제를 풀다가 Buffer 와 관련한 문제를 마주치게 되었다.

 

사실 지금까지 Buffer에 대해서 정말 하나도 몰랐고, Scanner 만 사용하여 입력받고 println 을 통해 출력하기만 했었던지라 이번 기회에 Buffer 에 대해서 공부하며 알게 된 것들에 대해 정리해보려 한다.

 

Java 입출력

지금까지 기본적으로 자바에서 입출력을 위해 사용하던 것은 Scanner 와 print 이다. 이것들은 자바에서 가장 쉽게 입출력을 구현할 수 있는 수단이고, 때문에 그냥 단순히 입출력하는 방법 으로만 알아왔다.

사실 컴퓨터에서는 이러한 입력과 출력 즉, Input과 Output 을 줄여서 I/O 라고 한다. 자바에서는 이러한 모든 I/O 가 Stream(스트림) 을 통해 이루어진다.

 

- What is Stream?

그렇다면 Stream(스트림) 은 무엇일까? 스트림은 쉽게 이야기해서 Byte 형태로 데이터를 운반하는데 사용되는 연결통로 라고 생각하면 된다. 이는 자료(data)의 흐름이 물의 흐름과 같다는 의미에서 사용되었다고 한다. 다만 물이 한쪽 방향으로만 흐르는 것과 같이 스트림은 단방향 통신만 가능하기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리 할 수 없다. 또한 스트림은 먼저 보낸 데이터를 먼저 받게 되어있으며 연속적으로 데이터를 주고 받는다는 점에서 큐(queue)의 FIFO(First in Frist Out) 구조로 되어 있다.

 

이때 데이터의 스트림의 근원지(시작점)을 Source, 데이터 종착점을 Sink, 연결한 것을 Stream 이라고 표현하며, Source - 입력 스트림 input Stream- 출력 스트림 output Stream- Sink 으로 연결된다.

 

요약하자면 스트림(Stream)은 Byte 형태로 데이터를 운반하는데 사용되는 연결통로로써 단방향 통신을 하며, 큐의 FIFO 구조로 되어있다. 또한 입력과 출력을 통시에 수행하려면 입력을 위한 입력 스트림(input stream)과 출력을 위한(output stream), 모두 2개의 스트림이 필요하다는 것이다. 마지막으로 Source - 입력 스트림 input Stream- 출력 스트림 output Stream- Sink 순서로 데이터가 흐른다.

 

- Stream 활용 : InputStream / OutputStream

자바에서는 처리 단위에 따라 Reader - inputStream / Writer - outputStream 으로 나뉘어 통신한다. 이때 입력 스트림 inputStream 은 스트림을 한 줄 씩 읽고, 출력스트림 outputStream 으로 데이터를 내보내며 해당 공간을 비운다.

즉, 컴퓨터와 키보드를 연결하여 '입력' 받는 것은 입력 스트림림인 InputStream 의 역할이며, 컴퓨터에 입력된 정보를 모니터로 출력하는 것은 출력 스트림 OutputStream 의 역할이라고 할 수 있다. 이때 InputStream 은 System.in 을 사용하며, OutputStream 은 System.out 을 사용한다.

 

InputSteam / outputStream 은 자바에서 다음과 같이 사용 할 수 있다.

import java.io.InputStream; // 임포트 필수
import java.io.OutputStream;		
  
 /*
* InputStream 로 입력받는 경우 맨 앞 문자 1개만 출력됨 && int 형태로 입력받음
 */		
InputStream in = System.in;
OutputStream out = System.out;
        
int idata = in.read(); // input 은 read 와 연결되어있기 때문에 in.read 를 사욯한다.
		
out.write(idata); // output 은 write 와 연결되어있기 때문에 out.write 를 사용한다
out.flush(); // flush 를 써주지 않으면 출력되지 않는다
out.close(); // output 을 끝내는 매서드

 

이때 중요한 것은 다음과 같다.

- InputStream / OutputStream 을 사용하려면 반드시 해당 패키지를 import 해주어야한다 => 이 부분은 귀찮으면 import java.io.* 로 퉁쳐도 상관없다

- inputStream 로 받아오는 경우 여러 개의 값을 입력해도 단 1개의 문자밖에 못 가져오며 기본형은 int 로 받아온다.

따라서 char 형태로 표현하고 싶다면, char 타입으로 캐스팅해주어야 한다.

많이 입력해도 출력되는건 단 하나!

- 출력을 위해서는 out.write() 후 flush() 와 close() 를 모두 사용해주어야한다. flush 는 write 에 저장된 값을 출력함과 동시에 비워주는 역할이고, close() 를 끝 마무리해주는 역할이라고 한다.

 

- 추가로 System.in 같은 경우 사실 많이 봤던건데, 그렇다. 바로 입력을 받기 위해 자주 사용하는 Scanner 에서 사용하는 바로 그것이다(아마도...맞을 것이다)

Scanner scan = new Scanner(System.in) // 그렇다 바로 scanner 에서 입력 스트림이 사용된다

 

- InputStreamReader / OutputStreamWriter

앞서서 InputStream / OutputStream 을 사용해보았다. 그런데 이것들은 아주 심각한! 단점이 있다. 잠깐 언급은 했지만 얘들은 단 하나의 값밖에 입력받지 못하고, 따라서 출력도 하나밖에 못한다. 그렇다면 여러개의 값을 출력하기 위한 방법은 없을까?

당연히 있다. 그것은 바로 InputStreamReader / OutputStreamWriter 이다. 바로 아래 코드를 통해 설명을 보겠다.

import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

/*
 * InputStreamReader 로 입력받는 경우에는 배열을 어떻게 주느냐에 따라 2개 이상의 값을 받을 수 있음
 */
InputStream in = System.in;
InputStreamReader reader = new InputStreamReader(in); // InputStreamReader 사용하기 위해 객체 생성
		
OutputStream out = System.out;
OutputStreamWriter writer = new OutputStreamWriter(out); // OutputStreamWriter 사용하기 위해 객체 생성
		
char cdata[] = new char[2]; // 이제는 char 를 기본형으로 받을 수 있고, 2개 이상의 값을 배열을 통해 받아올수 있다.
reader.read(cdata);
		
int IcData = cdata[0]-'0'; // 배열이기 때문에 char 로 받은 값을 int로 변환하여 계산하고 싶은 경우 이처럼 사용해야한다.
		
writer.write("입력받은 값 : ");
writer.write(cdata);
writer.write("\n");
writer.write("입력받은 첫번째 값 + 10 : ");
writer.write(IcData+10+"\n"); // 입력받은 첫번째 값 +10
		
System.out.println("#######결과#######");
writer.flush(); // 이 매서드를 통해 출력
writer.close();

결과

 

다시 한 번! 중요한 것은 다음과 같다.

- 기존 InputStream 에서 1개의 값밖에 받아올 수 없는 부분을 보완하기 위해 InputStreamReader 사용! 다만 InputStreamReader 를 통해서 2개 이상의 값을 받아오기 위해서는 배열을 사용해서 값을 받아와야 한다.

- 즉 InputStreamReader 는 고정적인 값 밖에 받아올 수 없다. 고정적인 값만 받아온다는 것은 내가 입력하는 값이 고정되어있는 값보다 작다면 그만큼 공간의 낭비가 생기게 되고, 고정되어있는 값보다 크다면 공간이 부족해지는 문제가 발생 한다. 이 부분은 결과 사진을 보면 알 수 있는데, 입력한 값은 123 이지만 입력받은 값은 12 로 나오면서 고정된 배열 2 이상은 받지 않았다는 것을 알 수 있다 => 한마디로 효율이 나쁘다

- char 타입을 기본형으로 하기 때문에 따로 캐스팅 할 필요는 없다. 다만 값을 int 로 변형하여 계산을 돌리고 싶은 경우는 위 코드처럼 바꿔 주어야 한다.

- 마지막에 writer.write() 로 여러번은 써준 것은 결국 저렇게 써놓아도 flush()에 가서 한번에!!!!!!!! 출력되기 때문이다.

 

What is Buffer?

여기까지 왔다면 이제 "버퍼가 무엇인가" 에 대해서 조금이지만 감을 잡은 사람이 있을 것이다. 결국 Buffer(이하 버퍼)는 위의 InputStream 과 InputStreamReader 를 보완하고 합쳐서 탄생한 입출력의 최종형태 정도로 받아드리면 된다(이 점은 코드로도 나타난다). 뭔가 디지몬의 성장기 - 성숙기 - 완전체 같은 느낌의...?

먼저 버퍼는 고정값이 아니라 가변적인 값을 받게 된다. 즉 5개의 값을 받으면 5개의 공간이 사용되며, 10개의 값을 받으면 10개의 공간이 사용되어 보다 효율적이 된다(다만 기본값이 없지는 않습니다)

동시에 버퍼는 입력받은 값은 버퍼에 저장해두었다가 버퍼가 가득차거나 개행 문자가 나타나면 버퍼의 내용을 한 번에 전송하게 된다(이 부분이 정말 중요하다)

위에는 Scanner 아래는 Buffer 라고 생각하면 된다.

버퍼의 가장 큰 장점은 중 하나는 바로 이렇게 한 번에 전송하는 방법을 통해 속도가 엄청나게 빨라진다고 한다. 근데 사실 말로만 들어서는 그냥 바로바로 입력받고 그때그때 출력하는 형식이 더 빠르지 않을까...? 라고 생각이 든다. 사실 우리도 이렇게 한번에 모아서 일을 진행하는 방법을 생활에서도 쓰고 있다.

예를 들어 이사를 가는 경우를 생각해보자. 우리가 이삿짐을 옮기기 위해 물건을 침대, 책상 부터 시작해서 하나하나 다른 집으로 옮기는 사람은 없을 것이다. 물건은 한 번에 담아(BufferedReader를 통해 입력받고) 차로 옮기고, 그 다음 이사할 집에 한번에 옮겨버리게 된다(BufferedWriter의 writer() 을 통해 저장했던 값을 한번에 출력한다) 이후 짐을 담는데 사용했던 가방이나 바구니들을 비우며 정리한다(flush() 를 사용하여 남아있는 값 출력 및 버퍼를 비운다).

결국 이러한 방식을 취하는 것은 양이 많을 경우 하나하나 옮기는 것보다 한번에 옮기는 게 빨라서이고, 이는 컴퓨터도 마찬가지이다. 컴퓨터에 입력받는 값이 많으면 많을 수록 Buffer 를 사용하여 데이터를 입출력 하는게 Scanner 를 통해 하나하나 출력하는 것보다 훨씬 빠르다.

 

다만 당연히 단점도 있다. 사실 단점이라고해도 큰 단점은 아니다.

1. 머저 아래 코드를 보면 확! 느껴지겠지만 처음 Buffer 를 사용하게 되면 코드가 살짝, 복잡하다고 느껴 질 수 있다. 내가 맨날 Scanner 만 사용해서 편하게 입력받고 print를 사용하여 출력해서 그런가 처음에 사용하기 어렵다고 느껴졌다.

2. 다음으로 띄어쓰기(스페이스)와 엔터(개행문자)를 경계로 입력 값을 인식하는 Scanner 과 달리 BufferReader는 엔터만 경계로 인식하기 때문에 중간에 띄어쓰기라도 하는 경우 데이터를 가공해주어야한다.

3. 마지막으로 Buffer 로 입력받는 기본 타입은 Stirng 이기 때문에 int 로 계산해야하는 경우에도 형변환이 필수적이된다.

 

- How to use Buffer?

Buffer는 2가지 형태로 코드를 확인해보도록 하겠다.

- Buffer 객체 생성 시에는 InputStream 과 InputStreamReader 의 합쳐진 형태를 취한다.

- Buffer 사용시에는 java.io.Buffered 패키지 임포트 뿐만 아니라 반드시!! 메인 메소드 뒤에 public static void main(String[] args) throws IOException {} 요런 식으로 throws IOException 을 사용하거나 try ~ catch 문을 넣어주어야 한다. 아니면 에러 뿜뿜 

import java.io.BufferedReader; // 임포트 필수
import java.io.BufferedWriter;

public static void main(String[] args) throws IOException { // 여기도 필수! 아니면 try ~ catch 사용해야함

	InputStream in = System.in;	
	InputStreamReader reader = new InputStreamReader(in);
		
	OutputStream out = System.out;
	OutputStreamWriter writer = new OutputStreamWriter(out);

	// 위의 4줄이 아래의 하나의 줄로 줄어든다.
	// 어떻게 줄어들었는지 확 감이 오지 않는가?
	BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
	BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
 }

 

다음으로 입출력하는 방법이다.

import java.io.BufferedReader; // 임포트 필수
import java.io.BufferedWriter;

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

	BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
	BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

	String s = br.readLine(); // bufferedwriter 의 기본형은 String
		
	int i = Integer.parseInt(s) +10; // String 을 int로 형변환 후 +10 !!
		
	br.close(); // bufferedreader 도 입력을 마쳤다면 닫아주자
		
	bw.write("입력받은 값 : "+ s); // 출력
	bw.newLine(); // 개행 메소드
	bw.write("입력받은 값 +10 : "+i+"\n"); // 이렇게 하니까 제대로 출력됨

	bw.flush(); // 남은 값 출력 && 버퍼 초기화
	bw.close(); // bufferedwriter 닫기
 }

요렇게 출력이 완료되었다

이제 마지막으로 Buffer 에 대한 정리를 해보겠다.

1. Buffer 사용시에는 java.io.Buffered 패키지 임포트 뿐만 아니라 반드시!! 메인 메소드 뒤에 public static void main(String[] args) throws IOException {} 요런 식으로 throws IOException 을 사용하거나 try ~ catch 문을 넣어주어야 한다. 아니면 에러 뿜뿜 

2. Buffer 는 가변적인 값을 받을 수 있으며, 입력받은 값을 Buffer 에 저장하고, Buffer 에 저장한 값을 한번에 출력하기 때문에 보다 빠른 속도를 자랑한다!

3. Buffer 의 기본 타입은 String 이며, 엔터(enter)를 경계로 값을 인식한다. 이 때문에 중간에 띄어쓰기(스페이스) 를 기준으로 값을 분리해야하는 경우 따로 데이터를 가공해주어야 한다.

4. BufferedWriter 을 사용해서 입력되었던 것들을 출력한다. 이후 flush()를 사용하게 된다. 사실 Buffer는 기본적으로는 버퍼가 꽉 차기 전에는 출력되지 않는데, flush() 를 통해 꽉 차지 않아도 buffer 내용을 (강제적으로)출력 후 버퍼를 비우게 된다.

 

+a 이거는 나만 그런지 아니면 어쩐지 모르겠는데, 이상하게도 String 으로 받은 값을 int 로 변환해서 바로 출력하면 아래처럼 코드를 짜고 바로 출력을 해버리면 오류가 발생하는 문제가 있었다.

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
		
String i = br.readLine();
br.close();
			
bw.write(Integer.parseInt(i));
bw.flush();
bw.close();

요렇게 이상하게 출력되었따

사실 지금도 왜 이렇게 출력되는지는...모르겠다. 분명 형변환도 제대로 한 것 같은데 이상하게도 저렇게 출력하면 출력에서 오류가 발생한다. 다만 앞에 문자열을 붙이거나 뒤에 개행문자(\n) 를 붙이는 경우 제대로 출력되었다.

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
		
String i = br.readLine();
br.close();
		
int ii = Integer.parseInt(i);
		
bw.write(Integer.parseInt(i)+10+ "\n"); // 요렇게 개행문자를 넣거나
bw.write("이렇게하면 출력 가능 : "+ (Integer.parseInt(i)+10));

출력도 심지어 계산도 잘된다

사실 이 부분 때문에 막혀서 더 오래 걸렸던 것 같다. 아마 나중에 좀 더 buffer 를 공부하면 좀 더 잘 알 수 있지 않을까..? 하는 바람이 있다.

- 자주 사용하는 BufferedReader / BufferedWriter 클래스의 메인 메소드

1. BufferedReader : 빨간색은 자주쓰는 메소드

타입 / 메소드 내용 설명
void Close() : Closes the stream and releases any system resources associated with it.
입력 스트림을 닫고 사용하던 자원들을 푼다
void mark(int, readAheadLimit) : Marks the present position in the system.
스트림의 현재 위치를 마킹한다
boolean markSupported() : Tells whether this stream supports the mark() operation, which it does.
스트림이 mark 기능을 지원하는지 true / false 로 알려준다
int read() : Reads a single character.
한 글자만 읽어 정수형으로 변환해준다. 즉 1을 '1'이라고 읽어서 '1'의 정수형(아스키코드)인 (int)1 = 49 을 출력한다.
String readLine() : Reads a line of text.
String 타입으로 한 줄을 읽어온다
boolean ready()  Tells whether this stream is ready to be read.
입력 스트림이 사용할 준비가 되었는지 확인한다. 1이면 준비 완료!
void reset() Resets the stream to the most recent mark.
마킹이 있으면 그 위치부터 다시 시작, 그렇지 않다면 처음부터 다시 시작!
long
skip(long n) Skips characters.
n개의 문자를 건너뛴다.

 

2. BufferedWriter : 빨간색은 자주쓰는 메소드 => 여기는 사실 다 중요하다.

타입 / 메소드 내용 설명
void flush() : Flushes the stream.
버퍼에 남은 값 출력 && 버퍼 초기화(비우기)
void Close() : Closes the stream and releases any system resources associated with it.
버퍼 끝내기. 끝내기 전에는 반드시!!! flush() 사용해야함
void newLine() : Writes a line separator.
버퍼에서 사용하는 개행 메소드
void write(int c) : Writes a single character
한 글자만 읽어 정수형으로 변환해준다. 즉 1을 '1'이라고 읽어서 '1'의 정수형(아스키코드)인 (int)1 = 49 을 출력한다.
위와 비슷한듯?

 

이렇게 자바 I/O, 스트림, 버퍼 에 대한 내용을 간략히 정리했다. 사실 개인적으로 버퍼만 3일정도 걸렸던 것 같다. 어쩔때는 위에 이야기했던 입력값이 제대로 출력이 안되는 경우 때문에 그렇기도 하고 처음 buffer 를 접하면서 I/O 가 무엇인지, 스트림이 무엇인지 전체적으로 한번 알아가면서 정리하려고 했기 때문이라고 생각한다.

 

다소 오래 걸리긴 했지만 분명 많은 것을 알았고, 앞으로는 Buffer 를 자주 이용해서 문제를 풀어볼 생각이다.

 

- 정리하면서 참고한 글

https://jhnyang.tistory.com/m/92

 

[Java 자바 입출력] BufferedReader/BufferedWriter

[자바 입출력 함수] BufferedReader / BufferWriter BufferedReader/BufferedWriter은 이름처럼 버퍼를 이용해서 읽고 쓰는 함수입니다. 이 함수는 버퍼를 이용하기 때문에 이 함수를 이용하면 입출력의 효율이..

jhnyang.tistory.com

 

https://m.blog.naver.com/haha7037/221893441579

 

Java 입출력(스트림), Buffer 개념설명

Java 입출력 스트림 자료의 입출력을 도와주는 중간 매개체 응용 프로그램과 입출력 장치를 연결하는 소프...

blog.naver.com

https://coding-factory.tistory.com/251

 

[Java] BufferedReader, BufferedWriter를 활용한 빠른 입출력

BufferedReader/BufferedWriter는 Buffer에 있는 IO 클래스입니다. 입력된 데이터가 바로 전달되지 않고 중간에 버퍼링이 된 후에 전달되됩니다. 출력도 마찬가지로 버퍼를 거쳐서 간접적으로 출력장치로

coding-factory.tistory.com

 

댓글