[Java/자바] 5. 입출력

입출력 java.io

자바는 입출력 스트림을 통해 다양한 경로로의 입출력을 구현하는 일관된 방식 제공

입출력 스트림

스트림(Stream)이란 순서가 있는 일련의 데이터를 의미한다.

입출력 스트림

  • Source: 키보드, 파일, 네트워크 등 데이터 근원지
  • Input Stream: 근원지에서 흘러 들어오는 데이터
  • Destination: 모니터, 파일, 네트워크 등 데이터 목적지

특징

  • 순차적인 데이터의 흐름으로 데이터의 무작위적인 접근이 불가능
  • 단방향의 흐름으로 입력 스트림과 출력 스트림이 따로 존재
  • 모든 데이터의 입출력이 근원지, 목적지의 형태와 관계없이 일정한 형태로 전송된다.

∴ 동일한 방법으로 프로그램이 작성될 수 있는 유연한 구조의 API를 가진다.

👉 자바는 입출력을 위해 스트림을 생성하고 다루는 클래스들을 java.io 패키지를 통해 제공

java.io 패키지

자바로 입출력 기능을 구현하는 프로그램을 개발하는 데에 필요한 다양한 클래스를 포함하는 패키지

File 클래스

  • 입출력 프로그램의 기본은 파일과 관련된 데이터의 입출력이기 때문에 파일을 제어하기 위한 클래스를 제공
  • File 클래스로 파일, 디렉토리를 모두 표현하고 관리할 수 있다.

    • 파일이 실제로 존재하는 지 알 수 있다.
    • 파일 복사 가능
    • 파일의 이름 변경 가능
  • File 클래스 자체에서는 파일의 데이터를 입출력하기 위한 메소드는 제공하지 않음 - 입출력은 스트림 클래스를 기반으로 수행
  • ❗️File 클래스로부터 생성된 객체는 변경할 수 없다. 즉, File 객체에 의해 표현되는 추상 경로명은 절대로 변하지 않는다.
  • 파일 클래스로부터 객체를 생성하는 일반적인 방법

    //C:₩Program Files 디렉토리에 해당하는 파일 객체 생성
    File file = new File("C:₩Program Files");
    
    //현재 작업 디렉토리에 해당하는 파일 객체 생성. '.'은 현재 작업 디렉토리 의미
    File file = new File(".");
    
    //현재 작업 디렉토리 밑에 위치한 파일에 대한 파일 객체 생성
    File file = new File("./systemLog.log");
  • 주요 메소드

    • boolean delete(): 폴더가 비어있다면, 파일이나 폴더를 삭제
    • boolean exists(): 파일의 존재여부 return
    • String getAbsolutePath(): 파일의 절대 경로를 문자열로 return
    • String getName(): 파일이나 폴더의 이름 return
    • boolean isDirectory(): 폴더인지의 여부 return

RandomAccessFile 클래스

파일에 무작위적인 접근을 가능하게 함

Stream 클래스

입출력 스트림을 가능하게 함

  • 분류

    • 입출력 단위에 의한 분류

      • 바이트 단위의 입출력 클래스
      • 캐릭터 단위의 입출력 클래스
    • 데이터 이동 통로에 의한 분류

      • 1차 스트림 클래스(데이터가 이동하는 통로를 직접 만드는 클래스)
      • 2차 스트림 클래스(이미 만들어져 있는 통로에 새로운 기능을 더하는 클래스)

    즉, 자바에서 입출력에 사용하는 스트림 클래스의 형태는 크게 4가지로 분류할 수 있다.
    ❗️바이트 단위와 캐릭터 단위는 서로 호환하지 않는 것을 원칙으로 한다.

바이트 & 캐릭터 단위 입출력

