자바의 정석 15장 - 입출력 (I/O)

청포도봉봉이·2024년 2월 26일
1

자바의 정석

목록 보기
15/16
post-thumbnail

자바에서의 입출력

입출력이란?

I/O란 Input과 Ouput의 약자로 입력과 출력, 간단히 줄여서 입출력이라고 한다. 입출력은 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고받는 것을 말한다.

예를 들면 키보드로 부터 데이터를 입력 받는다든가 System.out.println()을 이용해서 화면에 출력한다던가 하는 것이 가장 기본적인 입출력의 예이다.

스트림(stream)

자바에서 입출력을 수행하려면, 즉 어느 한쪽에서 다른 쪽으로 데이터를 전달하려면 두 대상을 연결하고 데이터를 전송할 수 있는 무언가가 필요한데 이것을 스트림(stream)이라고 정의했다. 입출력에서는 14장의 스트림과 같은 용어를 쓰지만 다른 개념이다.

스트림이란 데이터를 운반하는데 사용되는 연결통로이다.

스트림은 연속적인 데이터의 흐름을 물에 비유해서 붙여진 이름인데, 물이 한쪽 방향으로만 흐르는 것과 같이 스트림은 단방향통신만 가능하기 때문에 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없다.

그래서 입력과 출력을 모두 수행하려면 입력스트림(input stream)과 출력스트림(output stream), 모두 2개의 스트림이 필요하다.

스트림은 먼저 보낸 데이터를 멈ㄴ저 받게 되어 있으며 중간에 건너뜀 없이 연속적으로 데이터를 주고 받는다. 큐(queue)와 같은 FIFO(First In First Out)구조로 되어 있다고 생각하면 된다.

바이트기반 스트림 - InputStream, OutputStream

스트림은 바이트단위로 데이터를 전송하며 입출력 대상에 따라 다음과 같은 입출력스트림이 있다.

위 표의 입출력 스트림은 InputStream과 OutputStream의 자손들이다.

입출력을 처리할 수 있는 표준화된 방법으로 제공함으로써 대상이 달라져도 동일한 방법으로 입출력이 가능하다.

보조 스트림

스트림의 기능을 보완하기 위한 보조스트림이 제공된다. 보조스트림은 실제 데이터를 주고받는 스트림이 아니기 때문에 데이터를 입출력할 수 있는 기능은 없지만, 스트림의 기능을 향상시키거나 새로운 기능을 추가할 수 있다. 그래서 보조 스트림만으로는 입출력을 처리할 수 없고, 스트림을 먼저 생성한 다음에 이를 이용해서 보조스트림을 생성해야 한다.

// 먼저 기반스트림을 생성한다.
FileInputStream fis = new FileInputStream("test.txt");

// 기반스트림을 이용해서 보조스트림을 생성한다.
BufferedInputStream bis = new BufferedInputStream(fis);

bis.read(); // 보조스트림인 BufferedInputStream으로부터 데이터를 읽는다.

코드 상으로는 보조스트림인 BufferedInputStream이 입력기능을 수행하는 것처럼 보이지만, 실제 입력 기능은 FileInputStream이 수행하고, 보조스트림인 BufferedInputStream는 버퍼만을 제공한다. 버퍼를 사용한 입출력과 사용하지 않은 입출력간의 성능차이는 상당하기 때문에 대부분의 경우에 버퍼를 이용한 보조스트림을 사용한다.

문자기반 스트림 - Reader, Writer

지금까지 알아본 스트림은 모두 바이트기반의 스트림이다. 바이트기반이라 함은 입출력 단위가 1byte라는 뜻이다. Java에서는 한 문자를 의미하는 char형이 1 byte가 아니라 2 byte이기 때문에 바이트기반의 스트림으로 2 byte 문자를 처리하는 데는 어려움이 있다.

이 점을 보완하기 위해서 문자기반의 스트림이 제공된다.

InputStream -> Reader
OutputStream -> Writer

아래의 표는 바이트 기반 스트림과 문자 기반 스트림의 읽고 쓰는 메소드의 비교이다.

바이트기반 스트림

InputStream과 OutputStream

아래 표는 InputStream의 메서드이다.

아래 표는 OutputStream의 메서드이다.

