Daily Notebook

[Java] 파일 입출력 소화하기

by 배부른코딩로그
💡 매번 복붙하던 FILE I/O 관련 코드들을 총 정리하고 반복 숙달해보자!

목표

  • FILE I/O의 기본 개념을 설명할 수 있다.
  • FILE I/O 코드를 직접 작성하고 예제들을 정리하는 공간으로 활용할 수 있다.
  • FILE I/O 시 발생할 수 있는 예외들을 처리할 수 있다.

 

대학생 때는 이런 생각을 했다.

DB가 있는데, FILE I/O를 왜 사용할까? 파일과 관련된 코드들은 이제 사용하지 않겠구나!!

 

개발을 하면 할수록 완벽하게 어리석은 생각임을 알게되었다.

프로젝트 설정이나 파일 업/다운로드, REQ/RES, (S)FTP 등 DB가 아닌 File로 관리해야 할 상당히 많은 부분이 있음을 경험하고 있다. 그럼에도 불구하고, 파일을 제어할 때, 항상 구글을 애용하고 있다는 점 때문에 아예 싹 정리해볼까 한다.

 

기억이 안 날 때, 이 글을 참고 하기 위함이다.

 

FILE I/O 이것만은 기억하자!

파일 입출력은 무엇을 통해 이뤄질까?

파일에 쓸 데이터는 물론, 그 데이터를 보낼 통로가 있어야 한다. 그 반대의 경우도 마찬가지이다.

파일에 쓸 데이터가 키보드(Node1)를 통해 KEY IN 됐다고 하자. 그리고 파일(Node2)이 존재한다.

두 노드(키보드, 메모리, 파일 등) 사이를 연결하기 위한 무언가가 필요하고, 이를 스트림(Stream)이라고 한다.

 

스트림은 데이터를 운반하는데 사용되는 통로이다.

즉, 스트림을 통해 데이터가 줄줄이 흘러들어가고 흘러나오는 것이다.

 

스트림은 단방향 통신만 가능하기 때문에 입력과 출력을 하나의 스트림에서 처리하는 것은 불가능하다.

그래서, FILE I/O는 항상 두 개의 스트림이 필요하다.

 

처리할 데이터의 타입에 따라 사용하는 스트림이 나눠지는데,

바이트(byte) 기반의 스트림문자(character) 기반의 스트림이 존재한다.

바이트 기반의 스트림은 ~In/Out Stream.class이며, 문자 기반의 스트림은 ~Read/Write er.class이다.

 

솔직히 아무 생각없이 냅다 사용한 내 입장에서 '이렇게 구분되고 있었구나'하며, 충격 받은 내용이다.

 

바이트 스트림(Byte Streams)

바이트(8 bit) 단위의 바이너리한 데이터만을 처리하는 것이 바이트 스트림이라고 하고,

주로 이미지나 동영상 등을 송수신할 때 사용한다.

InputStream 및 OutputStream 클래스(추상)는 모든 바이트 스트림의 슈퍼 클래스이며,

바이트 스트림을 읽고 쓰는데 사용되다. 관련 클래스들의 네이밍 룰이 ~Stream.class임을 기억하자.

 

InputStream과 OutputStream는 추상 클래스이므로 new를 통해 객체 생성이 불가능하다.

이를 상속받아 구현된 '보조 스트림'을 활용할 수 있다. (위 그래프 참고)

 

아래의 코드들은 Java Docs에 기입되어 있는 내용 중 필요한 내용만 요약한 것이다.

VSCode나 IntelliJ에서 확인할 수 있는 내용이니, 에디터에서 Java Docs를 자세히 살펴보는 것도 중요하다.

 

InputStream

/**
 * Reads the next byte of data from the input stream.
 * The value byte is returned as an {@code int} in the range {@code 0} to {@code 255} (=1byte).
 * If no byte is available because the end of the stream has been reached, the value {@code -1} is returned.
 * 자식 클래스들이 구현해야할 추상 메서드이다.
 *
 * @return     the next byte of data, or 더 이상 읽을 값이 없다면 {@code -1}.
 * @throws     IOException  if an I/O error occurs.
 */
public abstract int read() throws IOException;

/**
 * byte b의 길이만큼 데이터를 input stream 으로부터 읽고, 
 * stores them into the buffer array {@code b}. 
 * The number of bytes actually read is returned as an integer.
 *
 * @param      b   the buffer into which the data is read.
 * @return     읽은 바이트 개수를 반환하거나, 
 *             더 이상 읽을 값이 없다면 {@code -1}.
 * @throws     IOException  If the first byte cannot be read for 
 *             any reason other than the end of the file, 
 *             if the input stream has been closed, or 
 *             if some other I/O error occurs.
 * @throws     NullPointerException  if {@code b} is {@code null}.
 */