바이트 스트림

  • 스트림 클래스

    • 바이트 스트림: 8비트의 바이트를 읽고 쓰기 위한 스트림
    • 문자 스트림: 16비트 문자나 문자열들을 읽고 쓰기 위한 스트림
  • 모두 바이트 단위로 입출력을 수행한다는 공통적인 특징과 함께 각 클래스들만의 고유한 기능을 가진다.
  • 어떤 클래스는 입출력 장치를 대상으로 직접 입출력을 하는 반면, 다른 클래스를 대상으로 입출력을 하는 클래스도 존재함
  • Java에서는 두 개 이상의 클래스를 연결한 입출력 스트림을 생성해서 입출력을 하는 경우가 매우 일반적이다.

    try{
        FileOutputStream output = new FileOutputStream("message.txt");
        BufferedOutputStream buffOutput = new BufferedOutputStream(output);
        buffOutput("Message Output..");
    }catch(IOException e){
        e.printStackTrace();
    }

InputStream 클래스

  • 바이트 단위 입력 스트림의 최상위 클래스로 추상 클래스로 정의되어있기 때문에 스스로 객체화 될 수 없다.

∴ InputStream을 상속한 자식 클래스는 InputStream 클래스에 존재하는 모든 추상 메소드를 적절하게 오버라이딩해야한다. & InputStream 클래스를 상속한 자식 클래스의 객체를 생성하여 입력 관련 로직을 구현 가능

  • 주요 메소드

    • void close() throws IOException: 입력 스트림 닫기
    • int read() throws IOException: 입력 스트림에서 한 바이트를 읽어서 int 값으로 return

      • 데이터를 읽어들이는 기능 제공
      • 입력 스트림에서 하나의 바이트를 읽어들임
      • EOF를 만나면 -1을 return하여 읽기 작업이 끝났다는 것을 알려준다.
    • int read(byte buf[]) throws IOException: 입력 스트림에서 buf[] 크기만큼을 읽어 buf에 저장하고, 읽은 바이트 수를 return

      • 사용자가 지정한 byte[]를 이용하여 한꺼번에 원하는 양을 읽어들일 수 있음
      • 일반적으로 available()메소드를 사용하여 스트림에서 읽을 수 있는 바이트 수를 얻은 후 이를 기준으로 byte[]를 생성하며 한꺼번에 읽을 수 있음

이중 가장 중요한 메소드는 read() 메소드

EOF(End Of File)이란?
파일의 끝을 의미한다. 윈도우에서는 Ctrl-Z, 리눅스나 유닉스에서는 Ctrl-D를 사용한다. 이 값을 -1로 표현할 수 있다. 하지만 read()의 리턴값은 int이다. 그래서 양수를 기준으로 표현하는데 0~127까지만 사용할 수 있다. 이를 해결하기 위해 더 큰 자료형을 리턴으로 사용하여 0~255까지 사용할 수 있게 하고, -1을 특수한 입력 값으로 처리하도록 구성하기 위해 int형을 return 값 형식으로 사용하는 것이다.

OutputStream 클래스

  • 바이트 단위의 출력을 대표하는 최상위 클래스
  • InputStream 클래스와 마찬가지로 이 클래스에 정의되어 있는 메소드는 자식 클래스가 상속 후 그대로 사용 또는 오버라이딩 해야한다.
  • 주요 메소드

    • void close() throws IOException: 출력 스트림 닫기
    • void flush() throws IOException: 버퍼에 남은 출력 스트림 출력
    • void write(int i) throws IOException: 정수 i의 하위 8비트 출력
    • void write(byte buf[]) throws IOException: buf의 내용 출력

이중 가장 중요한 메소드는 write(int i) 메소드

  • 1바이트를 출력하는 메소드로서, 메소드의 인자도 바이트가 아닌 정수형을 사용
  • write(int i)메소드에는 세 개의 메소드가 오버로딩됨

    • 인자로 byte[]를 사용하는 것
    • 배열과 함께 시작 위치 및 크기를 지정하는 메소드
    • 출력이 끝났음을 알려주는 flush()메소드

      • 버퍼는 일종의 완충지대로 입출력을 조금 더 빨리 할 수 있게 도와주는 역할
      • 모든 출력은 도착 지점으로 바로 나가지 않고 먼저 버퍼에 쌓인다.
      • 버퍼에 데이터가 충분히 쌓인 후 flush()명령을 받으면 현재 버퍼에 있는 모든 내용을 도착 지점으로 내보내고 버퍼를 비운다.
      • flush()메소드를 호출하지 않으면 버퍼로만 출력되기 때문에 실제로 도착 지점에서 데이터를 받지 못하는 경우 발생 ∴ 일반적인 출력 스트림에서는 데이터를 출력하고나면 자동으로 flush()메소드를 호출할 수 있는 기능을 제공하는 경우가 많다.

