입출력과 아이들

cutiepazzipozzi·2023년 6월 9일
2

지식스택

목록 보기
31/35
post-thumbnail

사실 책 없이 (책 요약본만 잇음ㅋ) 찾으면서 글을 작성하는 거라 흐름(오 stream?ㅋ)이 일목요연하지 않을 수 있다. 주의하라고 하는 말이에요....

입출력(I/O)

= 입력+출력을 합쳐 부르는 말
= 두 대상간의 데이터를 주고 받기 위해 사용됨

java.io.~ 이 패키지를 코테를 풀며 자주 import 했었는데 io가 도대체 뭐지? 하고 그땐 찾아볼 생각을 안했는데 이 단원을 배워보며 생각하니 입출력의 io였다. (코테에서 왜 import 했나면 BufferedReader를 사용해 사용자의 입력, 즉 터미널 창의 내 입력을 받아오기 위해 사용했다.)

입출력과 스트림

왜 입출력에 대한 설명이 나오고 스트림이 등장하냐면, 사실 별 의민 없다. 두 데이터를 주고 받기 위해 만들어진 입출력, 즉 java.io 패키지에서 Stream 관련한 클래스들이 제공되기 때문에 두 개념이 연관된다.

그리고 간단히 생각하면 두 대상간의 데이터를 주고받으려면(입출력) 그 사이의 징검다리(스트림)가 필요하겠쥬?

스트림

= 데이터가 단방향으로 흘러가는 개념
= Stream은 단방향으로 데이터를 전달하기에 물의 흐름에 비유
= 데이터의 입출력에 사용되는 연결통로

  • byte 단위로 데이터 전송
  • 입출력 2개를 동시에 하려면 2개의 Stream이 필요함
  • close 메서드를 통해 꼭 사용종료 시 닫아줘야 한다.
    (주고받는 데이터들은 자바를 이용해 접근할 수 있는 외부자원인데, 이는 반납이 필수이다. 이때 한번에 파일의 내용을 모두 메모리에 올리면 부담이 가기 때문에, 스트림을 연결하여 언제든지 원하는 부분의 원하는 만큼의 내용을 읽어들이도록 한다)
    => 아니라면 try-with-resource를 사용해도 좋다!
    try문의 () 자리에 종료를 원하는 객체를 넣어주면 자동으로 close()가 호출된다. (다만 사용조건이 AutoCloseable을 구현한 객체여야 함 - 이 클래스가 close 메서드를 포함하는 인터페이스거든)
try (FileInputStream fis = new FileInputStream("file.txt")){
 ...
} catch(IOException e) {
 ...
}

외부 자원의 반납이 필요한 이유:

외부 자원: 파일, 네트워크 연결, 데이터베이스 연결, 그래픽 리소스 등 다양한 것들을 포함

  1. 메모리 누수: 반복적으로 자원을 사용하게 되는 경우 종료하지 않으면 심각한 메모리 낭비가 발생
  2. 리소스 누수: 일부 외부 자원은 시스템 리소스를 사용하므로
  3. 안정성 문제: 만약 파일을 읽고 쓰는 도중 문제가 생겨(비정상적인 종료) 파일이 제대로 닫히지 않는다면, 데이터 손실이 발생함

스트림의 종류

아래에서는 두 종류에 대한 간단한 설명과, 두 종류에 해당되는 자주 사용하는 클래스에 대해서 알아볼 것이다.

바이트 기반 스트림

= 문자, 영상 등의 여러 데이터 형태를 다루는 스트림
= 데이터를 바이트 단위로 주고받는 스트림

  • 바이트 기반 스트림의 최상위 바이트 기반 클래스
    => InputStream, OutputStream 의 메서드를 (모두 Closeable을 구현)

[InputStream]

  1. abstract int read()
    입력 stream으로부터 데이터의 다음 byte를 읽어옴
    return값 = 데이터의 다음 byte값 (끝에 도달했다면 -1 반환)
  2. int read(byte[] b)
    몇 byte씩 읽어오고 이를 버퍼 배열에 저장함 (배열에 인덱스 0부터 차례대로 저장)
  3. int read(byte[] b, int off, int len)
    인력 stream으로부터 len byte만큼의 데이터를 다른 byte 배열에 읽어옴
    return값 = 버퍼에 읽어온 총 byte 수 (더이상 없다면 -1 반환)