public int read(byte b[]) throws IOException {
    return read(b, 0, b.length);
}

/**
 * Reads up to {@code len} bytes of data from the input stream into
 * an array of bytes.  An attempt is made to read as many as
 * {@code len} bytes, but a smaller number may be read.
 * The number of bytes actually read is returned as an integer.
 *
 * @param      b     the buffer into which the data is read.
 * @param      off   the start offset in array {@code b}
 *                   at which the data is written.
 * @param      len   the maximum number of bytes to read.
 * @return     the total number of bytes read into the buffer, or
 *             더 이상 읽을 값이 없다면 {@code -1}.
 * @throws     IOException If the first byte cannot be read for any reason
 *             other than end of file, or if the input stream has been closed,
 *             or if some other I/O error occurs.
 * @throws     NullPointerException If {@code b} is {@code null}.
 * @throws     IndexOutOfBoundsException If {@code off} is negative,
 *             {@code len} is negative, or {@code len} is greater than
 *             {@code b.length - off}
 * @see        java.io.InputStream#read()
 */
public int read(byte b[], int off, int len) throws IOException {
    ...
}

/**
 * The maximum size of array to allocate.
 * Some VMs reserve some header words in an array.
 * Attempts to allocate larger arrays may result in
 * OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

/**
 * Closes this input stream and 
 * releases any system resources associated with the stream.
 *
 * @throws IOException  if an I/O error occurs.
 */
public void close() throws IOException {}

OutputStream

/**
 * Writes the specified byte to this output stream. The general
 * contract for {@code write} is that one byte is written
 * to the output stream. 
 * {@code b} to be written is the eight low-order bits.
 * The 24 high-order bits of {@code b} are ignored.
 * 자식들이 구현해야 할 추상 메서드이다.
 *
 * @param      b   the {@code byte}.
 * @throws     IOException  if an I/O error occurs. In particular,
 *             an {@code IOException} may be thrown if the
 *             output stream has been closed.
 */
public abstract void write(int b) throws IOException;

/**
 * Writes {@code b.length} bytes from 
 * the specified byte array to this output stream.
 *
 * @param      b   the data.
 * @throws     IOException  if an I/O error occurs.
 * @see        java.io.OutputStream#write(byte[], int, int)
 */
public void write(byte b[]) throws IOException {
    write(b, 0, b.length);
}

/**
 * Writes {@code len} bytes from the specified byte array
 * starting at offset {@code off} to this output stream.
 * {@code b}에 저장된 데이터 중 off 위치부터 len 개를 읽어 노드에 쓴다.
 *
 * @param      b     the data.
 * @param      off   the start offset in the data.
 * @param      len   the number of bytes to write.
 * @throws     IOException  if an I/O error occurs. In particular,
 *             an {@code IOException} is thrown if the output
 *             stream is closed.
 */
public void write(byte b[], int off, int len) throws IOException {
    ...
}

/**
 * Flushes this output stream and forces any buffered output bytes
 * to be written out.
 * 출력 스트림에 있는 모든 데이터를 노드에 출력하고 버퍼를 비운다.
 *
 * @throws     IOException  if an I/O error occurs.
 */
public void flush() throws IOException {}

/**
 * Closes this output stream and 
 * releases any system resources associated with this stream. 
 * A closed stream cannot perform output operations and cannot be reopened.
 *
 * @throws     IOException  if an I/O error occurs.
 */
public void close() throws IOException {}

 

캐릭터 스트림(Character Streams)

유니코드(16 bit) 단위의 데이터만을 처리하는 것이 캐릭터 스트림이라고 하고,

주로 텍스트 파일이나 HTML 문서 등을 송수신할 때 주로 사용한다.

Reader 및 Writer 클래스(추상)는 모든 문자 스트림 클래스의 슈퍼 클래스이며,

문자 스트림을 읽고 쓰는데 사용되고, 관련 클래스들의 네이밍 룰은 ~er.class임을 기억하자.

 

아래의 코드 역시 Java Docs 내용 중 필요한 내용만 요약한 것이다.

 

Reader

private static final int TRANSFER_BUFFER_SIZE = 8192;

/**
 * Reads a single character.  This method will block until a character is
 * available, an I/O error occurs, or the end of the stream is reached.
 *
 * @return     The character read, as an integer in the range 0 to 65535
 *             ({@code 0x00-0xffff}), or -1 if the end of the stream
 *
 * @throws     IOException  If an I/O error occurs
 */
public int read() throws IOException {
    ...
}

/**
 * Reads characters into an array.  This method will block until some input
 * is available, an I/O error occurs, or the end of the stream is reached.
 *
 * @param       cbuf  Destination buffer
 *
 * @return      The number of characters read, or -1
 *              if the end of the stream
 *
 * @throws      IOException  If an I/O error occurs
 */