표준 입출력 in java.lang 패키지

  • 표준 입력: 사용자가 키보드를 통해 입력한 데이터를 읽어들이는 작업
  • 표준 출력: 프로그램의 수행 결과 메세지가 콘솔 창에 출력되는 것

👉 자바에서는 표준 입출력 기능을 System 클래스에서 제공

∵ 모든 프로그램에서 기본적으로 표준 입출력을 사용할 수 있도록 하기 위해서이다.

표준 입력

  • System클래스의 in이라는 클래스 변수로 제공된다.
  • System.in의 타입은 InputStream이다.
  • System.in

    • 입력 스트림 객체를 참조하며 이를 통해 키보드 입력을 처리할 수 있다.
    • 변수의 타입은 InputStream 클래스지만 실제 참조하는 객체는 InputStream 자식 객체.
    • 자연스러운 형변환으로 인해 가능
    • JVM이 메모리로 올라오면서 미리 객체를 생성해놓는 대표적인 객체
    • 영문과 한글의 처리를 분리해서 구성해야 제대로 인식함

      //키보드 입력을 위한 구문 System.in.read();

표준 출력

  • System.out 표준 출력 장치 객체
  • 다양한 타입의 데이터를 출력하기 위한 System.out.println(), System.out.print() 등의 메소드가 데이터 타입 별로 오버로딩 되어있음.

FileInputStream 클래스

InputStream 클래스를 상속한 자식 클래스로 하드디스크 상에 존재하는 파일로부터 바이트 단위의 입력을 처리하는 클래스. 즉, 스트림을 생성하는 클래스

  • 생성자의 인자로 File객체를 주거나, 파일의 이름을 직접 String 형태로 줄 수 있다.
  • 파일 이름을 String 형태로 주는 경우가 많은데 이때 파일이 존재하지 않을 가능성도 있으므로 FileNotFoundException에 대한 예외 처리를 해야한다.

FileOutputStream 클래스

OutputStream 클래스를 상속한 자식 클래스로 파일로 바이트 단위의 출력을 처리하는 클래스

버퍼를 이용한 파일 입출력

보다 효율적인 프로그램 작성을 위해서는, 기계적인 동작의 횟수를 줄이는 것이 가장 좋은 방법이다. 입출력 횟수를 줄이면 프로그램의 효율을 높일 수 있고 더 빠른 프로그램으로 개선할 수 있다.

🗝 1바이트씩 읽어들이는 read()메소드 👉 한꺼번에 많은 데이터를 바이트의 배열로 읽어들이는 read(byte[])메소드를 사용하는 것이 좋다!

//읽어들일 가용량을 파악하여 size를 지정
int size = input.available(); 
// 입출력을 최소화하기 위해 한 번의 입력으로 파일의 모든 내용을 버퍼로 가져온 후,
// 한번의 출력으로 다른 파일로 저장

📍버퍼의 크기가 무조건 파일 크기만큼 크면 성능이 좋아질까?
❌ 버퍼의 크기가 아주 작으면 효율은 급격히 떨어지고, 버퍼의 크기가 크다면 메모리 낭비를 유발할 수 있다. 또한 파일의 크기가 예상하는 것보다 훨씬 큰 경우 충분한 메모리를 준비할 수 없는 상황도 발생 가능

∴ 버퍼의 크기는 적정선에서 만족할 수 있도록 잡는 것이 필요(일반적으로 1024, 2048, 4096 같이 배수 형식으로 준비하는 것이 일반적)

BufferedInputStream / BufferedOutputStream
  • 사용자가 일일이 버퍼를 지정하여 입출력하는 것보다 조금 더 편리하게 입출력할 수 있는 방법
  • 2차 스트림의 일종으로, 이미 존재하고 있는 스트림에 새로운 기능을 추가하는 형식으로 구성