프로그램이 종료될 때, 사용하고 닫지 않은 스트림을 JVM이 자동적으로 닫아 주기는 하지만, 스트림을 사용해서 모든 작업을 마치고 난 후에는 close()를 호출해서 반드시 닫아 주어야 한다.

ByteArrayInputStream과 ByteArrayOutputStream

ByteArrayInputStream / ByteArrayOutputStream은 메모리, 즉 바이트배열에 데이터를 입출력 하는데 사용되는 스트림이다. 주로 다른 곳에 입출력하기 전에 데이터를 임시적으로 바이트배열에 담아서 변환 등의 작업을 하는데 사용한다.

import java.io.*;
import java.util.Arrays;

public class Ex15_1 {

	public static void main(String[] args) {
		byte[] inSrc = {0,1,2,3,4,5,6,7,8,9};
		byte[] outSrc = null;
		
		ByteArrayInputStream input = null;
		ByteArrayOutputStream output = null;
		
		input = new ByteArrayInputStream(inSrc);
		output = new ByteArrayOutputStream();
		
		int data = 0;
		
		while((data = input.read())!=-1)
			output.write(data); // void write(int b)
		
		outSrc = output.toByteArray();
		
		System.out.println("Input Source : " + Arrays.toString(inSrc));
		System.out.println("Output Source:" + Arrays.toString(outSrc));

	}

}
(data = input.read()) != -1

1. data = input.read() // read()를 호출한 반환값을 변수 data에 저장
2. data != -1 // data에 저장된 값이 -1이 아닌지 비교한다.

read()와 write(int b)를 사용하기 때문에 한번에 1 byte만 읽고 쓰므로 작업효율이 떨어진다. 아래 코드는 그걸 개선한 코드이다.

import java.io.*;
import java.util.Arrays;

public class Ex15_2 {

	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[10];
		
		ByteArrayInputStream input = null;
		ByteArrayOutputStream output = null;
		
		input = new ByteArrayInputStream(inSrc);
		output = new ByteArrayOutputStream();
		
		input.read(temp,0,temp.length);
		output.write(temp, 5, 5);
		
		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));
		

	}

}

바이트배열은 사용하는 자원이 메모리 밖에 없으므로 가비지컬렉터에 의해 자동적으로 자원을 반환하므로 close()을 이용해서 스트림을 닫지 않아도 된다.

input.read(tmp, 0, tmp.length) // 읽어 온 데이터를 배열 tmp에 담는다.
output.write(temp, 5, 5); // temp[5]부터 5개의 데이터를 wrtie한다.

read()나 write()이 IOException을 발생시킬 수 있기 때문에 try-catch문으로 감싸주어야 한다.

FileInputStream과 FileOutputStream

FileInputStream / FileOutputStream은 파일에 입출력을 하기 위한 스트림이다.

import java.io.*;

public 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); // void write(int b)
			
			fis.close();
			fos.close();
		}catch(IOException e) {
			e.printStackTrace();
		}

	}

}

단순히 FIleCopy.java의 내용을 read()로 읽어서 write(int b)로 FIleCopy.bak에 출력한다.

이처럼 텍스트파일을 다루는 경우에는 FileInputStream / FileOutputStream 대신 문자기반의 스트림인 FileReader / FileWriter를 사용하는 것이 더 좋다.

바이트기반의 보조 스트림

FilterInputStream과 FilterOutputStream

FilterInputStream / FilterOutputStream은 InputStream과 OutputStream의 자손이면서 모든 보조스트림의 조상이다. 보조스트림은 자체적으로 입출력을 수행할 수 없기 때문에 기반스트림을 필요로 한다.

protected FilterInputStream(InputStream in)
public FilterOutputStream(OutputStream out)

FilterInputStream / FilterOutputStream의 모든 메소드는 단순히 기반스트림의 메소드를 그대로 호출한다. 즉, FilterInputStream / FilterOutputStream 자체로는 아무런 일도 하지 않음을 의미한다. 다음은 FilterInputStream/FilterOutputStream의 생성자이다.

FilterInputStream / FilterOutputStream은 상속을 통해 원하는 작업을 수행하도록 읽고 쓰는 메소드를 오버라이딩해야 한다.

public class FilterInputStream extends InputStream {
    protected volatile InputStream in;
    
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
    
    public int read() throws IOException {
        return in.read();
    }
    ...
}

