1. 스트림 ( Stream )

스트림이란 프로그램과 I/O 객체를 연결하여 데이터를 송수신 하는 길을 말합니다.

InputStream은 데이터를 읽어 들이는 객체이고, OutputStream은 데이터를 써서 보내는 객체입니다.


데이터를 어떤 방식으로 전달하느냐에 따라 스트림은 두가지로 나뉩니다.

  • 바이트 스트림( Byte Stream )
    • binary 데이터를 입출력하는 스트림입니다.
    • 이미지, 동영상 등을 송수신할 때 주로 사용합니다.
  • 문자 스트림( Character Stream )
    • 말 그대로 text 데이터를 입출력하는데 사용하는 스트림입니다.
    • HTML 문서, 텍스트 파일을 송수신할 때 주로 사용합니다.


자바에서는 스트림과 관련하여 추상 클래스를 지원하고 있습니다.

  • 바이트 스트림
    • InputStream / OutputStream
      • byte 기반 input / output stream의 최고 조상
    • ByteArrayInputStream / ByteArrayOutputStream
      • byte array( byte[] )에 대한 데이터를 입출력 하는 클래스
    • FileInputStream / FileOutputStream
      • 파일( File )에 대한 데이터를 입출력 하는 클래스
  • 문자 스트림
    • Reader / Writer
      • Character 기반 input / output stream의 최고 조상
    • FileReader / FileWriter
      • 문자 기반의 파일을 입출력 하는 클래스





2. 보조 스트림

보조 스트림이란 "프로그램에서" 파일을 읽기/쓰기 할 수 있도록 해주며, 위에서 소개한 클래스들은 주 스트림으로써 "외부에서" 파일 읽기/쓰기를 수행합니다.

다음 클래스들은 보조 스트림의 종류들입니다.

  • FilterInputStream / FilterOutputStream
    • byte 기반 보조 스트림의 최고 조상
  • BufferedInputStream / BufferedOutputStream
    • 입출력 효율을 높이기 위해 버퍼( byte[] )를 사용하는 보조스트림
  • BufferedReader / BufferedWriter
    • 입출력 효율을 높이기 위해 버퍼( char[] )를 사용하는 보조스트림
    • 라인 단위의 입출력에 용이
  • InputStreamReader / OutputStreamReader
    • byte 기반 스트림을 character 기반 스트림처럼 쓸 수 있도록 함
    • 인코딩 변환 가능


이 밖에도 Java에서 제공하는 스트림의 종류는 매우 많은데, 전부 다룰 수는 없을 것 같습니다.

이 글에서는 위에서 소개한 클래스들을 사용하는 예제들을 살펴보려고 합니다.





3. ByteArrayStream

public class IOExample {
public static void main(String[] args) {
byte[] src = {0, 1, 2, 3};
byte[] dest = null;

try{
InputStream is = new ByteArrayInputStream(src);
OutputStream os = new ByteArrayOutputStream();

int data = -1;
while( (data = is.read()) != -1 ){
os.write(data);
}

dest = ((ByteArrayOutputStream)os).toByteArray();
System.out.println(Arrays.toString(dest)); // [0, 1, 2, 3]
}
catch (IOException e){
e.printStackTrace();
}
}
}




4.  FileStream

public class FileCopy {
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;

try{
// ./는 현재경로를 의미합니다.
is = new FileInputStream("./asd.jpg");
os = new FileOutputStream("./sad.jpg");

int data = -1;
while( (data = is.read()) != -1 ){
os.write(data);
}
}
catch (FileNotFoundException e){
System.out.println("파일 없음");
e.printStackTrace();
}
catch (IOException e){
System.out.println("I/O 에러");
e.printStackTrace();
}
finally {
// 예외가 발생했을 때도 스트림을 닫아야 하므로 finally에서 스트림을 닫아줍니다.
try {
if( is != null ){
is.close();
}
if( os != null ){
os.close();
}
}
catch ( IOException e){
e.printStackTrace();
}
}
}
}




5. FileReader 

public class FileReaderTest {
public static void main(String[] args) {
Reader reader = null;
InputStream is = null;

try{
reader = new FileReader("./hello.txt");

int count = 0;
int data = -1;
while( (data = reader.read()) != -1 ){
count++;
System.out.println((char)data);
}
System.out.println("읽은횟수 : " + count);
}
catch (FileNotFoundException e){
System.out.println("파일 없음");
e.printStackTrace();
}
catch (IOException e){
System.out.println("I/O 에러");
e.printStackTrace();
}
finally {
// 예외가 발생했을 때도 스트림을 닫아야 하므로 finally에서 스트림을 닫아줍니다.
if( reader != null ){
try {
is.close();
}
catch ( IOException e){
e.printStackTrace();
}
}
}
}
}

기본적으로 Character 기반 스트림은 UTF-8 인코딩이 되어있습니다.





6. 데코레이션 패턴

객체를 장식하는 디자인 패턴으로 실행 중에 클래스를 덧붙이는 패턴입니다. ( 참고 )

데코레이터 패턴을 사용하면 부모 클래스를 건드리지 않으면서 실행중에 원하는 기능을 추가할 수 있습니다.