FileInputStream: 1차 스트림 BufferedInputStream: 2차 스트림

1차 스트림 - 2차 스트림 관계

👍 BufferedInputStream / BufferedOutputStream을 사용하여 프로그램을 작성 시

  • 1바이트씩 읽고 쓰는 모든 작업이 내부적으로는 버퍼를 대상으로 일어난다.
  • ✨ 필요에 따라 버퍼와 파일 간에 입출력이 간헐적으로 발생하므로 전체적인 입출력 성능이 향상된다.

문자 스트림

  • 16비트 문자 또는 문자열들을 읽고 쓰기 위한 스트림
  • 모두 Reader/Writer의 자식 클래스
  • 문자 입출력 스트임은 영어 이외의 문자에 대한 처리와 문자 인코딩을 내부에서 처리해준다.
  • 유니 코드를 지원하는 자바 특성에 맞게 2바이트 크기의 입출력을 한다.

👍 문자 스트림을 통해 특별한 문자 인코딩에 독립적인 프로그램을 작성할 수 있고, 좀 더 효율적으로 구현 가능

try{
    FileWriter fw = new FileWriter("test");
    PrintWriter pw = new PrintWriter(fw);
    pw.println("abc");
    pw.close();
    fw.close();
}catch(IOException e){ }

인코딩 Encoding

JAVA는 영미권에서 만들어졌기 때문에 영어를 입출력할 때에는 문제가 없지만 다른 나라에서 자국의 언어로 입출력을 할 때에는 문제가 발생한다.

📍각 나라의 언어를 표현아는 인코딩(Encoding)이 다르기 때문이다.

ASCII코드

120개(알파벳 소문자, 대문자 52개, 10진수 숫자 10개, 문장부호 50~60개)의 자를 표현하기 위해 2^7(7비트)로 표현된 문자 체계인 ASCII 문자 코드를 사용

  • 유럽의 경우 1비트를 더 사용해서 8비트로 표현된 체제 Latin-1을 사용
  • Extended ASCII는 128~255의 빈 공간을 사용하는데 한국도 이 공간을 사용하여 한글을 표현한다.

초성 자음 19개, 중성 모음 21개, 종성 모음 28개 == 15비트가 있어야 하나의 한글 표현 가능 - 2바이트를 묶어서 한 글자를 표현(Multi Byte Character Set) : KSC5601, EUC-KR, MS949

이렇게 각 나라별로 자신만의 고유한 언어를 표현하기 위해서 Extended ASCII를 사용하는데, 이는 전세계의 데이터가 서로 다르게 해석되는 문제를 야기한다. 이를 해결하기 위해 국제 표준화 기구에서 표준 인코딩을 제안했다.

유니코드 Unicode

IOS(International Organization for Standardization)에서의 표준화 작업(2바이트 기준 새로운 인코딩 제안) 진행

  • 전 세계 26개 언어가 모두 포함되어 있다.
  • 어떤 나라에서도 같은 데이터를 공유할 수 있다.
  • 자료를 공유하는 표준으로 사용할 수 있다.

UTF-8

  • 유니코드를 사용할 경우 영미권에 속한 나라는 데이터 공간이 두 배 이상 필요해지기 때문에 공간의 낭비를 감수해야만 한다. 공간의 낭비를 없애기 위해 고안된 방법이 UTF-8이다.
  • UTF-8은 가변 길이를 지원하는데 각 언어별로 최적화된 크기로 저장할 수 있도록 1바이트에서 3바이트까지 표현이 달라진다.
  • 영문은 1바이트, 한글은 3바이트.

❗️바이트 데이터의 값과 그 값이 해석된 인코딩이 서로 맞아 떨어질 때 정확한 글자가 나타난다.

자바에서 인코딩이 적용되는 부분

바이트 데이터를 기준으로 문자를 만들어 내는 부분

ex) char, String 클래스 또는 바이트 스트임에서 문자 스트림으로 변환되는 클래스

  1. String과 인코딩
  2. String 클래스 생성자에는 바이트 배열을 어떤 인코딩으로 해석하여 문자를 생성할 것인지 지정하는 생성자 있다.
  3. 기존의 String 객체를 어떤 인코딩을 통해 바이트 배열로 분해할 것인지 지정해서 실행하는 getBytes()메소드도 존재