생성자 FilterInputStream(InputStream in)는 접근 제어자가 protected이기 때문에 Filter InputStream의 인스턴스를 생성해서 사용할 수 없고 상속을 통해서 오버라이딩되어야 한다.

FilterInputStream/FilterOutputStream을 상속받아서 기반스트림에 보조기능을 추가한 보조스트림 클래스는 다음과 같다.

FilterInputStream의 자손 :
BufferedInputStream, DataInputStream, PushbackInputStream 등

FilterOutputStream의 자손 :
BufferedOutputStream, DataOutputStream, PrintStream 등

BufferedInputStream과 BufferedOutputStream

BufferedInputStream / BufferedOutputStream은 스트림의 입출력 효율을 높이기 위해 버퍼를 사용하는 보조스트림이다. 한 바이트씩 입출력하는 것 보다는 버퍼(바이트배열)를 이용해서 한 번에 여러 바이트를 입출력하는 것이 빠르기 때문에 대부분의 입출력 작업에 사용된다.

프로그램에서 입력소스로부터 데이터를 읽기 위해 처음으로 read메소드를 호출하면, BufferedInputStream은 입력소스로부터 버퍼 크기만큼의 데이터를 읽어다 자신의 내부 버퍼에 저장한다.
프로그램에서는 BufferedInputStream의 버퍼에 저장된 데이터를 읽으면 된다.
외부의 입력소스로부터 읽는 것보다 내부의 버퍼로부터 읽는 것이 훨씬 빠르기 때문에 그만큼 작업 효율이 높아진다.
프로그램에서 버퍼에 저장된 모든 데이터를 다 읽고 그 다음 데이터를 읽기 위해 read메소드가 호출되면 BufferedInputStream은 입력소스로부터 다시 버퍼크기 만큼의 데이터를 읽어다 버퍼에 저장해 놓는다.

BufferedOutputStream 역시 버퍼를 이용해서 출력소스와 작업을 하게 되는데, 입력 소스로부터 데이터를 읽을 때와는 반대로, 프로그램에서 write 메소드를 이용한 출력이 BufferedOutputStream의 버퍼에 저장된다. 버퍼가 가득 차면, 그 때 버퍼의 모든 내용을 출력소스에 출력한다. 그리고 버퍼를 비우고 다시 프로그램으로부터의 출력을 저장할 준비를 한다.

버퍼가 가득 찼을 때만 출력소스에 출력을 하기 때문에, 마지막 출력부분이 출력소스에 쓰이지 못하고 BufferedOutputStream에 남아있는 채로 프로그램이 종료될 수 있다는 점을 주의해야 한다.

import java.io.*;

public class Ex15_6 {

	public static void main(String[] args) {
		try {
			FileOutputStream fos = new FileOutputStream("123.txt");
			// BufferedOutputStream의 버퍼 크기를 5로 한다.
			BufferedOutputStream bos = new BufferedOutputStream(fos, 5);
			// 파일 123.txt에 1부터 9까지 출력한다
			for(int i = '1' ; i <= '9'; i++) {
				bos.write(1);
			}
			fos.close(); //FileOutputStream을 닫는다.
		} catch(IOException e) {
			e.printStackTrace();
		}

	}

}

위 예제로 생성된 123.txt에는 12345만 저장되는데, 버퍼에 남아있는 데이터가 출력되지 못한 상태로 프로그램이 종료되었기 때문이다.

DataInputStream과 DataOutputStream

각각 FilterInputStream/FilterOutputStream의 자손이다.

DataInputStream은 DataInput 인터페이스를, DataOutputStream은 DataOutput 인터페이스를 구현했기 때문에 데이터를 읽고 쓰는데 byte 단위가 아닌, 8가지 기본 자료형의 단위로 읽고 쓸 수 있다는 장점이 있다.

DataInputStream의 메서드

DataOutputStream의 메서드

SequenceInputStream

SequenceInputStream은 여러 개의 입력스트림을 연속적으로 연결해서 하나의 스트림으로부터 데이터를 읽는 것과 같이 처리할 수 있도록 도와준다. SequenceInputStream의 생성자를 제외하고 나머지 작업은 다른 입력스트림과 같다. 큰 파일을 여러 개의 작은 파일로 나누었다가 하나의 파일로 합치는 것과 같은 작업을 수행할 때 사용하면좋을 것이다.

