표준 입출력
자바에서 콘솔과 같은 표준 입출력 장치를 위해 운영체제 시스템과 관련된 기능을 제공하는 클래스 System을 정의해 놓았다. 모든 멤버가 static이기 때문에 별도의 인스턴스를 생성할 필요가 없으며 표준 입출력 관리와, 환경 변수 접근, 시스템 속성, 현재 ms / ns시간 확인, 객체 복사 등에 사용된다.
- 표준 입출력은 시스템에서 설정을 따르겠다는 뜻으로 일반적으로 표준 입력 장치는 키보드, 표준 출력 장치는 콘솔(console, 화면)을 의미하며 환경에 따라 다른 입출력 소스가 될 수도 있다.
System클래스는 표준 입출력을 위해 in, out.error와 같은 클래스 변수를 제공한다.
자료형 | 멤버 변수 | 설명 |
static PrintStream | out | 표준 출력 스트림 |
static InputStream | in | 표준 입력 스트림 |
static OutputStream | err | 표준 에러 스트림 |
- out, in, err 모두 static 변수로 System 클래스를 생성하지 않고 사용할 수 있다.
- 표준 입출력 스트림은 자바가 자동으로 생성하므로 별도로 스트림을 생성하지 않아도 사용할 수 있다.
- out, in, err의 타입은 InputStream과 OutputStream이지만 실제로는 버퍼를 이용하는 BufferedInputStream과 BufferedOutputStream의 인스턴스를 사용한다.
콘솔 입력은 버퍼를 갖고 있기 때문에 BackSpace키를 이용해서 편집이 가능하며 한 번에 버퍼의 크기만큼 입력이 가능하다.
Enter키나 입력의 끝을 알리는 '^z'를 누르기 전까지는 아직 데이터가 입력 중인 것으로 간주되어 커서가 입력을 계속 기다리는 상태(블락킹)에 머무르게 된다.
콘솔에 데이터를 입력하고 Enter키를 누르면 입력대기 상태에서 벗어나 입력된 데이터를 읽기 시작하고 입력된 데이터를 모두 읽으면 다시 입력대기 상태가 된다. 위 과정이 반복되다 '^Z'를 입력하면, read()는 입력이 종료되었을 인식하고 -1을 반환하며 반복을 벗어나 프로그램이 종료된다.
- 윈도우는 '^Z', 유닉스와 맥킨토시 '^d' 를 누르는 것이 스트림의 끝을 의미
- 윈도우의 콘솔은 한 번에 최대 255글자까지만 입력이 가능하다.
표준 입출력의 대상 변경
기본적으로 입출력 대상은 콘솔화면이다. 하지만 SetOut(), SetErr(), SetIn()을 통해 입출력 콘솔 이외의 다른 입출력 대상(파일 등)으로 변경하는 것이 가능하다.
메서드 | 설명 | |
static void setOut(PrintStream out) | System.out의 출력을 지정된 PrintStream으로 변경합니다. | |
static void setErr(PrintStream err) | System.err의 출력을 지정된 PrintStream으로 변경합니다. | |
static void setIn(InputStream in) | System.in의 입력을 지정된 PrintStream으로 변경합니다. |
class Main {
public static void main(String[] args){
try {
FileOutputStream fos = new FileOutputStream("test.txt");
PrintStream ps = new PrintStream(fos);
System.setOut(ps);
System.out.println("System.out의 출력대상을 test.txt파일로 변경");
} catch (FileNotFoundException e) {
System.err.println("File Not Found");
}
}
}
RandomAccessFile
자바에서는 입력과 출력이 각각 분리도어 별도로 작업을 하도록 설계되어 있는데 , RandomAccessFile 만은 하나의 클래스로 파일에 대한 입력과 출력을 모두 할 수 있도록 되어있다. InputStream과 OutputStream으로부터 상속받지 않고 DataInput와 DataOutput를 모두 구현했기 때문이다.
- 하나의 스트림으로 파일에 입력과 출력을 모두 수행할 수 있는 스트림
- 다른 스트림들과 달리 Object의 자손이다.
가본 자료형의 단위로 읽고 쓸 수 있는 DataInputStream은 DataInput인터페이스를, DataOutputStream는 DataOuput인터페이스를 각각 구현했는데 기본 자료형을 읽고 쓰기 위한 메서드 들은 DataInput와 DataOutput인터페이스에 정의되어 있다. 따라서 DataInput와 DataOutput를 구현한 RandomAccessFile는 기본자료형 단위로 데이터를 읽고 쓸 수 있다.
다른 입출력 클래스들은 소스로부터 읽기/쓰기를 순차적으로 하기 때문에 제한적이다. 하지만 RandomAccessFile는 내부적으로 파일 포인터를 사용해하는데 입출력 작업 시 수행되는 곳이 바로 파일 포인터가 위치한 곳이 된다.
- RandomAccessFile는 파일에 읽고 쓰는 위치에 대한 제한이 없다.
순차적으로 읽고 쓴다면 파일 포인터를 이동시키기 위한 별도의 작업이 필요치 않지만 파일의 임의의 위치에 있는 내용에 대해서 작업하고자 한다면 파일 포인터를 원하는 위치로 옮긴 다음 작업을 해야 한다.
- getFilePointer 작업중인 파일에서 포인터의 위치를 알려준다.
- 위치를 옮기기 위해서는 seek나 SkipBytes를 사용한다.
File 클래스
입출력 스트림을 사용하면 파일을 통한 입출력 작업을 수행할 수 있다. 하지만 파일의 제거나 디렉터리에 관한 작업 등은 입출력 스트림을 통해서는 수행할 수 없다.
자바는 이러한 입출력 작업 이외의 파일과 디렉터리에 관한 작업을 File 클래스를 통해 처리하도록 하고 있다.
- java.io.file
- 파일 이름 변경, 삭제, 디렉터리 생성, 크기 등 파일 관리기능을 제공한다.
- 반대로 File 객체는 파일 읽고 쓰기 기능 없음
File의 메서드
File f = new File("c:\\windows\\system.ini");
String filename = f.getName(); // "system.ini"
String path = f.getPath(); // "c:\\windows\\system.ini"
String parent = f.getParent(); // "c:\\windows"
if(f.isFile()) // 파일인 경우
System.out.println(f.getPath() + "는 파일입니다.");
else if(f.isDirectory()) // 디렉터리인 경우
System.out.println(f.getPath() + "는 디렉터리입니다.");
File 인스턴스를 생성했다고 해서 파일이나 디렉토리가 생성되지 않으며 파일이나 디렉토리명으로 지정된 문자열이 유효하지 않더라도 컴파일 에러나 예외를 발생시키지 않는다.
// 이미 존재하는 파일 참조
File f = new File("C:\\Users\\OK\\Desktop\\자바연습","File.java");
// 기존에 없던 파일을 새로 생성
File f = new File("C:\\Users\\OK\\Desktop\\자바연습","NewFile.java");
f.createNewFile();// 새로운 파일 생성
- 새로운 파일을 생성하기 위해서는 File 인스턴스를 생성한 다음, 출력 스트림을 생성하거나 createNewFile()을 호출해야 한다.
직렬화(Serialization)
직렬화란 객체를 데이터 스트림으로 만드는 것을 뜻한다. 다시 애기하면 객체에 저장된 데이터를 쓰기(write) 위해 연속적인(Serial) 데이터로 변환하는 것을 말한다. 반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)라고 한다.
- 객체를 ‘연속적인 데이터’로 변환하는 것. 반대과정은 ‘역직렬화’라고 한다.
- 객체의 인스턴스변수들의 값을 일렬로 나열하는 것
- 객체는 클래스에 정의된 오직 인스턴스 변수의 집합으로 객체에는 클래스변수나 메서드가 포함되지 않는다.
인스턴스 변수는 인스턴스마다 다른 값을 가질 수 있어야 하기 때문에 별도의 메모리 공간이 필요하지만 메서드는 변하는 것이 아니기 때문에 메모리를 낭비해 가면서 인스턴스마다 같은 내용의 메서드를 포함시킬 이유는 없다.
- 객체를 저장하거나 전송하기 위해서는 직렬화의 과정이 필요하다.
- 객체를 저장한다는 것은 객체의 모든 인스턴스변수의 값을 저장하는 것
따라서 객체를 저장한다는 것은 바로 객체의 모든 인스턴스변수의 값을 저장한다는 것과 같은 의미이다. 어떤 객체를 저장하고자 한다면, 현재 객체의 모든 인스턴스변수의 값을 저장하기만 하면 된다. 그리고 저장했던 객체를 다시 생성하려면 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스 변수에 저장하면 되는 것이다.
클래스에 정의된 인스턴스변수가 기본형이라면 간단한 일이지만 인스턴스 변수의 타입이 참조형이라면 간단하지 않다.(배열이라면 배열에 저장된 값을 모두 저장해야 한다.)자
바에서는 객체를 직렬화/역직렬화할 수 있는 ObjectInputStream와 ObjectOutputStream 을 사용하는 방법만 알면 된다.
ObjectInputStream와 ObjectOutputStream
ObjectInputStream은 객체를 직렬화(스트림에 객체 출력)ObjectOutputStream은 역직렬화(스트림으로부터 객체를 입력) 하여 입출력할 수 있게 해주는 보조스트림이다.
ObjectInputStream(InputStream in)
ObjectOutputStream(OutputStream out)
- 파일에 객체를 직렬화 하여 저장
FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(new UserInfo());
ObjectOutputStream의 writeObject(Object obj)를 사용해서 객체를 출력하면 객체가 파일에 직렬화되어 저장된다.
- 파일에 저장된 객체를 다시 읽어오기
FileInputStream fis = new FileInputStream("objectfile.ser");
ObjectInputStream in = new ObjectInputStream(fis);
Object obj = (UserInfo)in.readObject();
ObjectInputStream의 readObject()를 사용하여 저장된 데이터를 일기만 하면 객체로 역직렬화된다.
반환 타입이 Object이기 때문에 형변환이 필요하다.
직렬화 가능한 클래스 만들기
java.io.Serializable을 구현해야만 직렬화가 가능하다. 클래스를 직렬화 하고자 한다면 해당 클래스가 Serializable인터페이스를 구현하도록 해야 한다.
- Serializable인터페이스는 빈 인터페이스지만, 직렬화를 고려하야 작성한 클래스인지 판단하는 기준이 된다.
class UserInfo implements Serializable {
String name;
String password;
int age;
}
// public interface Serializable { }
class SupueruUserInfo implements Serializable{ boolean authority;}
class UserInfo extends SupueruUserInfo {
String name;
String password;
int age;
}
상속받은 객체를 직렬화 하면 조상에 정의된 인스턴스 변수도 함께 직렬화 된다.
- 조상클래스가 Serializable를 구현하지 않았다면 자손 클래스를 직렬화 할 때 조상클래스에 정의된 인스턴스 변수는 직렬화 대상에서 제외된다.(제외된 변수는 기본값으로 초기화)
class SupueruUserInfo{ boolean authority;}
class UserInfo extends SupueruUserInfo implements Serializable {
String name;
String password;
int age;
}
- 직렬화 할 수 없는 클래스의 객체를 인스턴스변수가 참조하고 있다면 java.io.NotSerializableException예외가 발생하면서 직렬화에 실패한다.
class UserInfo implements Serializable {
String name;
Object obj = new Object();
}
Object는 Serializable를 구현하지 않았기 때문에 Object의 인스턴스를 직렬화할수 없지만 인스턴스변수 obj의 타입이 직렬화가 안되는 Object여도 실제로 저장된 객체는 직렬화 가능한 클래스의 인스터스라면 직렬화가 가능하다.
....
Object obj = new String("문자");
....
인스턴스변수의 타입이 아닌 실제로 연결된 객체의 종류에 의해서 결정되기 때문이다.
- 직렬화가 안되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여서 직렬화 대상에서 제외되도록 할 수 있다.
class UserInfo implements Serializable {
String name;
transient String password;
int age;
transient Object obj = new Object();
}
제어자 transient가 붙은 인스턴스 변수는 직렬화 대상에서 제외되며 변수의 값은 그 타입의 기본값으로 직렬화된다.
obj와 password는 기본값인 null로 초기화 된다.
- 객체를 직렬화할 때의 순서와 역직렬화 할때의 순서가 일치해야 한다.
out.writeObject(us1);// => (UserInfo)in.readObject();
out.writeObject(us2);// => (UserInfo)in.readObject();
out.writeObject(us3);// => (UserInfo)in.readObject();
class SupueruUserInfo { boolean authority;}
public class UserInfo extends SupueruUserInfo implements Serializable {
String name;
String password;
int age;
// UserInfo() : 생성자에 의해 조상의 인스턴스 변수 authority를 true로 초기화
UserInfo(){this.name = "kim";this.password = "1234";this.age = 0; super.authority = true;}
UserInfo(String name, String password, int age){this.name = name;this.password = password ;this.age = age;}
public String toString() {return "name: " + name+", password: "+password+", age: "+age+ ", authority: "+ authority;}
}
// 직렬화
class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// objectfile.ser 파일에 객체를 직렬화 하여 저장
FileOutputStream fos = new FileOutputStream("objectfile.ser");
BufferedOutputStream bos = new BufferedOutputStream(fos);
ObjectOutputStream out = new ObjectOutputStream(bos);
UserInfo us1 = new UserInfo();
UserInfo us2 = new UserInfo("Kim","2222",26);
UserInfo us3 = new UserInfo("Lee","3333",20);
ArrayList<UserInfo> list = new ArrayList<>();
list.add(us1);
list.add(us2);
list.add(us3);
out.writeObject(list);
out.close();
}
}
그래서 직렬화할 객체가 많을 때는 개별적으로 직렬화 하는 것보다 List와 같은 컬렉션에 저장해서 직렬화 하는 것이 좋다.
컬렉션 하나만 역직렬화하면 되기 때문에 객체의 순서를 고려하지 않아도 되기 때문이다.
// 역직렬화
public class Sub {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// objectfile.ser 파일에 저장된 직렬화된 객체를 역직렬화
FileInputStream fis = new FileInputStream("objectfile.ser");
BufferedInputStream bis = new BufferedInputStream(fis);
ObjectInputStream in = new ObjectInputStream(bis);
// readObject()의 반환 타입은 Object로 형변환 필요
ArrayList list = (ArrayList) in.readObject();
in.close();
// true로 초기화되어도 직렬화 대상에 포함되지 않은 authority의 값은 기본값인 false로 초기화
System.out.println(list.get(0) ); // name: kim, password: 1234, age: 0, authority: false
System.out.println(list.get(1) ); // name: Kim, password: 2222, age: 26, authority: false
System.out.println(list.get(2) ); // name: Lee, password: 3333, age: 20, authority: false
}
}
List와 같은 객체를 직렬화 하면 List에 저장된 모든 객체들과 각 객체들의 인스턴스변수가 참조하고 있는 객체들까지 모두 직렬화 된다. 객체에 정의된 모든 인스턴스변수에 대한 참조를 찾아 들어가기 때문에 복잡하고 시간이 오래 걸리는 작업이 될 수 있다.
- 직렬화될 객체의 클래스에 readObject(), writeObject()를 정의해 직렬화 대상을 변경할 수 있다.
private void writeObject(ObjectOutputStream out)
throws IOException{
// 직렬화 될 대상 지정
out.defaultWriteObject();
out.writeBoolean(authority);
}
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException{
// 역직렬화 될 대상 지정
in.defaultReadObject();
authority = in.readBoolean();
}
}
기본적인 자바 직렬화 또는 역직렬화 과정에서 직렬화되지 않는 조상으로부터 상속받은 인스턴스변수에 대한 직렬화를 구현과 같은 별도의 처리가 필요할 때는 writeObject()와 readObject() 메서드를 직렬화될 객체의 클래스 내부에 선언해주면 된다.
1. writeObject()는 직렬화, readObject()는 역직렬화 작업 시 자동적으로 호출된다.
2. 두 메서드의 접근제어자는 private로 다른 접근 지정자로 지정한 경우 자동으로 호출되지 않는다.
3 .각 인스턴스변수의 타입에 맞는 booleanWrite()와 같은 메서드를 사용하여 인수로 넘기면 된다.
4. defaultWriteObject()는 직렬화될 객체의 클래스 내부에 정의된 인스턴스변수의 직렬화를 수행한다.
5. writeObject()와 readObject()에 정의된 직렬화 대상과 순서와 역직렬화할 대상과 순서가 일치해야 한다.
직렬화 가능한 클래스의 버전 관리
직렬화된 객체를 역직렬화할 때는 직렬화 했을 때와 같은 클래스를 사용해야 한다.
- 그러나 클래스의 이름이 같더라도 클래스의 내용이 변경된 경우 역직렬화에 실패하면 예외가 발생한다.
Exception in thread "main" java.io.InvalidClassException:
UserInfo; local class incompatible: stream classdesc
serialVersionUID = 4049974728620052005,
local class serialVersionUID = -3713004187774847561
직렬화 할 때와 역직렬화 할때의 클래스의 버전이 다르다는 뜻으로 객체가 직렬화 될때 클래스에 정의된 멤버들의 정보를 이용해서 serialVersionUID라는 클래스의 버전을 자동생성해서 직렬화 내용에 포함된다. 역직렬화 할 때 클래스의 버전을 비교함으로써 직렬화할 때의 클래스의 버전과 일치하는지 확일할 수 있는 것이다.
static변수나 상수,transient가 붙은 인스턴스 변수가 추가되는 경우에는 직렬화에 영향을 미치지 않기 때문에 클래스의 버전을 다르게 인식하도록 할 필요는 없다.
- 전송하는 경우 보내는 쪽과 받는 쪽이 모두 같은 클래스를 가지고 있어야 하는데 클래스가 조금만 변경되어도 프로그램을 관리하기 어렵게 만든다. 이럴 때는 클래스의 버전을 수동으로 관리해줄 필요가 있다.
public class UserInfo extends SupueruUserInfo implements Serializable {
private static final long serialVersionUID = 4049974728620052005L;
String name;
String password;
....
}
직렬화 가능한 클래스가 있을 때, 클래스의 버전을 수동으로 관리하려면 serialVersionUID를 추가로 정의해야 한다.이렇게 정의된 버전은 클래스 내용이 바뀌어도 클래스의 버전이 자동생성된 값으로 변경되지 않는다.
- serialver.exe는 클래스의 serialVersionUID를 자동생성해준다.
serialVersionUID의 값은 정수 값이라면 어떠한 값으로도 지정가능하지만 서로 다른 클래스 간의 같은 값을 갖지 않도록 를 사용해서 생성된 값을 사용하는 것이 보통이다.
serialver.exe는 serialVersionUID가 정의되어 있으면 그 값을 출력하고 정의되어 있지 않으면 자동 생성한 값을 출력한다. 이렇게 생성된 값은 클래스의 멤버들에 대한 정보를 바탕으로 생성되기 때문에 정보가 변경되지 않는한 항상 같은 값을 가진다.
'Java' 카테고리의 다른 글
[JAVA] 스트림(Stream) API(1) (0) | 2024.02.25 |
---|---|
[JAVA] 람다 표현식(Lambda Expression) (0) | 2024.01.28 |
[JAVA] 버퍼 스트림 (0) | 2024.01.18 |
[JAVA] 자바의 입출력과 스트림(I/O stream) (0) | 2024.01.18 |
[JAVA] 쓰레드의 동기화(synchronization) (0) | 2024.01.18 |