Programming-[Backend]/Java

자바 기초 강의 정리 - 5. 데이터 입출력(Stream, Reader, Writer)

컴퓨터 탐험가 찰리 2024. 6. 22. 08:00
728x90
반응형

인프런 얄코의 제대로 파는 자바 강의를 듣고 정리한 내용이다.

 

중요하거나 실무를 하면서 놓치고 있었던 부분들 위주로만 요약 정리한다.

자세한 내용은 강의를 직접 수강하는 것이 좋다.

https://www.inflearn.com/course/%EC%A0%9C%EB%8C%80%EB%A1%9C-%ED%8C%8C%EB%8A%94-%EC%9E%90%EB%B0%94/dashboard

 

 


 

1. 파일 다루기

 

기존 java.io.File 클래스는 멀티쓰레드 문제, 기능 한정, OS간 이식성 문제 등으로 사용하지 않고, java.nio.file 패키지의 기능들로 대체되었다.

 

1.1 Paths

nio 패키지 내의 클래스이다. 이전 버전의 File과는 다르게 Path를 String뿐만 아니라 Path라는 객체 자체로 다루면서 더 많은 기능을 제공한다. 내용 자체는 어렵지 않으므로 예제 코드와 주석을 보면서 이해하면 될 것 같다.

 

public class pathEx {

    public static final String CUR_PATH = "org/example";

    public static void main(String[] args) {

        //아무 값도 입력하지 않으면 프로젝트의 루트 디렉토리 경로를 표시함
        Path path = Paths.get("");
        Path absolutePath = path.toAbsolutePath();

        //루트 디렉토리에 입력한 문자열의 경로를 붙임(실제 파일이 없더라도)
        Path path2 = Paths.get("file.txt");
        Path absolutePath2 = path2.toAbsolutePath();

        // 인자들로 들어온 문자열들을 붙여서 경로로 만들어줌
        Path path3 = Paths.get(CUR_PATH, "sub1", "sub2");

        // 경로 이어 붙이기
        Path path4 = path3.resolve(path2);

        // 부모 경로
        Path path5 = path4.getParent();

        // path4 기준 path2까지의 상대경로를 구해줌
        Path path6 = path4.relativize(path2);

        // 맨 끝 파일 이름
        Path fileName = path4.getFileName();

        // 서브 경로
        Path subpath = path4.subpath(3, 5);
    }
}

 

 

 

1.2. Files

 위 파일의 메서드 내에 작성한다.

 

System.out.println(Files.exists(path2));

try {
    Files.createDirectory(
            Paths.get(CUR_PATH, "newFolder")
    );
} catch (IOException e) {
    System.out.println("폴더가 이미 있음");
}

// 중첩된 폴더 생성
try {
    Files.createDirectories(
            path4.getParent()
    );
    //폴더들을 미리 생성하고 파일 생성
    Files.createFile(path4);
} catch (IOException e) {
    System.out.println("파일 또는 폴더가 이미 있음");
}