// Vector에 저장된 순서대로 입력됨
import java.io.*;
import java.util.*;

class Ex15_7{
	public static void main(String[] args){
    	byte[] arr1 = {0,1,2};
        byte[] arr2 = {3,4,5};
        byte[] arr3 = {6,7,8};
        byte[] outSrc = null;
        
        Vector v = new Vector();
        v.add(new ByteArrayInputStream(arr1));
        v.add(new ByteArrayInputStream(arr2));
        v.add(new ByteArrayInputStream(arr3));
        
        SequenceInputStream input = new SequenceInputStream(v.elements());
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        
        int data = 0;
        try{
        	while((data=input.read()) != -1)[
            	output.write(data);
           	}
       	} catch(IOException e) {}
        
        outSrc = output.toByteArray();
        
        System.out.println("Input Source1  :" + Arrays.toString(arr1));
		System.out.println("Input Source2  :" + Arrays.toString(arr2));
		System.out.println("Input Source3  :" + Arrays.toString(arr3));
	    System.out.println("Output Source  :" + Arrays.toString(outSrc));
    }
}

PrintStream

PrintStream은 데이터를 기반스트림에 다양한 형태로 출력할 수 있는 print, println, printf와 같은 메소드를 오버로딩하여 제공한다.

PrintStream은 데이터를 적절한 문자로 출력하는 것이기 때문에 문자기반 스트림의 역할을 수행한다.

JDK 1.1부터 PrintStream보다 향상된 기능의 문자기반 스트림인 PrintWriter가 추가되었으나 System.out이 PrintStream이다 보니 둘 다 사용하게 되었다.

PrintStream와 PrintWriter는 거의 같은 기능을 가지고 있지만 PrintWriter가 PrintStream에 비해 다양한 언어의 문자를 처리하는데 적합하기 때문에 가능하면 PrintWriter를 사용하는 것이 좋다.

print()나 println()을 이용해서 출력하는 중에 PrintStream의 기반스트림에서 IOException이 발생하면 checkError()를 통해 인지할 수 있다.

println()이나 print()는 예외를 던지지 않고 내부에서 처리하도록 정의되었는데, 그 이유는 println()과 같은 메소드가 자주 사용되기 때문이다.

문자기반 스트림

Reader와 Writer

바이트기반 스트림의 조상이 InputStream/OutputStream인 것과 같이 문자기반의 스트림에서는 Reader/Writer가 그와 같은 역할이다.

Reader/Writer는 byte배열 대신 char배열을 사용한다는 것 외에는 InputStream/OutputStream의 메소드와 다르지 않다.

Reader의 메서드

Writer의 메서드

문자기반 스트림이라는 것이 단순히 2 byte로 스트림을 처리하는 것만이 아니라, 인코딩(encoding)기능 또한 포함되어 있다.

문자기반 스트림인 Reader/Writer, 그리고 그 자손들은 여러 종류의 인코딩과 자바에서 사용하는 유니코드(UTF-16)간의 변환을 자동적으로 처리해준다.

Reader는 특정 인코딩을 읽어서 유니코드로 변환하고 Writer는 유니코드를 특정 인코딩으로 변환하여 저장한다.

FileReader 와 FileWriter

FileInputStream,OutputStream과 사용법이 같다. 파일로 부터 텍스트 데이터를 읽고, 파일을 쓰는데 사용한다.

바이트 기반의 FileInputStream과 FileReader의 차이점은 같은 내용의 파일을 읽어도 FileInputStream 한글이 깨져서 출력되는 걸 볼 수 있다.

PipedReader와 PipedWriter

PipedReader / PipedWriter는 쓰레드 간에 데이터를 주고받을 때 사용된다. 다른 스트림과는 달리 입력과 출력스트림을 하나의 스트림으로 연결해서 데이터를 주고받는다는 특징이 있다.

스트림을 생성한 다음 어느 한쪽 쓰레드에서 connect()를 호출해서 입력스트림과 출력스트림을 연결한다. 입출력을 마친 후에는 어느 한쪽 스트림만 닫아도 나머지 스트림은 자동으로 닫힌다.

StringReader와 StringWriter

입출력 대상이 메모리인 스트림이다. StringWriter에 의해 출력되는 데이터는 내부의 StringBuffer에 저장된다.

