새소식

Java

Java 파일 쓰기 인코딩 설정 - FileWriter, PrintWriter, BufferedWriter

  • -
반응형

개요 - Java 파일 쓰기

회사 업무를 하다가 Java로 json 파일을 생성해야하는 코드를 짜게 되었습니다. 회사 프로젝트 내에서 FileWriter, PrintWriter가 존재했고 찾아보니 BufferedWriter도 있다는 것을 알게 되어서 각각의 Wirter들은 어떠한 차이점이 있는지 한 번 알아보고 정리하기 위해 글을 작성하게 되었습니다.

 

FileWriter

JavaDoc 설명(Java8 기준)

Convenience class for writing character files. The constructors of this class assume that the default character encoding and the default byte-buffer size are acceptable. To specify these values yourself, construct an OutputStreamWriter on a FileOutputStream.

Whether or not a file is available or may be created depends upon the underlying platform. Some platforms, in particular, allow a file to be opened for writing by only one FileWriter (or other file-writing object) at a time. In such situations the constructors in this class will fail if the file involved is already open.

FileWriter is meant for writing streams of characters. For writing streams of raw bytes, consider using a FileOutputStream.
더보기

문자 파일 작성을 위한 편의 클래스입니다. 이 클래스의 생성자는 기본 문자 인코딩과 기본 바이트 버퍼 크기가 허용된다고 가정합니다. 이러한 값을 직접 지정하려면 FileOutputStream에 OutputStreamWriter를 생성하세요.

 

파일을 사용할 수 있는지 또는 생성할 수 있는지 여부는 기본 플랫폼에 따라 다릅니다. 특히 일부 플랫폼에서는 한 번에 하나의 FileWriter(또는 다른 파일 쓰기 개체)만 쓰기 위해 파일을 열 수 있습니다. 이러한 상황에서 관련 파일이 이미 열려 있으면 이 클래스의 생성자가 실패합니다.

 

FileWriter는 문자 스트림을 쓰기 위한 것입니다. 원시 바이트 스트림을 작성하려면 FileOutputStream 사용을 고려하세요.

 

사용법

    @Test
    @DisplayName("FileWriter 사용법")
    public void fileWriterTest() {
        /**
         * given
         */
        String filename = TEST_JSON_PATH + "/fileWriter-usage.json";

        // 무작위 값을 넣은 TestJson 생성
        TestJson testJson = new TestJson();
        testJson.setName(createRandomKorean(3));
        testJson.setAge(createRandomAge());
        testJson.setHobby(createRandomKorean(50));
        testJson.setDescription(createRandomKorean(300));

        // Object -> Json String 변경
        String testJsonStr = null;
        try {
            testJsonStr = objectMapper.writeValueAsString(testJson);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        /**
         * when
         */
        // 파일 쓰기
        FileWriter writer;
        try {
            writer = new FileWriter(filename);
            writer.write(testJsonStr);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        /**
         * then
         */
        // 파일 생성 여부 확인
        File fileWriterUsageFile = new File(filename);
        Assertions.assertThat(fileWriterUsageFile.exists()).isTrue();
    }
  • 사용법은 when 부분만 참고하시면 됩니다.
  • 참고로, Java8의 경우 FileWriter에서 인코딩 설정을 하지 못합니다. 위 JavaDocs에서도 나와있듯이 해당 클래스의 생성자는 기본 문자 인코딩(iso-8859-1)과 기본 바이트 버퍼 크기가 허용된다고 나와있으며, 이러한 설정을 직접하기 위해서는 OutputStreamWriter를 생성하라고 안내합니다.
  • 단, Java11의 경우 FileWriter에서 인코딩 설정이 가능하며 아래의 블로그를 참고하시면 됩니다.

 

PrintWriter

JavaDoc 설명(Java8 기준)

Prints formatted representations of objects to a text-output stream. This class implements all of the print methods found in PrintStream. It does not contain methods for writing raw bytes, for which a program should use unencoded byte streams.

Unlike the PrintStream class, if automatic flushing is enabled it will be done only when one of the println, printf, or format methods is invoked, rather than whenever a newline character happens to be output. These methods use the platform's own notion of line separator rather than the newline character.

Methods in this class never throw I/O exceptions, although some of its constructors may. The client may inquire as to whether any errors have occurred by invoking checkError().
더보기

개체의 형식화된 표현을 텍스트 출력 스트림에 인쇄합니다. 이 클래스는 PrintStream에 있는 모든 인쇄 메소드를 구현합니다. 여기에는 프로그램이 인코딩되지 않은 바이트 스트림을 사용해야 하는 원시 바이트를 쓰기 위한 메서드가 포함되어 있지 않습니다.


PrintStream 클래스와 달리 자동 플러시가 활성화되면 개행 문자가 출력될 때마다가 아니라 println, printf 또는 형식 메서드 중 하나가 호출될 때만 수행됩니다. 이러한 메서드는 개행 문자 대신 플랫폼 자체의 줄 구분 기호 개념을 사용합니다.

이 클래스의 메서드는 I/O 예외를 발생시키지 않지만 일부 생성자는 발생할 수 있습니다. 클라이언트는 checkError()를 호출하여 오류가 발생했는지 여부를 문의할 수 있습니다.

 

사용법

    @Test
    @DisplayName("PrintWriter 사용법")
    public void printWriterTest() {
        /**
         * given
         */
        String filename = TEST_JSON_PATH + "/printWriter-usage.txt";

        /**
         * when
         */
        // 파일 쓰기
        PrintWriter writer;
        try {
//            writer = new PrintWriter(filename, StandardCharsets.UTF_8); // Java11
            writer = new PrintWriter(filename, String.valueOf(StandardCharsets.US_ASCII)); // Java8
            for(int i=1; i<11; i++) {
                String data = i+" 번째 줄입니다.";
                writer.println(data);
            }
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        /**
         * then
         */
        // 파일 생성 여부 확인
        File printWriterUsageFile = new File(filename);
        Assertions.assertThat(printWriterUsageFile.exists()).isTrue();
        Assertions.assertThat(getEncoding(printWriterUsageFile)).isEqualTo(StandardCharsets.US_ASCII);
    }
  • 해당 사용법은 참고자료의 점프 투 자바 위키독스를 참고했습니다.
  • json 파일 생성의 관점으로만 봤을 때 FileWriter와의 차이를 알지 못했습니다. 위키독스를 통해 PrintWriter는 println이라는 메서드를 통해 줄바꿈을 조금 더 편리하게 할 수 있다는 차이를 알게 되었습니다.
  • BufferedWriter에 대해 작성하다가 추가적으로 알게된 내용이 생겼습니다. PrintWriter의 경우 아래의 코드 처럼 PrintWriter를 생성할 때 new BufferedWirter를 생성하는 것을 볼 수 있습니다. 개인적으로 PrintWriter가 더 가독성이 좋은 것 같습니다.
    • 인코딩 설정할 때 java8과 java11은 조금 다르니 위 코드를 참고하시기 바랍니다. 
/* Private constructor */
private PrintWriter(Charset charset, File file)
    throws FileNotFoundException
{
    this(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), charset)),
         false);
}

