IO 패키지
IO 패키지는 입출력 작업을 수행하기 위한 클래스와 인터페이스를 제공하는 패키지입니다.
사용자가 키보드로 데이터를 `입력`할 수도 있고, 파일 또는 네트워크로부터 데이터가 입력될 수 있습니다. `출력`도 마찬가지입니다. IO 패키지를 활용하여 데이터를 시스템 콘솔, 파일, 네트워크 등 다양한 소스와 대상으로 전달하고 처리할 수 있습니다.
Java에서 데이터는 `Stream`을 통해 입/출력이 되기 때문에 스트림 특징을 이해해야 합니다. 스트림은 데이터를 읽거나 쓰는 작업을 수행합니다. 이때 데이터 교환을 위해서는 입력 스트림과 출력 스트림이 각각 필요한데, 그 이유는스트림은 단방향이기 때문에 하나의 스트림으로 입/출력을 동시에 할 수 없기 때문입니다.
스트림은 `바이트 기반 스트림`과 `문자 기반 스트림`으로 나뉘고, 입출력 대상의 특성에 맞게 사용하면 됩니다.
`바이트 기반 스트림`
: 데이터를 바이트 단위로 처리하는 스트림
: 그림, 멀티미디어, 문자 등 모든 종류의 데이터를 주고 받을 수 있습니다.
: `InputStream` & `OutputStream`을 기반으로 하는 클래스들이 포함되어 있습니다.
`문자 기반 스트림`
: 데이터를 문자 단위로 처리하는 스트림
: 문자만을 주고 받을 수 있습니다.
: `Reader`와 `Writer`를기반으로 하는 클래스들이 포함되어 있습니다.
입출력 스트림
InputStream & OutputStream
바이트 기반 스트림을 다루는 최상위 추상 클래스입니다.
InputStream
InputStream은 입력 소스에서 데이터를 바이트 단위로 읽어오는 작업을 수행하는 클래스입니다. 주요 메서드로는 다음과 같은 것들이 있습니다:
- `int read()`
입력 스트림으로부터 1바이트를 읽어옵니다. 읽어온 바이트를 반환하며, 읽은 데이터가 없을 경우 -1을 반환합니다. - `int read(byte[] buffer)`
입력 스트림으로부터 최대 buffer.length만큼의 바이트를 읽어와 buffer 배열에 저장합니다. 실제로 읽어온 바이트 수를 반환하며, 읽은 데이터가 없을 경우 -1을 반환합니다.
byte[] buffer = new byte[100];
입력 스트림에서 280개의 바이트가 들어오고 위와같이 길이를 100인 바이트로 설정하여 `read(buffer)`를 하면 길이가 100인 바이트 배열로 3번 읽어들입니다.
첫 번째 읽을 경우: 100 바이트 읽음
두 번째 읽을 경우: 100 바이트 읽음
세 번째 읽을 경우: 80 바이트 읽음
- `void close()`
입력 스트림을 닫습니다.
OutputStream
OutputStream은 출력 대상으로 데이터를 바이트 단위로 쓰는 작업을 수행하는 클래스입니다. 주요 메서드로는 다음과 같은 것들이 있습니다:
- `void write(byte[] buffer)`
출력 스트림에 바이트 배열 buffer의 모든 바이트를 씁니다. - `void flush()`
출력 버퍼에 남아있는 비우고 데이터를 강제로 출력합니다. 출력 작업을 완료하기 전에 호출할 수 있습니다. - `void close()`
출력 스트림을 닫습니다. 닫을 때 모든 데이터가 출력돼야 하기 때문에 내부적으로 `flush()` 메서드가 자동으로 호출됩니다.
Reader & Writer
문자 기반 스트림을 다루는 최상위 추상 클래스입니다. InputStream과 OutputStream의 문자 버전이라 생각하면 됩니다.
Reader
- `int read()`
입력 스트림으로부터 하나의 문자를 읽고 해당 문자의 유니코드 값을 반환합니다. 읽은 문자가 없을 경우 -1을 반환합니다. - `int read(char[] cbuf)`
입력 스트림으로부터 문자 배열 cbuf로 최대한 많은 문자를 읽고, 실제로 읽은 문자의 개수를 반환합니다. 읽은 문자가 없을 경우 -1을 반환합니다. - `void close()`
Reader를 닫고 관련된 시스템 리소스를 해제합니다.
Writer
- `void write(int c)`
출력 스트림에 하나의 문자를 씁니다. 유니코드 값 c에 해당하는 문자가 출력됩니다. - `void write(char[] cbuf)`
출력 스트림에 문자 배열 cbuf의 모든 문자를 씁니다. - `void write(String str)`
출력 스트림에 문자열 str을 씁니다. - `void flush()`
출력 버퍼에 남아있는 모든 데이터를 강제로 출력합니다. 출력 작업을 완료하기 전에 호출할 수 있습니다. - `void close()`
Writer를 닫고 관련된 시스템 리소스를 해제합니다. 이 과정에서 flush() 메서드가 자동으로 호출됩니다.
콘솔 입출력
콘솔(Console)은 시스템을 사용하기 위한 텍스트 기반 소프트웨어입니다. Linux OS에서는 터미널(terminal), Windows OS에서는 명령 프롬프트라고 불리는 것들이 콘솔입니다. Java에서 콘솔 입출력을 다루기 위해 `System` 클래스를 사용합니다.
- System.in: 콘솔로부터 데이터 입력 받을 때
- System.out: 콘솔로부터 데이터 출력할 때
- System.err: 에러를 출력할 때
System.in
콘솔로부터 입력을 받을 때 사용합니다. `System.in`은`InputStream` 객체이기 때문에 `InputStream` 변수로 참조가 가능합니다. 콘솔 입력을 편리하게 읽기 위해 `InputStreamReader`와 `BufferedReader` 등의 클래스를 사용할 수 있습니다. 이와 관련해서 아래에 설명을 따로 하도록 하겠습니다.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class ConsoleInputExample {
public static void main(String[] args) {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
try {
System.out.print("Enter a string: ");
String input = reader.readLine();
System.out.println("You entered: " + input);
} catch (IOException e) {
e.printStackTrace();
}
}
}
`Scanner` 클래스
java.io 패키지에 포함된 클래스는 아니지만, `java.util.Scanner` 클래스를 이용하면 콘솔로부터 기본 타입의 값을 바로 읽을 수 있습니다.
System.out
프로그램의 결과를 콘솔에서 보여주기 위해(출력) 사용합니다. `System.out`은 `PrintStream` 객체이고 `PrintStream`은 `OutputStream`의 하위 클래스이기 때문에 `OutputStream` 변수로 참조가 가능합니다.
PrintStream의 print(), println() 메서드를 사용하여 다양한 타입의 데이터를 콘솔에 출력한 적이 있을겁니다. `PrintStream`에 대해서도 아래 보조 스트림 부분에서 추가로 설명하도록 하겠습니다.
System.out.println("Hello World!!");
// 위 코드는 아래와 같습니다.
PrintStream ps = System.out;
ps.println("Hello World!!");
파일 및 파일 입출력
File
`java.io.File` 클래스는 파일 또는 디렉터리의 속성과 경로를 얻어내거나, 파일 생성 및 삭제를 하기 위한 기능을 제공합니다. 하지만 파일의 데이터를 읽고 쓰는 기능을 지원하지 않기 때문에, 파일에 대한 실제 데이터의 입출력을 위해선 스트림을 사용해야 합니다.
public class FileInfoExample {
public static void main(String[] args) {
// 파일 객체 생성
File file = new File("demoFile.txt");
// 파일 정보
System.out.println("파일명: " + file.getName());
System.out.println("파일 경로: " + file.getPath());
System.out.println("절대 경로: " + file.getAbsolutePath());
System.out.println("존재 여부: " + file.exists());
System.out.println("파일 크기: " + file.length() + " 바이트");
// 디렉터리 정보
File directory = new File("demo_directory");
System.out.println("디렉터리 경로: " + directory.getPath());
System.out.println("절대 경로: " + directory.getAbsolutePath());
System.out.println("존재 여부: " + directory.exists());
System.out.println("디렉터리인지 확인: " + directory.isDirectory());
// 파일 목록 조회
if (directory.isDirectory()) {
System.out.println("디렉터리 내 파일 목록:");
File[] files = directory.listFiles();
if (files != null) {
for (File f : files) {
System.out.println(f.getName());
}
}
}
}
}
FileInputStream & FileOutputStream
파일을 바이트 기반으로 읽고 쓰는 클래스입니다. 그림, 오디오, 비디오, 텍스트 파일등 모든 종류의 파일을 읽고 저장할 수 있습니다.
public class FileStreamExample{
public static void main(String[] args){
try{
FileInputStream fis = new FileInputStream("파일경로/파일명");
int data;
while((data = fis.read()) != -1){
System.out.write(data);
}
fis.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
`FileOutputStream` 경우, 이미 존재하는 파일을 경로로 지정하면 내용이 덮어쓰기가 되기때문에 주의해야 합니다. 기존의 파일내용 끝에 데이터를 추가하고 싶은 경우 다음과 같이 생성자의 두 번째 매개변수에 true를 지정하면 됩니다.
FileOutputStream ifs = new FileOutStream('C:/demo/demo.txt", true);
FileReader & FileWriter
파일을 문자 기반으로 읽고 쓰는 클래스입니다.
보조 스트림: 바이트기반 → 문자기반 변환
바이트 기반 스트림을 문자 기반 스트림으로 변환하는역할을 합니다.
문지 기반 스트림은 텍스트 데이터를 처리하기 위해 설계되었기 때문에 텍스트 데이터를 다루는데 있어서 유리합니다.
텍스트를 다루는데 다양한 기능과 메서드를 제공하고 문자 인코딩 처리가 편리합니다.
- `InputStreamReader`
바이트 입력 스트림을 문자 입력 스트림으로 변환합니다. 주로 `FileInputStream`, `Socket.getInputStream()` 등의 바이트 기반 입력 스트림과 함께 사용됩니다.
Reader reader = new InputStreamReader(System.in);
- `OutputStreamWriter`
바이트 출력 스트림을 문자 출력 스트림으로 변환합니다. 주로 `FileOutputStream`, `Socket.getOutputStream()` 등의 바이트 기반 출력 스트림과 함께 사용됩니다.
Writer writer = new OutputStreamWriter(바이트출력스트림);
보조 스트림: Buffer로 입출력 성능 향상
입출력 성능을 향상시키기 위한 방법으로 `메모리 버퍼`를 사용합니다. 기존 방법은 프로그램이 입출력 시 디스크와 직접 데이터를 처리하였었습니다. 하지 중간에 `메모리 버퍼`를 두어 처리함으로, 프로그램은 메모리 버퍼에 데이터를 보내어 쓰기 속도가 향상되고, 버퍼는 데이터를 쌓아서 한꺼번에 디스크에 데이터를보내어 출력 횟수를 줄여 성능을 높였습니다.
BufferedInputStream, BufferedOutputStream
바이트입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림입니다.
- BufferedInputStream
바이트 기반 입력 스트림에 대한 버퍼링을 제공합니다. 바이트 단위로 데이터를 효율적으로 읽을 수 있도록 도와줍니다. read() 메서드를 통해 데이터를 읽을 수 있습니다. - BufferedOutputStream
바이트 기반 출력 스트림에 대한 버퍼링을 제공합니다. 바이트 단위로 데이터를 효율적으로 쓸 수 있도록 도와줍니다. write() 메서드를 통해 데이터를 버퍼에 저장하고, flush() 메서드를 호출하거나 버퍼가 가득 찰 때 자동으로 출력합니다.
BufferedReader, BufferedWriter
문자 입력 스트림에 연결되어 버퍼를 제공해주는 보조 스트림입니다.
- BufferedReader
문자 기반 입력 스트림에 대한 버퍼링을 제공합니다. 문자열을 효율적으로 읽을 수 있도록 도와줍니다. `BufferedReader`는 `readLine()` 메서드를 추가적으로 갖고 있습니다. readLine() 메서드를 통해 한 줄씩 문자열을 읽을 수 있습니다. - BufferedWriter
문자 기반 출력 스트림에 대한 버퍼링을 제공합니다. 문자열을 효율적으로 쓸 수 있도록 도와줍니다. write() 메서드를 통해 문자열을 버퍼에 저장하고, flush() 메서드를 호출하거나 버퍼가 가득 찰 때 자동으로 출력합니다.
프린터 보조 스트림
PrintStream과 PrintWriter는 모두 출력을 위한 보조 스트림 클래스입니다. 둘 다 `print(), `println()`, `printf()`를 사용할 수 있습니다. 둘의 차이점은 다음과 같습니다.
PrintStream ps = new PrintStream(바이트출력스트림);
PrintWriter pw = new PrintWriter(문자출력스트림);
PrintWriter 사용 예제
PrintWriter writer = new PrintWriter(new OutputStreamWriter(System.out));
writer.println("Hello, World!");
writer.flush();
객체 입출력 보조 스트림
Java는 메모리에 생성된 객체를 파일이나 네트워크로 출력할 수가 있습니다.
객체는 텍스트가 아니기 때문에 바이트 기반 스트림으로 출력을 해야합니다. 또한 객체를 출력하기 위해서는 객체의 데이터(필드 값)를 일렬로 늘어선 연속적인 바이트로 변경해야 하는데 이를 객체 직렬화(serialization)라고 합니다. 반대로 파일에 저장되어 있거나 네트워크에서 전송된 객체를 읽을 수도 있는데, 입력 스트림으로부터 읽어들인 연속적인 바이트를 객체로 복원하는 것은 역직렬화(deserialization)라고 합니다.
ObjectInputStream & ObjectOutputStream
`ObjectOutputStream`
바이트 출력 스트림과 연결되어 객체를 직렬화하여 바이트 형태로 출력하는 스트림입니다. 객체를 출력하면 객체의 상태와 구조가 바이트 배열로 변환되어 출력됩니다.
ObjectOutputStream의 `writeObject()` 메소드는 객체를 직렬화합니다.
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("File명"));
MyObject obj = new MyObject(); // 직렬화할 객체
oos.writeObject(obj);
oos.close();
`ObjectInputStream`
바이트 입력 스트림과 연결되어 객체를 역직렬화하여 객체로복원하는 스트림입니다.
ObjectInputStream의 readObject() 메소드는 입력 스트림에서 읽은 바이트를 역직렬화해서 객체로 생성한다.
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("File명"));
MyObject obj = (MyObject) ois.readObject();
ois.close();
Serializable: 직렬화가 가능한 클래스
모든 객체를 직렬화할 수 있는 것이 아니라 Java는 Serializable 인터페이스를 구현한 클래스만 직렬화할 수 있도록 제한하고 있습니다. Serializable 인터페이스는 필드나 메소드가 없는 빈 인터페이스지만, 객체를 직렬화할 때 private 필드를 포함한 모든 필드를 바이트로 변환해도 좋다는 표시 역할을 해줍니다.
객체를 직렬화하면 바이트로 변환되는 것은 필드들이고, 생성자 및 메소드는 직렬화에 포함되지 않습니다. 따라서 역직렬화할 때에는 필드의 값만 복원됩니다. 하지만 모든 필드가 직렬화 대상이 되는 것은 아닙니다. 필드 선언에 transient가 붙어 있을 경우에는 직렬화가 되지 않습니다.
`serialVersionUID 필드`
직렬화된 객체를 역직렬화할 때는 직렬화했을 때와 같은 클래스를 사용해야 합니다. 클래스 이름이 같더라도 클래스의 내용이 변경되면, 역직렬화는 실패합니다. serialVersionUID는 같은 클래스임을 알려주는 식별자 역할을 합니다. 이는 선언하지 않더라도 컴파일 시에 자동으로 생성되지만, 클래스를 재컴파일하면 serialVersionUID 값이 달라게 됩니다.
예를 들어, 네트워크로 객체를 직렬화하여 전송하는 경우, 보내는 쪽과 받는 쪽이 모두 같은 serialVersionUID를 갖는 클래스를 가지고 있어야 하는데 한 쪽에서 클래스를 변경해서 재컴파일하면 다른 serialVersionUID를 가지게 되므로 역직렬화에 실패하게 됩니다.
불가피하게 클래스의 수정이 필요하거나 재컴파일할 때도 있기 때문에 이를 위해 serialVersionUID를 명시적으로 개발자가 선언할 수도 있습니다.(클래스 작성시 필드로 선언하면 됩니다.)
'Java' 카테고리의 다른 글
Java 어노테이션(@annotation)을 활용한 커스텀 메타데이터 만들기 (0) | 2023.07.05 |
---|---|
Java 직렬화와 역직렬화: 객체 저장과 복원을 위한 기술 (0) | 2023.06.28 |
Java 소켓을 사용하여 단체 채팅방 만들기 (0) | 2023.06.26 |
Java try-wtih-resources로 자원 관리하기 (0) | 2023.06.23 |
Java 소켓 프로그래밍: 네트워크 통신을 위한 Java Socket (0) | 2023.06.23 |