StringBuffer getBuffer() : StringWriter에 출력한 데이터가 저장된 StringBuffer를 반환

String toString() : StringWriter에 출력된(StringBuffer에 저장된) 문자열을 반환

문자 기반의 보조스트림

BufferedReader와 BufferedWriter

BufferedReader / BufferedWriter는 버퍼를 이용해 입출력의 효율을 높일 수 있도록 한다. 버퍼를 이용하면 입출력의 효율이 매우 좋아진다. readLine()은 데이터를 라인 단위로 읽게 해주는 메서드이고 newLine()은 줄바꿈을 해주는 메서드이다.

InputStreamReader & OutputStreamWriter

바이트 기반 스트림을 문자기반 스트림으로 연결시켜주는 역할을 한다. 그리고 바이트기반 스트림의 데이터를 지정된 인코딩의 문자데이터로 변환하는 작업을 수행한다.

InputStreamReader 생성자와 메서드

OutputStreamWriter 생성자와 메서드

표준입출력과 File

표준입출력 - System.in, System.out, System.err

콘솔을 통한 데이터 입력과 콘솔로의 데이터 출력을 의미한다. 자바에서는 표준 입출력(standard I/O)을 위해 3가지 입출력 스트림인 System.in, System.out, System.err을 제공하는데, 이들은 자바 어플리케이션의 실행과 동시에 사용할 수 있께 자동적으로 생성되기 때문에 개발자가 별도로 스트림을 생성하는 코드를 작성하지 않고도 사용이 가능하다.

System.in : 콘솔로부터 데이터를 입력받는데 사용
System.out : 콘솔로 데이터를 출력하는데 사용
System.err : 콘솔로 데이터를 출력하는데 사용

System클래스의 소스에서 알 수 있듯이 in, out, err는 System클래스에 선언된 클래스 변수(static 변수)이다. 선언부분은 PrintStream이지만 실제로는 버퍼를 이용하는 BufferedInputStream과 BufferedOutputStream의 인스턴스를 사용한다.

public final class System{
	public final static InputStream in = nullInputStream();
    public final static PrintStream out = nullPrintStream();
    public final static PrintStream err = nullPrintStream();
    ...
}

표준입출력의 대상변경 - setOut(), setErr(), setIn()

그러나 JDK1.5부터 Scanner 클래스가 제공되면서 System.in으로부터 데이터를 입력받아 작업하는 것이 편리해졌다.

System.out, System.err 모두 출력대상이 콘솔이기 때문에 둘 다 같은 결과를 얻는다.

RandomAccessFile

자바에서는 입력과 출력이 각각 분리되어 별도로 작업을 하도록 설계되어 있는데, RandomAccessFile만은 하나의 클래스로 파일에 대한 입출력을 모두 할 수 있도록 되어 있다.

사실 DataInputStream은 DataInput 인터페이스를, DataOuputStream은 DataOuput 인터페이스를 구현했다. 이 두 클래스의 기본 자료형을 읽고 쓰기 위한 메서드들은 모두 이 2개의 인터페이스에 정의되어 있는 것들이다. 따라서 RandomAccessFile 클래스도 기본자료형 단위로 데이터를 읽고 쓸 수 있다.

그래도 역시 RandomAccessFile 클래스의 장점은 파일의 어느 위치에나 읽기/쓰기가 가는ㅇ하다는 것이다.

이것을 가능하게 하기 위해서는 내부적으로 파일 포인터를 사용하는데, 입출력 시에 작업이 수행되는 곳이 바로 파일 포인터가 위치한 곳이 된다.

File

파일은 기본적이면서도 가장 많이 사용되는 입출력 대상이다. 자바에서는 File 클래스를 통해서 파일과 디렉토리를 다룰 수 있다.

File의 생성자와 경로와 관련된 메서드

경로와 관련된 File의 멤버변수

import java.io.*;