[OutputStream]

  1. abstract void write(int b)
    b만큼의 byte를 이 결과 스트림에 작성
  2. void write(byte[] b)
    b 길이만큼의 byte를 결과 스트림에 작성
  3. void write(byte[] b, int off, int len)
    offset에서 시작하여 출력 스트림에 지정된 byte 배열의 len 길이만큼 작성

문자 기반 스트림

= 정말 말 그대로 문자만 주고받기 위해 사용되는 스트림

  • 문자 기반 스트림의 최고 조상 클래스
    => Reader, Writer의 메서드들
    (따라서 하위 클래스의 이름도 ~Reader, ~Writer로 지어진다고 함..)

[Reader]

  • int read()
    하나의 문자(2byte)를 읽어옴
    이외에도 같은 메서드명의 read(char[] buf), abstract int read(char[] buf, int off, int len) 등이 있당.

[Writer]

  • void write(int c)
    하나의 문자를 작성
    이외에도 같은 메서드명의 write(String str), write(String str, int off, int len) 등이 있당.
    ++ public static Writer nullWriter()
    모든 문자를 삭제하는 새로운 Writer 클래스를 반환

보조 스트림

= 스트림 기능 향상 or 새로운 기능 추가를 위해 사용되는 스트림

  • 독립적으로 입출력 수행 ❌
//기반 스트림 생성
FileInputStream file = new FileInputStream("test.txt");
//기반 스트림 -> 보조 스트림 생성
BufferedInputStream bis = new BufferedInputStream(file);
//보조 스트림으로부터 데이터를 읽음
bis.read();
  • 종류
  1. FilterInputStream, FilterOutputStream
    필터를 이용한 입출력
  2. BufferedInputStream, BufferedOutputStream
    버퍼를 활용한 스트림 입출력 효율 향상
    데이터를 버퍼 크기만큼 가져와 저장하고 입출력함
  3. DataInputStream, DataOutputStream
    int, float와 같은 기본형 단위로 데이터 처리
  4. SequenceInputStream, SequenceOutputStream
    여러개의 입력 스트링 연속으로 연결한 뒤 처리
  5. ObjectInputStream
    데이터를 객체 단위로 읽고 쓰는데 사용함
    (주로 파일 이용, 객체 직렬화와 관련!)

직렬화

= 객체 -> 데이터 스트림
= 자바 객체를 byte의 정적 스트림(sequence)으로 변환한 뒤 DB에 저장 or 네트워크로 전송
( 시퀀스: 데이터 소스(컬렉션, 배열, 파일)에서 가져온 연속된 요소들의 순서**
-> 중간 연산을 통해 시퀀스를 필터링하거나 변환하고, 최종 연산을 통해 시퀀스의 결과를 얻을 수 있음)

  • 객체-독립적인 과정
    (ex. 이 플랫폼에서 직렬화가 이뤄져도 다른 플랫폼에서 역직렬화가 가능함)
  • 직렬화에 적합한 인터페이스 java.io.Serializable을 구현해야 함
  • 직렬화 가능 객체의 필드 중 하나가 객체 배열로 구성된 경우, 이러한 모든 객체도 직렬화가 가능해야 함 (그렇지 않으면 NotSerializableException이 발생)

ex. ObjectOutputStreamfinal void writeObject(Object o) throws IOException 메서드는 직렬화가 가능한 객체를 가져와 byte stream(sequence)로 변환해준다.

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    static String country = "ITALY";
    private int age;
    private String name;
    transient int height; //직렬화 대상에서 제외
}

@Test 
public void whenSerializingAndDeserializing_ThenObjectIsTheSame() () 
  throws IOException, ClassNotFoundException { 
    Person person = new Person();
    person.setAge(20);
    person.setName("Joe");
    
    FileOutputStream fileOutputStream
      = new FileOutputStream("yourfile.txt");
    ObjectOutputStream objectOutputStream 
      = new ObjectOutputStream(fileOutputStream);
    objectOutputStream.writeObject(person); //요기!!!! 
    objectOutputStream.flush();
    objectOutputStream.close();
    
    FileInputStream fileInputStream
      = new FileInputStream("yourfile.txt");
    ObjectInputStream objectInputStream
      = new ObjectInputStream(fileInputStream);
    Person p2 = (Person) objectInputStream.readObject(); //요기!!
    objectInputStream.close(); 
 
    assertTrue(p2.getAge() == person.getAge());
    assertTrue(p2.getName().equals(person.getName()));
}

Lombok이나 JUnit 처리를 위해 오랜만에 IntelliJ를 켜봤는데, 역시나 작동이 잘 안된다ㅠㅠ 테스트가 잘 동작하는지 확인하고 싶었다만.. 추후에 성공하면 추가하도록 하겠다.