/**
 * Creates a new PrintWriter, without automatic line flushing, with the
 * specified file name and charset.  This convenience constructor creates
 * the necessary intermediate {@link java.io.OutputStreamWriter
 * OutputStreamWriter}, which will encode characters using the provided
 * charset.
 *
 * @param  fileName
 *         The name of the file to use as the destination of this writer.
 *         If the file exists then it will be truncated to zero size;
 *         otherwise, a new file will be created.  The output will be
 *         written to the file and is buffered.
 *
 * @param  csn
 *         The name of a supported {@linkplain java.nio.charset.Charset
 *         charset}
 *
 * @throws  FileNotFoundException
 *          If the given string does not denote an existing, writable
 *          regular file and a new regular file of that name cannot be
 *          created, or if some other error occurs while opening or
 *          creating the file
 *
 * @throws  SecurityException
 *          If a security manager is present and {@link
 *          SecurityManager#checkWrite checkWrite(fileName)} denies write
 *          access to the file
 *
 * @throws  UnsupportedEncodingException
 *          If the named charset is not supported
 *
 * @since  1.5
 */
public PrintWriter(String fileName, String csn)
    throws FileNotFoundException, UnsupportedEncodingException
{
    this(toCharset(csn), new File(fileName));
}

/**
 * Creates a new PrintWriter, without automatic line flushing, with the
 * specified file name and charset.  This convenience constructor creates
 * the necessary intermediate {@link java.io.OutputStreamWriter
 * OutputStreamWriter}, which will encode characters using the provided
 * charset.
 *
 * @param  fileName
 *         The name of the file to use as the destination of this writer.
 *         If the file exists then it will be truncated to zero size;
 *         otherwise, a new file will be created.  The output will be
 *         written to the file and is buffered.
 *
 * @param  charset
 *         A {@linkplain java.nio.charset.Charset charset}
 *
 * @throws  IOException
 *          if an I/O error occurs while opening or creating the file
 *
 * @throws  SecurityException
 *          If a security manager is present and {@link
 *          SecurityManager#checkWrite checkWrite(fileName)} denies write
 *          access to the file
 *
 * @since  10
 */