class FileEx1 {
	public static void main(String[] args) throws IOException
	{
		File f = new File("c:\\jdk1.8\\work\\ch15\\FileEx1.java");
		String fileName = f.getName();
		int pos = fileName.lastIndexOf(".");

		System.out.println("경로를 제외한 파일이름 - " + f.getName());
		System.out.println("확장자를 제외한 파일이름 - " + fileName.substring(0,pos));
		System.out.println("확장자 - " + fileName.substring(pos+1));

		System.out.println("경로를 포함한 파일이름 - "		+ f.getPath());
		System.out.println("파일의 절대경로        - "	+ f.getAbsolutePath());
		System.out.println("파일의 정규경로        - "	+ f.getCanonicalPath());
		System.out.println("파일이 속해 있는 디렉토리 - "	+ f.getParent());
		System.out.println();
		System.out.println("File.pathSeparator - "		+ File.pathSeparator);
		System.out.println("File.pathSeparatorChar - "  + File.pathSeparatorChar);
		System.out.println("File.separator - "		+ File.separator);
		System.out.println("File.separatorChar - "	+ File.separatorChar);
		System.out.println();
		System.out.println("user.dir=" + System.getProperty("user.dir"));
		System.out.println("sun.boot.class.path=" + System.getProperty("sun.boot.class.path"));
	}
}

출력 결과
경로를 제외한 파일이름 - FileEx1.java
확장자를 제외한 파일이름 - FileEx1
확장자 - java
경로를 포함한 파일이름 - c:\jdk1.8\work\ch15\FileEx1.java
파일의 절대경로(루트부터 파일의 전체 경로)- c:\jdk1.8\work\ch15\FileEx1.java
파일의 정규경로(.을 포함하지 않는 경로) - C:\jdk1.8\work\ch15\FileEx1.java
파일이 속해 있는 디렉토리 - c:\jdk1.8\work\ch15

File.pathSeparator - ;
File.pathSeparatorChar - ;
File.separator - \
File.separatorChar - \

user.dir=C:\Users\appti\eclipse-workspace\ch15
sun.boot.class.path=null

File 인스턴스를 생성했다고 해서 파일이나 디렉토리가 생성되는 것은 아니므로 새로운 파일을 생성할 때에는 주의해야 한다.

// 1. 이미 존재하는 파일을 참조할 때 :
File f = new File("c:\\jdk1.8\\work\\ch15", "FileEx1.java");

// 2. 기존에 없는 파일을 새로 생성할 때 :
File f = new File("c:\\jdk1.8\\work\\ch15", "NewFile.java");
f.craeteNewFile();

File의 메서드

직렬화(Serialization)

객체를 컴퓨터에 저장했다가 다음에 다시 꺼내 쓸 수는 없을지 또는 네트웍을 통해 컴퓨터 간에 서로 객체를 주고 받을 수는 없을까라고 고민해 본적이 있는가? 이러한 일들은 가능하다. 지금부터 배울 직렬화(Serialization)가 이러한 일들을 가능하게 해준다.

직렬화란?

직렬화(Serialization)란 객체를 데이터 스트림으로 만드는 것을 의미한다. 다시 얘기하면 객체에 저장된 데이터를 스트림에 쓰기(write)위한 연속적인(serial) 데이터로 변환하는 것을 말한다.

반대로 스트림으로부터 데이터를 읽어서 객체를 만드는 것을 역직렬화(deserialization)라고 한다.

직렬화라는 용어 때문에 괜히 어렵게 느껴질 수 있는데 사실 객체를 저장하거나 전송하려면 당연히 이렇게 할 수 밖에 없다.

객체가 무엇이고 저장하는 것은 무엇을 의미하는가에 대해서 다시 한 번 정리하고 넘어가는 것이 좋을 것 같다.

  • 객체는 클래스에 정의된 인스턴스변수의 집합이다.
  • 객체에는 클래스변수나 메소드가 포함되지 않는다.
  • 객체는 오직 인스턴스 변수들로만 구성되어 있다.

객체를 저장한다는 것은 바로 객체의 모든 인스턴스변수의 값을 저장한다는 것과 같은 의미이다.

어떤 객체를 저장하고자 한다면 현재 객체의 모든 인스턴스변수의 값을 저장하기만 하면 된다.

그리고 저장했던 객체를 다시 생성하려면, 객체를 생성한 후에 저장했던 값을 읽어서 생성한 객체의 인스턴스변수에 저장하면 된다.

클래스에 정의된 인스턴스변수가 단순히 기본형일 때는 인스턴스변수의 값을 저장하는 일이 간단하지만, 인스턴스변수의 타입이 참조형일 때에는 간단하지 않다.