public int read(char cbuf[]) throws IOException {
    return read(cbuf, 0, cbuf.length);
}

/**
 * Reads characters into a portion of an array.  This method will block
 * until some input is available, an I/O error occurs, or the end of the
 * stream is reached.
 *
 * @param      cbuf  Destination buffer
 * @param      off   Offset at which to start storing characters
 * @param      len   Maximum number of characters to read
 *
 * @return     The number of characters read, or -1 if the end of the
 *             stream has been reached
 *
 * @throws     IOException  If an I/O error occurs
 * @throws     IndexOutOfBoundsException
 *             If {@code off} is negative, or {@code len} is negative,
 *             or {@code len} is greater than {@code cbuf.length - off}
 */
public abstract int read(char cbuf[], int off, int len) throws IOException;

Writer

/**
 * Size of writeBuffer, must be >= 1
 */
private static final int WRITE_BUFFER_SIZE = 1024;

/**
 * Writes a single character.  The character to be written is contained in
 * the 16 low-order bits of the given integer value; the 16 high-order bits
 * are ignored.
 *
 * @param  	c     int specifying a character to be written
 *
 * @throws  IOException   If an I/O error occurs
 */
public void write(int c) throws IOException {
    synchronized (lock) {
        ...
    }
}

/**
 * Writes an array of characters.
 *
 * @param  	cbuf  Array of characters to be written
 * @throws  IOException  If an I/O error occurs
 */
public void write(char cbuf[]) throws IOException {
    write(cbuf, 0, cbuf.length);
}

/**
 * Writes a portion of an array of characters.
 *
 * @param  	cbuf  Array of characters
 * @param  	off   Offset from which to start writing characters
 * @param  	len   Number of characters to write
 * @throws  IndexOutOfBoundsException
 *          Implementations should throw this exception
 *          if {@code off} is negative, or {@code len} is negative,
 *          or {@code off + len} is negative or greater than the length
 *          of the given array
 * @throws  IOException  If an I/O error occurs
 */
public abstract void write(char cbuf[], int off, int len) throws IOException;

/**
 * Writes a string.
 *
 * @param  	str   String to be written
 *
 * @throws  IOException  If an I/O error occurs
 */
public void write(String str) throws IOException {
    write(str, 0, str.length());
}

/**
 * Writes a portion of a string.
 *
 * @param  	str   A String
 * @param  	off   Offset from which to start writing characters
 * @param  	len   Number of characters to write
 *
 * @throws  IndexOutOfBoundsException
 *          Implementations should throw this exception
 *          if {@code off} is negative, or {@code len} is negative,
 *          or {@code off + len} is negative or greater than the length
 *          of the given string
 *
 * @throws  IOException
 *          If an I/O error occurs
 */
public void write(String str, int off, int len) throws IOException {
    synchronized (lock) {
        ...
    }
}

/**
 * Appends the specified character sequence to this writer.
 *
 * @param  	csq   The character sequence to append.  If {@code csq} is
 *         {@code null}, then the four characters {@code "null"} are
 *         appended to this writer.
 * @return  This writer
 * @throws  IOException  If an I/O error occurs
 * @since  1.5
 */
public Writer append(CharSequence csq) throws IOException {
    write(String.valueOf(csq));
    return this;
}

/**
 * Appends a subsequence of the specified character sequence to this writer.
 * {@code Appendable}.
 *
 * @param  	csq  The character sequence from which a subsequence will be
 *         	appended.  If {@code csq} is {@code null}, then characters
 *         	will be appended as if {@code csq} contained the four
 *         	characters {@code "null"}.
 * @param  	start The index of the first character in the subsequence
 * @param  	end   The index of the character following 
 *         	the last character in the subsequence
 * @return  This writer
 * @throws  IndexOutOfBoundsException
 *          If {@code start} or {@code end} are negative, {@code start}
 *          is greater than {@code end}, or {@code end} is greater than
 *          {@code csq.length()}
 * @throws  IOException   If an I/O error occurs
 * @since  1.5
 */
public Writer append(CharSequence csq, int start, int end) throws IOException {
    if (csq == null) csq = "null";
    return append(csq.subSequence(start, end));
}

/**
 * Appends the specified character to this writer.
 *
 * @param  	c   The 16-bit character to append
 * @return  This writer
 * @throws  IOException  If an I/O error occurs
 * @since 1.5
 */
public Writer append(char c) throws IOException {
    write(c);
    return this;
}

/**
 * Flushes the stream.  If the stream has saved any characters from the
 * various write() methods in a buffer, write them immediately to their
 * intended destination.  Then, if that destination is another character or
 * byte stream, flush it.  Thus one flush() invocation will flush all the
 * buffers in a chain of Writers and OutputStreams.
 *
 * @throws  IOException  If an I/O error occurs
 */