public PrintWriter(String fileName, Charset charset) throws IOException {
    this(Objects.requireNonNull(charset, "charset"), new File(fileName));
}

 

 

BufferedWriter

JavaDoc 설명(Java8 기준)

Writes text to a character-output stream, buffering characters so as to provide for the efficient writing of single characters, arrays, and strings.

The buffer size may be specified, or the default size may be accepted. The default is large enough for most purposes.

A newLine() method is provided, which uses the platform's own notion of line separator as defined by the system property line.separator. Not all platforms use the newline character ('\n') to terminate lines. Calling this method to terminate each output line is therefore preferred to writing a newline character directly.

In general, a Writer sends its output immediately to the underlying character or byte stream. Unless prompt output is required, it is advisable to wrap a BufferedWriter around any Writer whose write() operations may be costly, such as FileWriters and OutputStreamWriters. For example,

PrintWriter out
   = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));

will buffer the PrintWriter's output to the file. Without buffering, each invocation of a print() method would cause characters to be converted into bytes that would then be written immediately to the file, which can be very inefficient.
더보기

단일 문자, 배열 및 문자열을 효율적으로 쓸 수 있도록 문자를 버퍼링하여 문자 출력 스트림에 텍스트를 씁니다.
버퍼 크기를 지정하거나 기본 크기를 사용할 수 있습니다. 기본값은 대부분의 목적에 충분히 큽니다.

시스템 속성 line.separator에 정의된 대로 플랫폼 고유의 줄 구분 기호 개념을 사용하는 newLine() 메서드가 제공됩니다. 모든 플랫폼에서 줄 바꾸기 문자('\n')를 사용하여 줄을 종료하는 것은 아닙니다. 따라서 각 출력 행을 종료하기 위해 이 메소드를 호출하는 것이 개행 문자를 직접 쓰는 것보다 선호됩니다.

일반적으로 작성기는 출력을 기본 문자 또는 바이트 스트림으로 즉시 보냅니다. 프롬프트 출력이 필요하지 않은 경우 FileWriters 및 OutputStreamWriters와 같이 write() 작업에 비용이 많이 드는 Writer 주위에 BufferedWriter를 래핑하는 것이 좋습니다. 예를 들어,

  PrintWriter 출력
    = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));
 
PrintWriter의 출력을 파일에 버퍼링합니다. 버퍼링이 없으면 print() 메서드를 호출할 때마다 문자가 바이트로 변환되어 즉시 파일에 기록되므로 매우 비효율적일 수 있습니다.

 