이러한 문제는 객체를 직렬화/역직렬화 할 수 있는 ObjectInputStream과 ObjectOutputStream의 사용법을 알기만 하면 된다.

여기서 두 객체가 동일한지 판단하는 기준은 두 객체의 인스턴스변수의 값들이 같고 다름이다.

ObjectInputStream, ObjectOutputStream

직렬화(스트림에 객체를 출력)에는 ObjectOutputStream을 사용하고 역직렬화(스트림으로부터 객체를 입력)에는 ObjectInputStream을 사용한다.

ObjectInputStream / ObjectOutputStream은 각각 InputStream/OutputStream을 직접 상속받지만 기반스트림을 필요로 하는 보조스트림이다.

// 즉 객체를 생성할때 입출력(직렬화/역직렬화)할 스트림을 지정해 주어야한다.
ObjectInputStream(InputStream in)
ObjectOutputStream(OutputStream in)

만일 파일에 객체를 저장(직렬화)하고 싶다면 다음과 같이 하면 된다.

FileOutputStream fos = new FileOutputStream("objectfile.ser");
ObjectOutputStream out = new ObjectOutputStream(fos);

out.writeObject(new UserInfo())

위 코드는 objectfile.ser이란 파일에 UserInfo객체를 직렬화 하여 저장한다.

출력할 스트림 생성하고 이를 기반으로 ObjectOutputStream 생성해서 write를 통해 객체 출력하면 객체가 파일에 직렬화 되어 저장된다.

아래는 역직렬화의 예시이다.

FileInputStream fis = new FileInputStream("objectfile.ser");
ObjectInputStream in = new ObjectInputStream(fis);

// 반환 타입이 Object라 형변환을 해야한다.
UserInfo info = (Userinfo)in.readObject();

ObjectInputStream과 ObjectOutputStream의 메서드

이 메소드들은 직렬화와 역직렬화를 직접 구현할 때 주로 사용되며, defaultReadObject()와 defaultWriteObject()는 자동 직렬화를 수행한다.

객체를 직렬화/역직렬화하는 작업은 객체의 모든 인스턴스변수가 참조하고 있는 모든 객체에 대한 것이기 때문에 상당히 복잡하며 시간도 오래 걸린다.

readObject()와 writeObject()를 사용한 자동 직렬화가 편리하기는 하지만 직렬화작업시간을 단축시키려면 직렬화하고자 하는 객체의 클래스에 추가적으로 다음과 같은 2개의 메소드를 직접 구현해주어야 한다.

직렬화가 가능한 클래스 만들기 - Serializable, transient

직렬화하고자 하는 클래스가 Serializable 인터페이스를 구현하도록 하면 된다.

public class UserInfo implements java.io.Serializable {
	String name;
    String password;
    int age;
}

Serializable을 구현한 클래스를 상속받는다면, Serializable을 구현하지 않아도 직렬화 가능하다.

public class SuperUserInfo implements Serializable{
	String name;
    String password;
}

public class UserInfo extends SuperUserInfo{
	int age;
}

조상 클래스가 Serializable을 구현하지 않았다면 자손 클래스 직렬화 시 조상클래스의 인스턴스 변수는 대상 제외된다.

public class SuperUserInfo{
	String name; // 직렬화 대상에서 제외
    String passwrod; // 직렬화 대상에서 제외
}

public class UserInfo extends SuperUserInfo implements Serializable{
	int age;
}

아래 코드는 Object객체는 직렬화할 수 없지만 닫른 방법으로 직렬화를 할 수 있는 예시이다.

public class UserInfo implements Serializable {
   String name;
   String password;
   int age;
   
   Object obj = new Object(); // Object 객체는 직렬화할 수 없다.
   
   Object obj = new String("abc"); // String은 직렬화 될 수 있다.
}

직렬화하고자 하는 객체의 클래스에 직렬화가 안되는 객체에 대한 참조를 포함하고 있다면, 제어자 transient를 붙여서 직렬화 대상에서 제외되도록 할 수 있다.

//obj와 password의 값은 null이 된다
public class UserInfo implements Serializable {
    String name;
    transient String password; // 직렬화 대상에서 제외된다.
    int age;
    
    transient Object obj = new Object(); // 직렬화 대상에서 제외된다.
}
profile
서버 백엔드 개발자

0개의 댓글