try {
    Files.write(path4, "파일 써보기".getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
    throw new RuntimeException(e);
}

byte[] path4Bytes;
try {
    path4Bytes = Files.readAllBytes(path4);
} catch (IOException e) {
    throw new RuntimeException(e);
}

//문자열의 인자로 넣어서 바이트배열을 문자열로 출력해줌
String path4Contents = new String(path4Bytes);
System.out.println(path4Contents);

// 파일의 내용을 한줄씩 읽어서 출력
List<String> path4Read = null;
try {
    path4Read = Files.readAllLines(path4);
    path4Read.stream()
            .map(s -> "## " + s)
            .forEach(System.out::println);
} catch (IOException e) {
    throw new RuntimeException(e);
}


// Stream으로 받아올 수도 있음
// 한 줄씩만 받아오므로 효율적이고, 중단 시 스트림을 닫아야하므로 try-with-resources 적용
System.out.println("\n- - - - - \n");
try (Stream<String> lineStrm = Files.lines(path4)) {
    lineStrm
            .map(s -> "## " + s)
            .forEach(System.out::println);
} catch (IOException e) {
    throw new RuntimeException(e);
}

// 파일 복사
// 복사할 파일의 경로를 만들어서 copy 메서드 적용
Path copiedPath = Paths.get(
        path4.getParent().toString(), "copied.txt"
);

try {
    Files.copy(path4, copiedPath);
} catch (IOException e) {
    
}

 

Files.write, readAllBytes 방식은 간단한 내용을 쓸때만 적합하고, 대용량의 파일을 작성하는 방법으로는 적절하지 않다.

 

 

2.  I/O 스트림

 

2.1 InputStream, OutputStream

파일을 읽고 쓰는것 뿐만 아니라 네트워크 통신 및 다른 프로그램과의 소통 등에도 사용한다.

 

배열 인자를 넘겨주지 않고 그냥 1바이트씩 실행할 수도 있으나, 성능에 좋지 않아서 바이트 배열을 buffer로 인자로 넘겨주어 처리한다.

public static void fileInput3() {

    byte[] buffer = new byte[1024];

    Charset charset = StandardCharsets.UTF_8;

    try (FileInputStream fis = new FileInputStream(RELICS_PATH)) {
        int readByte;
        int count = 0;

        // 인자로 들어간 buffer 크기만큼 바이트를 읽어들임
        // 더 읽어올 것이 없으면 -1 반환
        while ((readByte = fis.read(buffer)) != -1) {
            String readStr = new String(buffer, 0, readByte, charset);
            System.out.printf("\n 🪐 - - - %d: %d - - - \n%n", ++count, readByte);
            System.out.println(readStr);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

넘겨준 바이트 배열의 크기만큼 잘려서 표시되는 것을 확인할 수 있다.

 

그런데 남은 부분을 1024로 만들기 위해서 0으로 채워버리는 성질이 있기 때문에 BufferedInputStream을 사용한다.

 

2.2 BufferedInputStream

내부적으로 8192 바이트에 해당하는 버퍼를 갖고 있다.

 

아래 방식은 일단 BufferedInputStream으로 8192 바이트 크기의 버퍼에 담아서 데이터를 가져온 다음, 1024 바이트의 버퍼에서 다시 한번 처리하는 방식이다.

public static void fileInput4() {

    Charset charset = StandardCharsets.UTF_8;

    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(RELICS_PATH))) {
        //BufferedInputStream의 8192 바이트용 버퍼에 일단 한번 담은 것을 내부 1024 바이트의 버퍼에서 담아서 처리함
        //BIS를 통해 데이터를 가져오므로 더 빠르고, 버퍼보다 적은 양의 데이터가 있으면 그만큼만 가져온다.
        byte[] buffer = new byte[1024];
        int readByte;
        int count = 0;

        while ((readByte = bis.read(buffer)) != -1) {
            String readStr = new String(buffer, 0, readByte, charset);
            System.out.printf("\n 🪐 - - - %d: %d - - - \n%n", ++count, readByte);
            System.out.println(readStr);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

 

2.3 BufferedOutputStream

Input과 크게 다른 점은 없다.

public static void fileOutput1() {
    var filePath = Paths.get(RELICS_PATH).getParent().resolve("new_file.txt").toString();
    Charset charset = StandardCharsets.UTF_8;

    List<String> lines = Arrays.asList(
            "가나다라",
            "마바사",
            "아자차카",
            "타파하"
    );

    try (
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(
                    new FileOutputStream(filePath)
            )
    ) {
        for (String line : lines) {
            byte[] buffer = (line + "\n").getBytes(charset);
            //처음 위치부터 버퍼의 끝 부분까지 데이터를 쓴다
            bufferedOutputStream.write(buffer, 0, buffer.length);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

 

2.4 DataOutputStream

특정 데이터들을 바이너리 데이터로 만들 필요가 있을 때 사용한다. .bin 확장자로 저장한다. FileOutputStream에 저장한 Stream을 DataOutputStream에 넘겨주는 방식으로 처리한다.

 

public static void fileOutput2() {
    String outputDataPath = Paths.get(RELICS_PATH).getParent().resolve("data.bin").toString();

    try (
            FileOutputStream fos = new FileOutputStream(outputDataPath);
            DataOutputStream dos = new DataOutputStream(fos)
    ) {
        dos.writeBoolean(true);
        dos.writeInt(1);
        dos.writeChar('a');
        dos.writeUTF("UTF 호환 문자열");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

 

반대로 DataInputStream을 통해서 값을 읽어올 수도 있다. 다만 바이트 데이터이기 때문에 읽어올 때 읽어오는 데이터 타입과 순서가 완벽하게 일치해야한다.

 

 

3. Reader, Writer

 

위 I/O Stream은 바이트 기반이였으나, Reader, Writer는 문자열 기반이다. 아래 코드는 기존 파일의 문자열들을 복사해서 새로운 파일에 복사하는 코드인데, buffer를 문자열 String line으로 받아와서 한 줄씩 처리하는 점이 위에서 살펴본 바이트기반과 다른 점이다.

 

public static void bufferedWriterTest() {
    Charset charset = StandardCharsets.UTF_8;

    try (
            FileReader fileReader = new FileReader(RELICS_PATH, charset);
            BufferedReader bufferedReader = new BufferedReader(fileReader);
            FileWriter fileWriter = new FileWriter(
                    RELICS_PATH.replace("relics", "relics_new"), charset
            );
            BufferedWriter bufferedWriter = new BufferedWriter(fileWriter)
    ) {
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
            bufferedWriter.write(line);
            bufferedWriter.newLine();
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

 

 

3.1 사용자 입력 받아서 파일 만들기

System.in 클래스를 넣어서 사용자의 입력을 받을 수 있다.

public static void userInput() {
        Charset charset = StandardCharsets.UTF_8;


        System.out.println("한 줄씩 입력. 종료 시 quit 입력");
        try (
                InputStreamReader ir = new InputStreamReader(System.in);
                BufferedReader br = new BufferedReader(ir);

                FileOutputStream fos = new FileOutputStream(RELICS_PATH);
                OutputStreamWriter ow = new OutputStreamWriter(fos, charset);
                BufferedWriter bw = new BufferedWriter(ow);
        ) {
            String line;
            while((line = br.readLine()) != null) {
                if (line.equalsIgnoreCase("quit")) break;

                bw.write(line);
                bw.newLine();
            }

        } catch (IOException e) {

        }
    }

 

 

3.2 StringReader, StringWriter

사용 중인 메모리 상에서 문자열을 다룰 때 사용한다. 대용량 문자열에 대한 텍스트 처리에 적합하다. StringWriter는 내부적으로 StringBuilder를 사용하여 문자열들을 이어붙인다.

 

public static void stringReaderWriter() {

    String input = "aqaqakaks akaskdasl";
    String output;

    try (
            StringReader sr = new StringReader(input);
            StringWriter sw = new StringWriter();
    ) {
        int c;
        while ((c = sr.read()) != -1) {
            System.out.print((char) c);
        }
        System.out.println();

        sw.write("writer에 들어갈");
        sw.write("  ");
        sw.write("문자열");

        output = sw.toString();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

    System.out.println(output);
}

 

 

이외에도 StringWriter에 출력을 하듯이 문자열을 더해줄 수 있는 PrintWriter도 있다.

 

 

728x90
반응형