사용법

    @Test
    @DisplayName("BufferedWriter 사용법")
    public void bufferedWriterTest() {
        /**
         * given
         */
        String filename = TEST_JSON_PATH + "/bufferedWriter-usage.txt";

        /**
         * when
         */
        // 파일 쓰기
        BufferedWriter writer;
        try {
            Writer out = new OutputStreamWriter(new FileOutputStream(filename), StandardCharsets.UTF_8);
            writer = new BufferedWriter(out);
            for(int i=1; i<11; i++) {
                String data = i+" 번째 줄입니다.";
                writer.write(data);
                writer.newLine();
            }
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        /**
         * then
         */
        // 파일 생성 여부 확인
        File bufferedWriterUsageFile = new File(filename);
        Assertions.assertThat(bufferedWriterUsageFile.exists()).isTrue();
        Assertions.assertThat(getEncoding(bufferedWriterUsageFile)).isEqualTo(StandardCharsets.UTF_8);
    }

    private static Charset getEncoding(File file) {
        String encoding = null;
        try {
            encoding = UniversalDetector.detectCharset(file);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        if (encoding == null) {
            throw new IllegalArgumentException("No encoding detected.");
        }

        return Charset.forName(encoding);
    }

 

성능비교

제 나름대로 각 Wiriter의 성능비교하는 테스트 코드를 작성했습니다만, 좋은 성능 비교 글들을 찾게 되어 참고하시기 발바니다.

비교한 Writer는 위에서 언급했던 FileWriter, PrintWriter, BufferedWriter 3가지 입니다. 또한, 랜덤한 한글과 숫자를 넣은 TestJson(데이터클래스) 10만 개를 넣은 리스트를 생성했습니다. 해당 리스트를 가지고 각 Writer로 10번씩 파일을 생성한 시간을 측정해 평균을 구했습니다.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class FileWriterTest {

    private List<TestJson> testJsons = new ArrayList<>();
    private static final String TEST_JSON_PATH = "C:/Workspace/java11-spring2.7.13-test/src/test/java/com/example/java11Spring2_7_13Test";
    private ObjectMapper objectMapper = new ObjectMapper();

    @BeforeAll
    @DisplayName("테스트 Json 생성")
    public void createTestJsons() {
        for(int i = 0; i < 100000; i++) {
            TestJson testJson = new TestJson();
            testJson.setName(createRandomKorean(3));
            testJson.setAge(createRandomAge());
            testJson.setHobby(createRandomKorean(50));
            testJson.setDescription(createRandomKorean(300));

            testJsons.add(testJson);
        }
    }

    private String createRandomKorean(int targetCount) {
        String randomKorean = "";
        for (int i = 1; i <= targetCount; i++) {
            char ch = (char) ((Math.random() * 11172) + 0xAC00);
            randomKorean += ch;
        }
        return randomKorean;
    }

    private int createRandomAge() {
        return (int) (Math.random() * 100) + 12;
    }
    
    @Test
    @DisplayName("각각의 Writer 10번씩 실행 후 평균 구하기")
    public void avg10Times() throws IOException {
        int fileWriterSpeedAvg = 0;
        for(int i = 0; i < 10; i++) {
            fileWriterSpeedAvg += fileWriterSpeed();
        }

        int printWriterSpeedAvg = 0;
        for(int i = 0; i < 10; i++) {
            printWriterSpeedAvg += printWriterSpeed();
        }

        int bufferedWriterSpeedAvg = 0;
        for(int i = 0; i < 10; i++) {
            bufferedWriterSpeedAvg += bufferedWriterSpeed();
        }

        System.out.println("fileWriterSpeedAvg : " + fileWriterSpeedAvg/10 + "ms");
        System.out.println("printWriterSpeedAvg : " + printWriterSpeedAvg/10 + "ms");
        System.out.println("bufferedWriterSpeedAvg : " + bufferedWriterSpeedAvg/10 + "ms");
    }

    private int fileWriterSpeed() throws IOException {

        // given
        String filename = TEST_JSON_PATH + "/fileWriter.json";
        String testJsonsStr = objectMapper.writeValueAsString(testJsons);
        long startTime = System.currentTimeMillis();

        // when
        FileWriter writer = new FileWriter(filename);
        writer.write(testJsonsStr);
        writer.close();

        // then
        return (int) (System.currentTimeMillis() - startTime);
    }

    private int printWriterSpeed() throws IOException {

        // given
        String filename = TEST_JSON_PATH + "/printWriter.json";
        String testJsonsStr = objectMapper.writeValueAsString(testJsons);
        long startTime = System.currentTimeMillis();

        // when
        PrintWriter writer = new PrintWriter(filename);
        writer.write(testJsonsStr);
        writer.close();

        // then
        return (int) (System.currentTimeMillis() - startTime);
    }

    private int bufferedWriterSpeed() throws IOException {

        // given
        String filename = TEST_JSON_PATH + "/bufferedWriter.json";
        String testJsonsStr = objectMapper.writeValueAsString(testJsons);
        long startTime = System.currentTimeMillis();

        // when
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename)));
        writer.write(testJsonsStr);
        writer.close();

        // then
        return (int) (System.currentTimeMillis() - startTime);
    }

    @Test
    @DisplayName("FileWriter 인코딩 설정 테스트")
    public void setEncodingByFileWriterTest() throws IOException {
        // given
        String filename = TEST_JSON_PATH + "/fileWriter-utf8.json";

        TestJson testJson = new TestJson();
        testJson.setName(createRandomKorean(3));
        testJson.setAge(createRandomAge());
        testJson.setHobby(createRandomKorean(50));
        testJson.setDescription(createRandomKorean(300));
        String testJsonStr = objectMapper.writeValueAsString(testJson);

        // when
        FileWriter writer = new FileWriter(filename, StandardCharsets.UTF_8);
        writer.write(testJsonStr);
        writer.close();
    }
}

여러번 실행 결과 BufferedWriter가 가장 속도가 빨랐습니다.

 

마치며

JavaDocs와 여러 블로그를 참고하며 머리속에서 정리가 잘 되지 않아 블로그에 정리를 했습니다. 확실히 하나의 글로 정리를 하니 보기만 했을 때와는 다르게 머리속에 정리가 조금 되는 것 같습니다. 조금 아쉬웠던 부분은 정리를 하면서도 새롭게 알게되는 사실들이 있어서 글이 조금 뒤죽박죽인 느낌이 있다는 점이 아쉽습니다. 또한 다른 블로그와는 다르게 테스트를 조금 러프하게 하지 않았나라는 생각을 하게 되었습니다. 그리고 테스트를 보면 그 지식의 깊이가 보이는 것 같아 반성하게 되네요. 감사합니다.

 

참고자료

 

그 외 관련 자료

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.