ex) Parent = new Child( new Child() );


보조스트림을 사용하기 위해서는 데코레이션 패턴을 사용합니다.

외부의 파일은 Byte 기반 스트림 ( 주 스트림 - FileInputStream )으로 읽고, Character 기반 스트림 보조스트림 – InputStream )으로 인코딩하여 읽어들입니다.

즉, 실제적인 파일 읽기를 수행하는 객체는 보조스트림인 InputStream입니다.

데코레이션 패턴이 중요한 것은 아니므로, 아래 예제를 통해 보조스트림을 사용하는 방법만 다루도록 하겠습니다.





7. BufferedOutputStream 

public class BufferedOutputStreamTest {
public static void main(String[] args) {
BufferedOutputStream bos = null;

try{
/* FileOutputStream으로 주 스트림을 생성하고, 보조스트림 BufferedOutputStream을 주 스트림에 할당합니다.
* 이것이 데커레이터 패턴이며, 파일을 읽어들이는 것은 주스트림인데, 실제로 파일을 다루는 것은 보조스트림이기 때문입니다.
*/
bos = new BufferedOutputStream(new FileOutputStream("./hello.txt"));

// hello.txt 파일에 문자 1~9까지 write
for (int i = 1; i <= 9; i++) {
bos.write(i);
}

// buffer가 끝나지 않은 상태에서 플러시를 강제로 하는 메서드입니다.
// 스트림을 close() 하면 자동으로 플러시됩니다.
// bos.flush();
}
catch (FileNotFoundException e){
System.out.println("파일 없음");
e.printStackTrace();
}
catch (IOException e){
System.out.println("I/O 에러");
e.printStackTrace();
}
finally {
// 예외가 발생했을 때도 스트림을 닫아야 하므로 finally에서 스트림을 닫아줍니다.
if( bos != null ){
try {
bos.close();
}
catch ( IOException e){
e.printStackTrace();
}
}
}
}
}





8. BufferReader

public class BufferReaderTest {
public static void main(String[] args) {
BufferedReader br = null;

try{
br = new BufferedReader(new FileReader("./src/io/BufferReaderTest.java"));

String line = null;
while( (line = br.readLine()) != null ){
System.out.println(line);
}
}
catch (FileNotFoundException e){
System.out.println("파일 없음");
e.printStackTrace();
}
catch (IOException e){
System.out.println("I/O 에러");
e.printStackTrace();
}
finally {
// 예외가 발생했을 때도 스트림을 닫아야 하므로 finally에서 스트림을 닫아줍니다.
if( br != null ){
try {
br.close();
}
catch ( IOException e){
e.printStackTrace();
}
}
}
}
}





9. InputStreamReader

public class InputStreamReaderTest {
public static void main(String[] args) {
Reader reader = null;

try{
/* file을 읽을 때 MS949로 인코딩해서 읽는 방식입니다.
* 인코딩을 변경하는 객체는 InputStreamReader라는 것을 전달하기 위해 MS949로 인코딩 했습니다.
*/
reader = new InputStreamReader(new FileInputStream("./hello.txt"), "MS949");

int data = -1;
while( (data = reader.read()) != -1 ){
System.out.println((char)data);
}
}
catch (UnsupportedEncodingException e) {
System.out.println("지원하지 않는 인코딩입니다.");
e.printStackTrace();
}
catch (FileNotFoundException e) {
System.out.println("파일 없음");
e.printStackTrace();
}
catch (IOException e) {
System.out.println("I/O 에러");
e.printStackTrace();
}
finally {
// 예외가 발생했을 때도 스트림을 닫아야 하므로 finally에서 스트림을 닫아줍니다.
if( reader != null ){
try {
reader.close();
}
catch ( IOException e){
e.printStackTrace();
}
}
}
}
}




10. File

public class PhoneList {
public static void main(String[] args) {
BufferedReader br = null;

/* File 객체는 예외가 발생하지 않음.
* File 객체는 스트림과 관련된 것이 아니라, 파일 정보에 대한 내용을 담고 있는 객체입니다.
*/
File file = new File("./hello.txt");
if (!file.exists()) {
System.out.println("파일이 존재하지 않습니다.");
return;
}

System.out.println(file.getName());
System.out.println(file.getAbsolutePath());
System.out.println(file.length());
System.out.println(new SimpleDateFormat("yyyy-mm-dd hh:mm:ss").format(file.lastModified()));
}
}





이상으로 자바에서 파일 입출력하는 다양한 클래스들과 그 예제들을 살펴보았습니다.


자바에서 입출력 프로그래밍의 핵심은 주스트림과 보조스트림을 구분하는 것입니다.

  • 주스트림
    • 파일을 읽음
    • Byte 기반 스트림
      • ByteArrayInputStream, FileInputStream, FilterInputStream
    • Character 기반 스트림
      • FileReader 
  • 보조스트림
    • 프로그램에서 파일을 읽거나 쓰는 주체

그 밖에 주 스트림과 보조 스트림을 구분하는 방법은 API 문서를 보고, 상속 관계를 확인하거나 생성자를 확인하여 구별하면 됩니다.