public abstract void flush() throws IOException;

/**
 * Closes the stream, flushing it first. Once the stream has been closed,
 * further write() or flush() invocations will cause an IOException to be
 * thrown. Closing a previously closed stream has no effect.
 *
 * @throws  IOException  If an I/O error occurs
 */
public abstract void close() throws IOException;

 

FILE I/O 예제

예제에서는 다음과 같은 내용이 기입된 파일을 활용할 것이다.

파일 내에는 숫자, 영문자, 한글, 한문, 이모지 등이 존재한다.

0123
START
경기도 
多
수원시 
✔
영통구
小
END
9876
final String FILE_NAME = "SAMEPLE.XXX";

 

File file = new File(FILE_NAME);

 

Byte Streams

InputStream

파일을 읽는 가장 기본적인 방법이다. 파일을 byte 단위로 한 글자씩 읽어들인다.

String readString = "";
try (InputStream in = new FileInputStream(file)) {
    int data = 0;
    while ((data = in.read()) != -1) {
        readString += (char) data;
    }
    in.close();
}
System.out.println(readString);
/*
 * 0123
 * START
 * °æ±???
 * ??
 * ¼?¿ø½?
 * ?
 * ¿???±¸
 * ?³
 * END
 * 9876
 */

결과값은 보면, 영숫자를 제외한 문자들은 전부 깨지는 것을 볼 수 있다.

이 부분이 위에 설명한 1byte 씩 읽을 때 발생하는 문제점이다.

한글과 한문, 이모지 등은 2~3bytes이기 때문에 깨지게 되는 것이다.

 

BufferedInputStream

그렇다면, 바이트 스트림은 1byte 문자만 읽을 수 있는 것일까?
NO!

바이트 스트림도 버퍼(Buffer)를 이용하면, 성능은 물론 2bytes 이상의 문자도 읽을 수 있게 된다.

byte[] readBytes;
try (InputStream in = new BufferedInputStream(new FileInputStream(file), BUFFER_SIZE)) {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] buffer = new byte[BUFFER_SIZE];
    int length = 0;
    while ((length = in.read(buffer)) != -1) {
        out.write(buffer, 0, length);
    }

    readBytes = out.toByteArray();

    out.flush();
    out.close();
    in.close();
}
System.out.println(new String(readBytes));
/*
 * 0123
 * START
 * 경기도
 * 多
 * 수원시
 * ?
 * 영통구
 * 小
 * END
 * 9876
 */

결과값은 보면, 이번에는 한글과 한문은 정상적으로 읽어졌으나, 이모지가 여전히 깨져있는 것을 볼 수 있다.

이모지와 같은 유니코드 심벌(Unicode symbol)UTF-16 포맷의 2개의 문자로 표현된다.

 (✔ 1 char is 2 bytes)

 

문득 궁금해서 이모지를 파일 내에 기입했는데, 읽어지지 않아 신기하여 찾아보는 중이다 : )

 

Character Streams

BufferedReader

// 파일 입력
try (FileInputStream fileInputStream = new FileInputStream(file)) {
    InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
    BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

    String readString = "";

    String str;
    while ((str = bufferedReader.readLine()) != null) {
        readString += bufferedReader.readLine() + "\n";
    }

    System.out.println(readString);
}
/*
 * 0123
 * START
 * 경기도
 * 多
 * 수원시
 * ?
 * 영통구
 * 小
 * END
 * 9876
 */

여전히 이모지 데이터는 읽어지지 않는다.

어떻게 하면 읽을 수 있는까..ㅎㅎ

 

BufferedWriter

// 파일 출력
// FileOutputStream fileOutputStream = new FileOutputStream(filePath);
// OutputStreamWriter OutputStreamWriter = new OutputStreamWriter(fileOutputStream);
// BufferedWriter bufferedWriter = new BufferedWriter(OutputStreamWriter);

쓰기는 그냥 반대로 제공해주는 메서드만 쓰면 된다 ㅎㅎ..

 

마무리

개발하면서 신박하거나 유용한 예제라고 생각되면 꾸준히 추가할 예정이다.

File I/O도 잘 다뤄야 생산성이 크게 올라간다!!!

 

 

 

출처

 

Since. 2022. 06. 02.

Last Updated. 2022. 06. 09.

 

반응형

'Java' 카테고리의 다른 글

[Java] Properties 한글 깨짐 이슈  (0) 2022.10.25
[Java] 특정 디렉토리 및 파일 모니터링  (0) 2022.06.10
[Java] Lombok 소화하기  (0) 2022.06.07
[Java] Null Safe한 자바 컬렉션 정렬  (0) 2022.05.24
[Java] Enum 소화하기  (0) 2022.05.09

블로그의 정보

배부른코딩로그

배부른코딩로그

활동하기