바이트 기반 스트림 종류

  1. ByteArrayInputStream, ByteArrayOutputStream
    byte 배열에 데이터를 입출력하는 바이트 기반 스트림
class IOEx3 {
	public static void main(String[] args) {
    	byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
        byte[] outSrc = null;
        
        byte[] temp = new byte[4];
        
        ByteArrayInputStream input = null;
        ByteArrayOutputStream output = null;
        
        try {
        	while(input.available() > 0) {
            	input.read(temp);
                output.write(temp);
            }
        } catch(IOException e) {}
        
        outSrc = output.toByteArray();
        
        System.out.println("Input Source: "+Arrays.toString(inSrc));
        System.out.println("temp: "+Arrays.toString(temp));
        System.out.println("Output Source: "+Arrays.toString(outSrc));
    }
}
  1. FileInputStream, FileOutputStream
    파일에 데이터를 입출력하는 바이트 기반 스트림
  • FileInputStream(String name)
  • FileInputStream(File file)
class FileCopy {
	public static void main(String[] args) {
    	try {
        	FileInputStream fis = new FileInputStream(args[0]);
            FileOutputStream fos = new FileOutputStream(args[1]);
            
            int data = 0;
            while((data = fis.read()) != -1) {
            	fos.write(data);
            }
            
            fis.close();
            fos.close();
        } catch(IOException e) {
        	e.printStackTrace();
        }
    }
}

// java FileCopy FileCopy.java FileCopy.bak

[보조스트림]

  1. FilterInputStream, FilterOutputStream
    = 바이트 기반 보조 스트림의 최고조상!!
  • 상속을 통해 두 클래스의 메서드 read(), write()를 원하는 기능대로 오버라이딩 해야함
  1. BufferedInputStream, BufferedOutputStream
    = 입출력 효율을 위해 버퍼를 사용하는 보조스트림
  • 보조스트림을 닫으면 기반스트림도 닫힘
  1. DataInputStream, DataOutputStream
    = 기본형 단위로 읽고 쓰는 보조스트림
  • 각 자료형의 크기가 다르기 때문에 입출력 시 순서를 주의해야함
  1. SequenceInputStream
    = 여러 입력스트림을 연결해서 하나의 스트림처럼 다룰 수 있도록 함
    = SequenceInputStream(Enumeration e) : Enumeration에 저장된 순서대로 입력 스트림 하나로 연결

  2. PrintStream
    = 데이터를 다양한 형식의 문자로 출력하는 기능 제공

  • 대표적인 예시 System.out, System.err
  • PrintStream << PrintWriter

문자 기반 스트림 종류

  1. FileReader, FilerWriter
    = 문자 기반의 파일 입출력, 텍스트 파일의 입출력에 사용

  2. PipedReader, PipedWriter
    = 프로세스(스레드) 간 데이터를 주고받는데 사용

  3. StringReader, StringWriter
    = 메모리의 입출력에 사용 (ex. CharArrayReader, CharArrayWriter)

  • StringWriter에 출력되는 데이터는 내부의 StringBuffer에 저장

[보조스트림]

  1. BufferedReader, BufferedWriter
    = 입출력 효율을 위해 버퍼를 사용하는 보조스트림
  • 라인 단위의 입출력..
  • String readLine(): 한 라인, void newLine(): 개행문자 출력
  1. InputStreamReader, OutputStreamWriter
    = byte 기반 스트림 => 문자 기반 스트림처럼!
  • 인코딩 변환, 입출력 가능케 함

표준 입출력 & File

= 화면(console)을 통한 데이터의 입출력

  • JVM이 시작되면서 자동적으로 생성되는 스트림
  1. System.in, System.out, System.err
  2. RandomAccessFile
    = 하나의 스트림으로 파일에 입출력을 모두 수행할 수 있는 스트림
    = Object의 자손
  3. File
    = 파일과 디렉토리를 다루는 클래스
  • 생성자와 메서드
  1. File(String fileName)
    = 주어진 fileName을 이름으로 갖는 파일을 위한 File 인스턴스 생성
  2. File(String pathName, String fileName)
    = 파일의 경로와 이름을 따로 분리해 지정할 수 있도록 한 생성자
  3. String getName()/getPath()/getAbsolutePath()/getParent()/getCanonicalPath()
//예시 코드