👍 자바에서 글자가 깨지는 현상이 발생한다면?
👉 깨진 글자를 getBytes()로 분해했다가 새롭게 String 객체를 만들어 출력하는 방법으로 해결 but 다양한 인코딩을 시도해 봐야한다.

Reader / Writer

  • 문자입력을 담당하는 추상 클래스
  • 다른 문자 입출력 클래스들에 대한 최상위 클래스
  • Reader와 Writer는 InputStream/OutputStream과 사용법이 유사
  • Reader 클래스 주요 메소드

    • abstract void close() throws IOException: 문자 입력 스트림 닫음
    • void mark(int limit) throws IOException: 문자 입력 스트림의 현재 위치 표시
    • int read(char buf[]) throws IOException: 문자 입력 스트림에서 buf[] 크기만큼을 읽어 buf에 저장하고 읽은 문자 수를 return
  • Writer 클래스 주요 메소드

    • abstract void close() throws IOException: 문자 출력 스트림 닫음
    • abstract void flush() throws IOException: 버퍼에 남은 출력 스트림 출력
    • void write(String s) throws IOException: 주어진 문자열 s를 출력
    • void write(char buf[]) throws IOException: buf의 내용을 출력

FileReader / FileWriter

  • 파일에 저장된 바이트를 유니코드 문자로 변환해서 읽어들이거나 출력할 유니코드 문자를 디폴트 문자 인코딩의 바이트로 변환해서 파일에 저장하는 데 사용되는 입출력 클래스
  • 각각 InputStreamReader, OutputStreamReader의 자식 클래스로, 유니코드 문자와 바이트 간의 변환 기능을 포함

BufferedReader / BufferedWriter

  • BufferedInputStream, BufferedOutputStream과 동일하게 입출력 스트림에 버퍼 기능을 추가한 문자 스트림
  • 문자 입력 스트림으로부터 문자를 읽어들이거나 문자 출력 스트림으로부터 문자를 내보낼 때 버퍼링을 함으로써 문자, 문자 배열, 문자열 라인 등을 보다 효율적으로 처리할 수 있도록 한다.
  • 버퍼링 기능을 추가하면 미리 버퍼에 데이터를 갖다 놓기 때문에 효율적으로 입출력 가능

InputStreamReader / OutputStreamReader

  • 바이트 스트림에서 문자 스트림으로, 또는 문자 스트림에서 바이트 스트림으로의 변환을 제공하는 입출력 스트림
  • 바이트를 읽어서 지정된 문자 인코딩에 따라 문자로 변환하는데 사용함
  • 문자로 변환하는 경우 인코딩 방식은 특정 방식으로 지정할 수도 있고 경우에 따라서는 플랫폼의 기본 인코딩을 이용하기도 한다.

InputStreamReader 예시

PrintStream / PrintWriter

데이터를 표준 출력하기 위해서 사용하는 스트림 클래스

  • PrintStream: System.out.println()의 out객체는 PrintStream 유형의 객체
  • PrintWriter: 문자 스트림으로 유니코드와 로컬 컴퓨터의 문자 인코딩 간의 변환을 처리

👉 PrintWriter를 사용하는 것이 유니코드를 표현하는 측면에서 더 효율적

이 클래스들에 있는 메소드들은 다른 스트림과 달리 IOException을 던지지 않는다. If 출력 에러가 발생하면,

  • 사건을 기록하기 위해 객체 내부에 flag 설정
  • 에러가 발생했는지 알기 위해 checkError()메소드로 검사
  • 에러가 발생하면 checkError()는 true return

    System.out.println("Hi");
    if(System.out.checkError())

Scanner 클래스

  • io 패키지에서 제공하는 클래스는 아니지만 표준 입출력 스트림으로부터 데이터를 읽어들일 수 있는 클래스
  • Scanner 클래스를 이용하면 키보드 입력같은 표준 입력 작업을 단순하게 처리할 수 있다.

Hi! I'm @Yeseul Lee
Passionate for what I love

GitHubLinkedIn