-
Java 입출력 - 2, 표준 입출력과 파일Java 2024. 2. 27. 15:20728x90반응형
표준입출력과 File
1. 표준입출력 - System.in, System.out, System.err
표준입출력은 콘솔(console, 도스창)을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미한다.
자바는 표준 입출력(standard I/O)을 위해 3가지 입출력 스트림,
System.in, System.out, System.err을 제공하는데,
이 들은 자바 어플리케이션의 실행과 동시에 사용할 수 있게 자동으로 생성되기 때문에,
개발자가 별도로 스트림을 생성하는 코드를 작성하지 않고도 사용이 가능하다.
이런 이유로 지금까지 스트림의 생성 없이 System.out을 사용해올 수 있었다.System.in 콘솔로부터 데이터를 입력받는데 사용 System.out 콘솔로 데이터를 출력하는데 사용 System.err 콘솔로 데이터를 출력하는데 사용
in, out, err은 클래스변수(static변수)다.
타입은 InputStream과 PrintStream이지만,
실제로는 BufferdInputStream과 BufferdOutputStream의 인스턴스를 사용한다.
참고 Editplus나 이클립스와 같은 에디터에서는 콘솔로의 출력을 중간에 가로채서 에디터에 뿌려주는 것이다.
Editplus의 설정화면에서 'Capture Output'옵션을 선택하면 콘솔에 출력하는 내용이 Editplus의 하단에 출력된다.
콘솔 입력은 버퍼를 가지고 있기 때문에 Backspace키를 이용해서 편집이 가능하며,
한 번에 버퍼의 크기만큼 입력이 가능하다.
입력의 끝을 알리기 전까지는 아직 데이터가 입력 중인 것으로 간주되어,
커서가 입력을 계속 기다리는 상태(블락킹 상태)에 머무르게 된다.
콘솔에 데이터를 입력하고 Enter키를 누르면,
입력대기상태에서 벗어나 입력된 데이터를 읽기 시작하고,
입력된 데이터를 모두 읽으면 다시 입력대기 상태가 된다.
참고 윈도우의 콘솔은 한 번에 최대 255자까지만 입력이 가능하다.
Enter키를 누르는 것은 두 개의 특수문자 '\r'과 '\n'이 입력된 것으로 간주된다.
문제는 Enter도 사용자입력으로 간주되어 매 입력마다 '\r'과 '\n'이 붙기 때문에,
이 들을 제거해주어야 하는 불편함이 있다.
이러한 불편함을 제거하기 위해,
System.in에 BufferReader를 이용해 readLine()을 통해 라인단위로 데이터를 입력받으면 된다.
텍스트기반의 사용자인터페이스 시대에 탄생한 C언어는 콘솔이 데이터를 입력받는 주요 수단이었지만,
자바가 탄생한 그래픽기반의 사용자인터페이스 시대는 콘솔을 통해 데이터를 입력받는 경우는 드물기 때문에,
Java에서 콘솔을 통한 입력에 대한 지원이 미약했다.
나중에 Scanner와 Console같은 클래스가 추가되면서 보완되었다.
2. 표준입출력의 대상변경 - setOut(), setErr(), setIn()
초기에는 System.in, System.out, System.err의 입출력대상이 콘솔화면이지만,
setOut(), setErr(), setIn()를 사용하면 입출력을 콘솔 이외에 다른 입출력 대상으로 변경하는 것이 가능하다.static void setOut(PrintStream out) System.out의 출력을 지정된 PrintStream으로 변경 static void setErr(PrintStream err) System.err의 출력을 지정된 PrintStream으로 변경 static void setIn(PrintStream in) System.in의 입력을 지정된 InputStream으로 변경
그러나 jdk1.5부터 Scanner 클래스가 제공되면서 System.in으로부터 데이터를 입력받아 작업하는 것이 편리해졌다.class StandardIOEx{ public static void main(String args[]){ Sysyem.out.println("out : Hello"); Sysyem.err.println("out : Hello"); } }
out, err 모두 출력대상이 콘솔이기 때문에 같은 결과를 얻는다.class StandardIOEx{ public static void main(String args[]){ PrintStream ps = null; FileOutputStream fos = null; try{ fos = new FileOutputStream("test.txt"); ps = new PrintStream(fos); System.setOut(ps); // System.out의 출력대상을 test.txt파일로 변경 } catch(FileNotFoundException e){ System.err.println("File not found."); } Sysyem.out.println("out : Hello"); Sysyem.err.println("out : Hello"); } }
out의 출력소스를 test.txt파일로 변경하였기 때문에 out을 이용한 출력은 모두 test.txt파일에 저장된다.
그래서 실행결과에는 err를 이용한 출력만 나타난다.
setOut같은 메서드를 사용하지 않고, 커맨드라인에서 표준입출력의 대상을 간단히 바꿀 수 있다.C:\jdk1.8\work\ch15>java StandardIOEx > output.txt
StandardIOEx의 out출력을 콘솔이 아닌 output.txt로 저장한다.
기존에 output.txt파일이 있었다면 기존의 내용은 삭제된다.
>와 달리 >>는 기존 내용의 마지막에 새로운 내용이 추가된다.C:\jdk1.8\work\ch15>java StandardIOEx >> output.txt
표준입력을 output.txt로 지정한다.
즉, 콘솔이 아닌 output.txt로부터 데이터를 입력받는다.C:\jdk1.8\work\ch15>java StandardIOEx < output.txt
3. RandomAccessFile
자바는 입력과 출력이 각각 분리되어 별도로 작업을 하도록 설계되어 있는데,
RandomAccessFile만은 하나의 클래스로 파일에 대한 입력과 출력을 모두 할 수 있도록 되어있다.
InputStream이나 OutputStream으로부터 상속받지 않고,
DataInput인터페이스와 DataOutput인터페이스를 모두 구현했기 때문에,
읽기와 쓰기가 모두 가능하다.
사실 DataInputStream은 DataInput을, DataOutputStream은 DataOutput을 구현했다.
이 두 클래스의 기본 자료형을 읽고 쓰기 위한 메서드들은 모두 이 2개의 인터페이스에 정의되어 있는 것들이다.
따라서 RandomAccessFile클래스도 기본자료형 단위로 데이터를 읽고 쓸 수 있다.
그래도 역시 가장 큰 장점은 파일의 어느 위치에나 읽기/쓰기가 가능하다는 것이다.
다른 입출력 클래스들은 입출력소스에 순차적으로 읽기/쓰기를 하기 때문에 읽기와 쓰기가 제한적인데,
RandomAccessFile 클래스는 파일에 읽고 쓰는 위치에 제한이 없다.
이것을 가능하게 하기 위해 내부적으로 파일 포인터를 사용하는데,
입출력 시에 작업이 수행되는 곳이 파일 포인터가 위치한 곳이 된다.
파일 포인터의 위치는 파일의 제일 첫 부분(0부터 시작)이며,
읽기 또는 쓰기를 수행할 때 마다 작업이 수행된 다음 위치로 이동하게 된다.
순차적으로 읽기나 쓰기를 한다면 파일 포인터를 이동시키기 위해 별도의 작업이 필요없지만,
파일의 임의의 위치에 있는 내용에 대해 작업하고자 한다면,
먼저 파일 포인터를 원하는 위치로 옮긴 다음 작업해야 한다
현재 작업 중인 파일에서 파일 포인터의 위치를 알고 싶을 때는 getFilePointer()를 사용하면 되고,
파일 포인터의 위치를 옮길 때는 seek(long pos)나 skipBytes(int n)를 사용하면 된다.
참고 사실 모든 입출력에 사용되는 클래스들은 입출력 시 다음 작업이 이루어질 위치를 저장하고 있는 포인터를 내부적으로 갖고 있다.
다만 내부적으로만 사용될 수 있기 때문에,
작업자가 포인터의 위치를 마음대로 변경할 수 없다는 것이 RandomAccessFile과 다른 점이다.
RandomAccessFile의 인스턴스를 "rw"mode로 생성할 때, 지정된 파일이 없으면 새로운 파일을 생성한다.
파일에 출력작업이 수행되었을 때 파일 포인터의 위치가 어떻게 달라지는 지에 대 보여준다.
int가 4byte이기 때문에 writeInt를 호출한 다음 파일 포인터의 위치가 0에서 4로 바뀐 것을 알 수 있다.import java.io.*; class RandomAccessFileEx{ public static void main(String args[]){ try{ RandomAccessFile raf = new RandomAccessFile("test.dat", "rw"); System.out.println("파일 포인터의 위치: " + raf.getFilePointer()); raf.writeInt(100); System.out.println("파일 포인터의 위치: " + raf.getFilePointer()); } catch(IOException e){ e.printStackTrace(); } } }
int 배열 score에 저장된 데이터를 score2.dat에 저장한 다음,
저장된 내용을 readInt로 읽어서 출력하도록 한 것.import java.io.*; class RandomAccessFileEx{ public static void main(String args[]){ int[] score = {1, 100, 90, 90, 2, 80, 90, 100, 3, 10, 20, 30, 4, 40, 50, 60, 5, 50, 60, 70}; try{ RandomAccessFile raf = new RandomAccessFile("score2.dat", "rw"); for(int i = 0; i < score.length; i++){ raf.writeInt(score[i]); } while(true){ System.out.println(raf.readInt()); } } catch(EOFException e){ // readInt()를 호출했을 때 더 이상 읽을 내용이 없으면 EOFException이 발생 } catch(IOException e){ e.printStackTrace(); } } }
하지만 score2.dat파일은 생성되지만 화면에 아무것도 출력되지 않는다.
writeInt()를 수행하면서 파일 포인터의 위치가 파일의 마지막으로 이동되었기 때문이다.
그 다음에 readInt()를 호출했으므로 파일의 앞부분이 아닌 마지막부분부터 읽기 시작하기 때문에,
아무것도 읽지 못하고 EOFException이 발생해서 무한반복문을 벗어나게 된다.
그래서 다음과 같이 seek(long pos)를 이용해서 파일포인터의 위치를 다시 처음으로 이동시킨 다음에 readInt()를 호출하도록 해야 한다.while(true){ System.out.println(raf.readInt()); } >> raf.seek(0); while(true){ System.out.println(raf.readInt()); }
이처럼 RandomAccessFile을 'rw(읽기쓰기)모드'로 생성해서 작업할 때는 이런 점을 염두에 두어야 한다.
4. File
파일은 기본적이면서도 가장 많이 사용되는 입출력 대상이기 때문에 중요하다.
그래서 관련된 내용뿐 아니라 다양한 예제를 실었다.
자바는 File클래스를 통해 파일과 디렉토리를 다룰 수 있도록 하고 있다.
그래서 File인스턴스는 파일일 수도 있고 디렉토리일 수도 있다.
File클래스의 생성자와 메서드를 관련된 것들 끼리 나누어서 예제와 함께 설명하고자 한다.static String pathSeparator OS에서 사용하는 경로 구분자. 윈도우 ";", 유닉스 ":" static char pathSeparator OS에서 사용하는 경로 구분자. 윈도우 ";", 유닉스 ":" static String separator OS에서 사용하는 경로 구분자. 윈도우 "\", 유닉스 "/" static String separator OS에서 사용하는 경로 구분자. 윈도우 "\", 유닉스 "/"
파일의 경로(path)와 디렉토리나 파일의 이름을 구분하는데 사용되는 구분자가 OS마다 다를 수 있기 때문에,
OS독립적으로 프로그램을 작성하기 위해서는 반드시 위의 멤버변수들을 이용해야 한다.
만일 윈도우에서 사용하는 구분자를 코드에 직접 적어 놓았다면,
이 코드는 다른 OS에서는 오류를 일으킬 수 있다.
절대경로(absolute path) 파일시스템의 루트(root)로부터 시작하는 파일의 전체 경로를 의미한다.
OS에 따라 다르지만, 하나의 파일에 대해 둘 이상의 절대경로가 존재할 수 있다.
현재 디렉토리를 의미하는 '.'와 같은 기호나 링크 등을 포함하지 않는 유일한 경로를 의미한다.
예를 들어 C:\jdk.18\work\ch15\FileEx.java의 또 다른 절대 경로는 C:\jdk.18\work\ch15\.\FileEx.java이 있지만,
정규경로는 C:\jdk.18\work\ch15\FileEx.java 단 하나 뿐이다.
시스템 속성 중에 user.dir의 값을 확인하면 현재 프로그램이 실행 중인 디렉토리를 알 수 있다.
그리고 우리가 OS의 시스템변수로 설정하는 classpath외에,
sun.boot.class.path라는 시스템속성에 기본적인 classpath가 있어서 기본적인 경로들은 이미 설정되어 있다.
그래서 처음 jdk설치 후 classpath를 따로 지정해주지 않아도 되는 것이다.
이 속성은 jdk1.2 이후 추가된 것이라 이전 버전에서는 rt.jar와 같은 파일을 classpath에 지정해야 했다.
주의할 점은 File인스턴스를 생성했다고 해서 파일이나 디렉토리가 생성되는 것은 아니다.
파일명이나 디렉토리명으로 지정된 문자열이 유효하지 않더라도 컴파일 에러나 예외를 발생시키지 않는다.
새로운 파일을 생성하기 위해서는 File인스턴스를 생성한 다음,
출력스트림을 생성하거나 createNewFile()을 호출해야 한다.File f = new File("c:\\jdk1.8\\work\\ch15", "FileEx.java"); // 이미 존재하는 파일을 참조할 때 File f = new File("c:\\jdk1.8\\work\\ch15", "NewFile.java"); // 파일을 새로 생성할 때 f.createNewFile();
p.920 ~ p.932 파일 입출력 예제들5. 직렬화(Serialization)
객체를 컴퓨터에 저장했다가 다음에 다시 꺼내 쓸 수는 없을지,
또는 네트웍을 통해 컴퓨터 간에 서로 객체를 주고받을 수는 없을까라고 고민해본 적이 있는가?
지금부터 배울 직렬화가 이러한 일들을 가능하게 해준다.
직렬화란 객체를 데이터 스트림으로 만드는 것을 뜬한다.
다시 얘기하면 객체에 저장된 데이터를 스트림에 쓰기(write) 위해 연속적인(serial) 데이터로 변환하는 것
반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)라고 한다.
사실 객체를 저장하거나 전송하려면 당연히 이렇게 할 수 밖에 없다.
객체는 클래스에 정의된 인스턴스변수의 집합이다.
객체에는 클래스변수나 메서드가 포함되지 않는다.
객체는 오직 인스턴스변수들로만 구성되어 있다.
전에는 이해를 돕기 위해 객체를 생성하면 인스턴스변수와 메서드를 함께 그리곤 했지만 사실 객체에는 메서드가 포함되지 않는다.
인스턴스변수는 인스턴스마다 다른 값을 가질 수 있어야하기 때문에 별도의 메모리공간이 필요하지만 메서드는 변하는 것이 아니라서,
메모리를 낭비해가면서 인스턴스마다 같은 내용의 메서드를 포함시킬 이유는 없다.
그래서 객체를 저장한다는 것은 바로 객체의 모든 인스턴스변수의 값을 저장한다는 것과 같다.
어떤 객체를 저장하고자 한다면, 현재 객체의 모든 인스턴스변수의 값을 저장하기만 하면 된다.
그리고 저장했던 객체를 다시 생성하려면,
객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스 변수에 저장하면 된다.
클래스에 정의된 인스턴스변수가 단순히 기본형일 때는 인스턴스변수의 값을 저장하는 일이 간단하지만,
참조형일 때는 그리 간단하지 않다.
예를 들어 인스턴스변수의 타입이 배열이라면 배열에 저장된 값들도 모두 저장되어야할 것이다.
그러나 우리는 객체를 어떻게 직렬화해야 하는지 전혀 고민하지 않아도 된다.
객체를 직렬화/역직렬화할 수 있는 ObjectInputStream/ObjectOutputStream을 사용하는 방법만 알면 된다.
참고 두 객체가 동일한지 판단하는 기준이 두 객체의 인스턴스 변수의 값들이 같고 다름이라는 것을 상기하자
5 - 1. ObjectInputStream/ObjectOutputStream
직렬화(스트림에 객체를 출력), 역직렬화(스트림에 객체를 입력)에는 ObjectOutputStream/ObjectInputStream을 사용한다.
각각 InputStream과 OutputStream을 직접 상속받지만 기반스트림을 필요로 하는 보조스트림이다.
그래서 객체를 생성할 때 입출력(직렬화/역직렬화)할 스트림을 지정해주어야 한다.ObjectInputStream(InputStream in) ObjectOutputStream(OutputStream out)
파일에 객체를 저장(직렬화)하고 싶다면FileOutputStream fos = new FileOutputStream("objectfile.ser"); ObjectOutputStream out = new ObjectOutputStream(fos); out.writeObject(new UserInfo());
위 코드는 objectfile.ser이라는 파일에 UserInfo객체를 직렬화하여 저장한다.
출력할 스트림(FileOutputStream)을 생성해서 이를 기반스트림으로 하는 ObjectOutputStream을 생성한다.
ObjectOutputStream의 writeObject(Object obj)를 사용해서 객체를 출력하면,
객체가 파일에 직렬화되어 저장한다.
역직렬화는 writeObject(Object obj)대신 readObject()를 사용하여 저장된 데이터를 읽기만 하면 객체로 역직렬화된다.
다만 readObject의 반환타입이 Object이기 때문에 객체 원래의 타입으로 형변환 해주어야 한다.FileInputStream fis = new FileInputStream("objectfile.ser"); ObjectInputStream in = new ObjectInputStream(fis); UserInfo info = (UserInfo)in.readObject(); // 원래는 자손 타입으로 바꿀 수 없지만, 자손 클래스를 부모 클래스로 형변환 시켜둔 상태일 때는 허용 단순히 자손타입으로 바꾸는 게 아니라, 부모가 되었다가 자손으로 다시 되돌아가는 느낌이라 허용
ObjectInputStream/ObjectOutputStream의 메서드들 중,
defaultReadObject()/defaultWriteObject()는 자동 직렬화를 수행하고,
나머지 메서드들은 직렬화와 역직렬화를 직접 구현할 때 주로 사용한다.
객체를 직렬화/역직렬화하는 작업은 객체의 모든 인스턴스 변수가 참조하고 있는 모든 객체에 대한 것이기 때문에 상당히 복잡하며 시간도 오래 걸린다.
readObject와 writeObject를 사용한 자동 직렬화가 편리하기는 하지만,
직렬화작업시간을 단축시키려면 직렬화하고자 하는 객체의 클래스에 추가적으로 2개의 메서드를 직접 구현해주어야 한다.private void writeObject(ObjectOutputStream out) throws IOException{ // write메서드를 사용해서 직렬화 수행 } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{ // read메서드를 사용해서 역직렬화 수행 }
6. 직렬화가 가능한 클래스 만들기 - Serializable, transient
직렬화하고자 하는 클래스가 java.io.Serializable 인터페이스를 구현하도록 하면 된다.
Serializable 인터페이스는 아무 내용 없는 빈 인터페이스지만,
직렬화를 고려하여 작성한 클래스인지를 판단하는 기준이 된다.
Serializable을 구현한 클래스를 상속받는다면,
Serializable을 구현하지 않아도 된다.public class SuperUserInfo implements Serializable{ String name; String password; } public class UserInfo extends SuperUserInfo{ int age; }
위 예제는 name, password, age 모두 직렬화된다.public class SuperUserInfo{ String name; String password; } public class UserInfo extends SuperUserInfo implements Serializable{ int age; }
위 예제는 name과 password는 직렬화되지 않는다.
name, password를 직렬화 대상에 포함시키려면,
조상 클래스가 Serializable을 구현하도록 하던가,
UserInfo에서 조상의 인스턴스변수들이 직렬화되도록 처리하는 코드를 직접 추가해야 한다.public class SuperUserInfo implements Serializable{ String name; String password; } public class UserInfo extends SuperUserInfo{ int age; Object obj = new Object(); // Object객체는 직렬화 안됨 }
모든 클래스의 최고조상인 Object는 Serializable을 구현하지 않았기 때문에 직렬화할 수 없다.
직렬화하면 java.io.NotSerializableException 발생
하지만 다음의 경우는 가능하다public class UserInfo extends SuperUserInfo{ int age; Object obj = new String(); }
인스턴스변수의 타입이 아닌 실제로 연결된 객체의 종류에 의해 결정된다.
직렬화하고자 하는 객체의 클래스에 직렬화가 안되는 객체에 대한 참조를 포함하고 있다면,
transient를 붙여 직렬화 대상에서 제외되게 할 수 있다.
또는 password같이 보안상 직렬화하면 안되는 값에 대해 transient를 사용할 수 있다.import java.io.*; import java.util.ArrayList; public class SerialEx{ public static void main(String args[]){ try{ String fileName = "UserInfo.ser"; FileOutputStream fos = new FileOutputStream(fileName); BufferdOutputStream bos = new BufferdOutputStream(fos); ObjectOutputStream out = new ObjectOutputStream(bos); UserInfo u1 = new UserInfo("JavaMan", "1234", 30); UserInfo u2 = new UserInfo("JavaWoman", "4321", 25); ArrayList<UserInfo> list = new ArrayList<>(); list.add(u1); list.add(u2); //직렬화 out.writeObject(u1); out.writeObject(u2); out.writeObject(list); out.close(); System.out.println("직렬화 끝"); } catch(IOException e){ e.printStackTrace(); } } } 결과 직렬화 끝
생성된 객체를 직렬화하여 파일에 저장하는 예제이다.
객체를 직렬화하는 것은 간단하지만,
객체에 정의된 모든 인스턴스변수에 대한 참조를 찾아들어가기 때문에,
상당히 복잡하고 시간이 걸리는 작업이 될 수 있다.
이 예제처럼 ArrayList같은 객체를 직렬화하면,
ArrayList에 저장된 모든 객체들과, 각 객체의 인스턴스변수가 참조하고 있는 객체들까지 모두 직렬화된다.
참고 확장자를 직렬화(Serialization)의 약자인 'ser'로 하는 것이 보통이지만 문법적인 제약은 없다.import java.io.*; import java.util.ArrayList; public class SerialEx{ public static void main(String args[]){ try{ String fileName = "UserInfo.ser"; FileInputStream fis = new FileInputStream(fileName); BufferdInputStream bis = new BufferdInputStream(fis); ObjectInputStream in = new ObjectInputStream(bis); // 객체를 읽을 때는 출력한 순서와 일치해야한다. UserInfo u1 = (UserInfo)in.readObject(); UserInfo u2 = (UserInfo)in.readObject(); ArrayList list = (ArrayList)in.readObject(); System.out.println(u1); System.out.println(u2); System.out.println(list); in.close(); } catch(IOException e){ e.printStackTrace(); } } } (JavaMan, 1234, 30) (JavaWoman, 4321, 25) [(JavaMan, 1234, 30), (JavaWoman, 4321, 25)]
직렬화한 객체를 역직렬화하는 예제이다.
FileInputStream과 ObjectInputStream을, writeObject 대신 readObject를 썻다는 점을 빼면 직렬화와 거의 같다.
read.Object()의 반환타입이 Object이므로 원래 타입으로 형변환해줬다.
역직렬화할 때는 직렬화할 때의 순서와 일치해야 한다는 것에 주의
그래서 직렬화할 객체가 많을 때는
각 객체를 개별적으로 직렬화하는 것보다 ArrayList같은 컬렉션에 저장해서 직렬화하는 것이 좋다.
역직렬화할 때 ArrayList 하나만 역직렬화 하면 되므로,
역직렬화할 객체의 순서를 고려하지 않아도 되기 때문이다.public class SuperUserInfo{ String name; String password; } public class UserInfo extends SuperUserInfo implements Serializable{ int age; }
아까의 예제이다.private void writeObject(ObjectOutputStream out) throws IOException{ out.writeUTF(name); out.writeUTF(password); out.defaultWriteObject(); } private void readObject(ObjectInputStream in) throws IOException{ name = in.readUTF(); password = in.readUTF(); in.defaultReadObject(); }
이 2개의 메서드를 추가해주면 조상으로부터 상속받은 인스턴스변수 name, password도 직렬화된다.
이 메서드들은 직렬화/역직렬화 작업시에 자동적으로 호출된다.
두 메서드의 접근자가 private인 건 그냥 단순히 미리 정해둔 규칙일 뿐이다.
name, password의 타입이 String이기 때문에 writeUTF/readUTF를 사용했고,
이 외에도 ObjectInputStream/ObjectOutputStream에는 writeInt(), readInt()같은 타입에 따른 다양한 종류의 메서드를 제공하므로, 각 인스턴스변수의 타입에 맞는 것을 선택해서 사용하면 된다.
그리고 defaultWriteObject()는 자신의 클래스에 정의된 인스턴스변수의 직렬화를 수행한다.
6. 직렬화가능한 클래스의 버전관리
직렬화된 객체를 역직렬화할 때는 직렬화했을 때와 같은 클래스를 사용해야 한다.
그러나 클래스 이름이 같더라도 클래스의 내용이 변경된 경우 역직렬화는 실패하며 예외가 발생한다.java.io.InvalidclassException: UserInfo; local class incompatible:stream classdesc serialVersionUID = ~~~~, local class serialVersionUID = ~~~~~
직렬화할 때와 역직렬화할 때의 클래스 버전이 같아야 하는데 다르다는 것이다.
객체가 직렬화될 때 클래스에 정의된 멤버들의 정보를 이용해서 serialVersionUID라는 클래스의 버전을 자동생성해서 직렬화 내용에 포함된다.
그래서 역직렬화할 때 클래스의 버전을 비교함으로써 직렬화할 때의 클래스의 버전과 일치하는지 확인할 수 있는 것이다.
그러나 static변수나 상수 또는 reansient가 붙은 인스턴스변수가 추가되는 경우에는,
직렬화에 영향을 미치지 않기 때문에 클래스의 버전을 다르게 인식하도록 할 필요는 없다.
네트웍으로 객체를 직렬화하여 전송하는 경우,
보내는 쪽과 받는 쪽이 모두 같은 버전의 클래스를 가지고 있어야하는데,
클래스가 조금만 변경되어도 해당 클래스를 재배포하는 것은 프로그램을 관리하기 어렵게 만든다.
이럴 때는 클래스의 버전을 수동으로 관리해줄 필요가 있다.class MyData implements java.io.Serializable{ int value1; }
위와 같이 MyData라는 직렬화 가능한 클래스가 있을 때,
클래스 버전을 수동으로 관리하려면 serialVersionUID를 추가로 정의해야 한다.class MyData implements java.io.Serializable{ static final long serialVersionUID = 3518731767529258119L; int value1; }
이렇게 클래스 내에 serialVersionUID를 정의해주면,
클래스의 내용이 바뀌어도 클래스의 버전이 자동생성된 값으로 변경되지 않는다.
serialVersionUID 값은 정수값이면 어떤 값으로도 지정할 수 있지만,
서로 다른 클래스간에 같은 값을 갖지 않도록 seialver.exe를 사용해서 생성된 값을 사용하는 것이 보통이다.C:\jdk1.8\work\ch15> serialver MyData MyData: static final long serialVersionUID = 3518731767529258119L;
serialver.exe 뒤에 serialVersionUID를 얻고자 하는 클래스의 이름만 적어주면,
클래스의 serialVersionUID를 알아낼 수 있다.
serialver.exe는 클래스에 serialVersionUID가 정의되어 있으면 그 값을 출력하고,
정의되어 있지 않으면 자동 생성한 값을 출력한다.
serialver.exe에 의해 생성되는 serialVersionUID값은 클래스의 멤버들에 대한 정보를 바탕으로 하기 때문에,
이 정보가 변경되지 않는 한 항상 같은 값을 생성한다.728x90반응형'Java' 카테고리의 다른 글
Java JDBC (0) 2024.02.27 Java 네트워킹 (1) 2024.02.27 Java 입출력 - 1, 스트림(I/O Stream) (1) 2024.02.27 Java 스트림(Stream) (1) 2024.02.27 Java 람다, 람다식 (3) 2024.02.26