class FileEx2 {
	public static void main(String[] args) {
    	if(args.length != 1) {
        	System.out.println("USAGE: java FileEx2 DIR");
            System.exit(0);
        }
        
        File f = new File(args[0]);
        
        if(!f.exists() || !f.isDirectory()) {
        	System.out.println("유효하지 않은 디렉토리입니다.");
            System.exit(0);
        }
        
        File[] files = f.listFiles();
        
        for(int i = 0; i < files.length; i++) {
        	String fileName = files[i].getName();
            System.out.println(files[i].isDirectory() ? "["+fileName+"]" : fileName);
        }
    }
}

(제대로 된) 직렬화

= 객체를 '연속적인 데이터' 형태로 변환하는 것
= 객체의 인스턴스 변수 값을 일렬로 나열하는 것
= (시스템) JVM의 메모리에 힙 or 스택 되어있는 객체 데이터를 바이트 형태로 변환하는 기술 + 직렬화된 byte 형태의 데이터를 객체로 변환해서 JVM으로 상주시키는 형태

  • 객체를 저장하려면 객체를 직렬화해야 함

  • 객체 저장 = 객체의 모든 인스턴스 변수 값 저장

  • 기본 자료형은 byte 변수가 정해져있기 때문에 byte 단위의 변화에 문제가 X

  • but 객체를 구성하는 자료형들의 종류, 수에 따라 객체의 크기가 변화할 수 있어 java.io.Serializable을 구현해야만 직렬화 가능

  • 제어자 transientSerializable을 구현하지 않은 클래스의 인스턴스도 직렬화 대상에서 제외

  • Serializable을 구현하지 않은 조상 멤버들도 직렬화 대상 제외

  • readObject()writeObject()를 오버라이딩 하면 직렬화 맘대로

  • ObjectInputStream, ObjectOutputStream
    = 객체를 직렬화하여 입출력할 수 있게 해주는 '보조스트림'

//사용법

//객체를 파일에 저장하는 방법
FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);

out.writeObject(new UserInfo());

//파일에 저장된 객체를 다시 읽어오는 방법
FileInputStream fis = new FileInputStream(objectfile.ser");
ObjectInputStream in = new ObjectInputStream(fis);

UserInfo info = (UserInfo) in.readObject();
  • 직렬화/역직렬화 시 클래스가 같은지 확인할 필요가 있다.

직렬화의 장점

  • 자바시스템의 개발에 최적화 되어 있다.
    (복잡한 데이터 구조의 객체라도 직렬화 기본 조건만 지킨다면 큰 작업없이 직렬화가 가능해진다. 또한 데이터 타입이 자동으로 맞춰진다.)

직렬화를 사용하는 이유

  • JVM의 메모리에서만 상주돼있는 객체 데이터를 그대로 영속화 해야할 때 사용
    ex. 서블릿 세션, 캐시, 자바 RMI(Remote Method Invocation)

  • 서로 다른 메모리 공간 사이의 데이터 전달을 위해
    (OS의 프로세스는 서로 다른 가상 메모리 주소 공간을 갖기 때문에 Object 타입의 참조값 데이터 인스턴스를 전달할 수 없다)

버퍼가 효율을 높여주는 이유

버퍼 = 특정 원시 타입의 데이터를 위한 컨테이너

  1. 입출력 횟수
    데이터를 한번에 큰 덩어리로 처리할 수 있기 때문에 입출력 횟수 ↓
    => 입출력 연산에 따른 overhead가 줄어듦
    (원래는 1byte씩 읽고 처리)
  2. 시스템 호출 수
    입출력은 주로 os의 시스템 호출(비용이 큰 연산)을 통해 이뤄진다. 이때 데이터마다 시스템 호출이 일어나기 때문에 횟수가 ↓
  3. 대기 시간 줄임
    버퍼를 사용한다면 데이터를 한꺼번에 처리하여 외부 장치와의 대기 시간을 줄일 수 있다.
  4. 데이터 처리 효율성 Good
    위와 마찬가지로 데이터를 한꺼번에 처리 하기 때문에!

그러나...
버퍼는 추가적인 메모리가 사용되기 때문에 사용량이 증가한다. 따라서 실시간 통신이나 대화형 상호작용과 같이 즉각적인 응답이 필요한 상황에서는 즉시 입출력을 처리하는 것이 유리하다!

참조

자바의 정석, 2nd Edition.
https://hudi.blog/java-inputstream-outputstream/
https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/io/OutputStream.html
https://www.baeldung.com/java-serialization
https://techblog.woowahan.com/2550/
https://ryan-han.com/post/java/serialization/

profile
노션에서 자라는 중 (●'◡'●)

0개의 댓글