본문 바로가기

WEB개발/JAVA

객체 직렬화 serialization, IOStream

1. 직렬화 serialization

직렬화 serialization

객체에 저장된 데이터를 스트림에 쓰기 위해 연속적인 데이터로 변환하는 것을 말한다. 반대로 스트림에서 데이터를 읽어 객체로 변환하는 것을 역직렬화(deserialization)라 한다.

 

역직렬화, deserialize

역직렬화는 ObjectInputStream의 readObject()를 이용한다. 다만 한 가지 주의 사항이 있는데 하나 이상의 객체를 직렬화 했을 경우 반드시 같은 순서로 역직렬화 해야 한다는 것이다.

 

package test.serialize;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.Base64;

public class TestMain {

	public static void main(String[] args) throws Exception{
		UserInfo userInfo = new UserInfo();

		userInfo.setAddr("주소주소");
		
		//OutputStream os = System.out;
		FileOutputStream fos = new FileOutputStream("test.txt");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(userInfo);
		
		FileInputStream fis = new FileInputStream("test.txt");
		ObjectInputStream ois = new ObjectInputStream(fis);
		UserInfo userInfo2 = (UserInfo) ois.readObject();
		
		System.out.println(userInfo2.getAddr());
		
	}

}

OUTPUT

주소주소

 

선언부에 transient 제어자를 사용하면 직렬화에서 제외된다. (역직렬화 시 null값)

package test.serialize;

import java.io.Serializable;

public class UserInfo implements Serializable {

	private String name;
	private int age;
	private transient String addr;
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public String getAddr() {
		return addr;
	}
	public void setAddr(String addr) {
		this.addr = addr;
	}
}

 

serialVersionUID

serialVersionUID란 역직렬화 시 클래스간 버전 비교에 사용되는 해시값이다.

 

명시하지 않을 경우 객체가 직렬화될 때 클래스에 정의된 멤버들의 정보를 이용해서 serialVersionUID에 클래스의 버전을 JVM이 자동으로 생성하여 직렬화 내용에 포함한다.

 

 


 

데이터입출력 (Stream)

: 프로그램에서는 데이터를 외부에서 읽고 다시 외부로 출력하는 작업

 

 

 

자바 입출력과 스트림(Stream)

 

 자바에서 데이터는 스트림(Stream)을 통해 입출력 됩니다. 스트림은 단일 방향으로 연속적으로 흘러가는 것을 말하는데 물이 높은 곳에서 낮은곳으로 흐르듯이 데이터는 출발지에서 나와 도착지로 흘러간다는 개념입니다.

 

Java.io 패키지

자바의 기본적인 데이터 입출력은 Java.io 패키지에서 제공합니다. java.io 패키지에서는 파일 시스템의 정보를 얻기 위한 File클래스와 데이터를 입출력하기 위한 다양한 입출력 스트림 클래스를 제공합니다.

 

 바이트 기반 스트림/보조 스트림

  • InputStream (바이트 스트림: Byte Stream): 1과 0으로 구성된 Binary 데이터의 입력처리를 위한 스트림(이미지, 사운드)

    Object
      └─ InputStream
           └─ FilterInputStream
                ├─ BufferedInputStream
                ├─ DataInputStream
                ├─ PushbackInputStream
                └─ CheckedInputStream


  • OutputStream (바이트 스트림: Byte Stream): 1과 0으로 구성된 Binary 데이터의 출력 처리를 위한 스트림 (이미지, 사운드)

    Object
      └─ OutputStream
           └─ FilterOutputStream
                ├─ BufferedOutputStream
                ├─ DataOutputStream
                ├─ PrintStream
                ├─ CheckedOutputStream
                └─ DigestOutputStream

 

 

DataStream , BufferedStream

 

DataOutputStream 클래스는 데이터를 이진 형식으로 출력하기 위한 스트림 클래스입니다. 

이 클래스는 writeByte, writeShort, writeInt, writeLong, writeFloat, writeDouble, writeBoolean, writeChar 등의 메서드를 제공합니다. 이러한 메서드를 사용하여 이진 형식으로 데이터를 출력할 수 있습니다. 이 클래스는 데이터를 직렬화하거나 네트워크를 통해 전송하기 위한 용도로 사용됩니다.

import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class DataOutputStreamExample {
    public static void main(String[] args) {
        try (DataOutputStream dataOut = new DataOutputStream(new FileOutputStream("data.txt"))) {
            // 기본 데이터 타입을 파일에 작성
            dataOut.writeInt(42);        // 정수
            dataOut.writeDouble(3.14159); // 실수
            dataOut.writeUTF("Hello, world!"); // 문자열

            System.out.println("데이터가 성공적으로 파일에 기록되었습니다.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class DataInputStreamExample {
    public static void main(String[] args) {
        try (DataInputStream dataIn = new DataInputStream(new FileInputStream("data.txt"))) {
            // 파일에서 데이터 읽기
            int intValue = dataIn.readInt();           // 정수 읽기
            double doubleValue = dataIn.readDouble();  // 실수 읽기
            String stringValue = dataIn.readUTF();     // 문자열 읽기

            System.out.println("읽어온 데이터:");
            System.out.println("정수: " + intValue);
            System.out.println("실수: " + doubleValue);
            System.out.println("문자열: " + stringValue);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}



반면에  BufferedOutputStream은 출력 스트림의 버퍼링을 제공하는 클래스입니다. 버퍼링은 출력 스트림이 바로 전송되는 것이 아니라, 일정한 크기의 버퍼에 모아두었다가 한 번에 전송함으로써 입출력 성능을 향상시키는 기능입니다. 

 

이 클래스는 생성자에서 지정된 출력 스트림에 데이터를 출력하기 전에 먼저 내부 버퍼에 데이터를 저장합니다. 이렇게 하면 매번 출력할 때마다 파일에 접근하는 비용이 줄어들어 성능이 향상됩니다. 또한 flush() 메서드를 호출하여 내부 버퍼에 있는 모든 데이터를 출력 스트림에 즉시 전송할 수 있습니다.

 

* 버퍼 없음
편지를 한 글자 쓸 때마다 우체국까지 직접 가서 보내는 것
A → 전송 B → 전송 C → 전송
👉 매번 디스크 / 네트워크 접근 → 엄청 느림

* 버퍼 있음
편지를 봉투에 다 모아두고 한 번에 우체국에 가는 것
ABC → 한 번에 전송
👉 접근 횟수 감소 → 성능 폭발적 향상

 

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedOutputStreamExample {
    public static void main(String[] args) {
        try (BufferedOutputStream bufferedOut = new BufferedOutputStream(new FileOutputStream("buffered_output.txt"))) {
            // 데이터를 파일에 기록
            String data = "Hello, BufferedOutputStream!";
            byte[] byteArray = data.getBytes();  // String을 바이트 배열로 변환

            bufferedOut.write(byteArray);  // 바이트 배열을 버퍼링된 출력 스트림을 통해 작성
            System.out.println("데이터가 성공적으로 파일에 기록되었습니다.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class BufferedInputStreamExample {
    public static void main(String[] args) {
        try (BufferedInputStream bufferedIn = new BufferedInputStream(new FileInputStream("buffered_output.txt"))) {
            int data;
            // 데이터를 한 바이트씩 읽어옴
            while ((data = bufferedIn.read()) != -1) {
                System.out.print((char) data);  // 읽어온 데이터를 문자로 출력
            }
            System.out.println("\n파일에서 데이터가 성공적으로 읽혔습니다.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


따라서 DataOutputStream 클래스는 이진 형식의 데이터를 출력하기 위한 것이며, BufferedOutputStream 클래스는 출력 성능을 향상시키기 위한 것입니다. 필요에 따라 두 클래스를 조합하여 사용할 수도 있습니다. 예를 들어, DataOutputStream으로 이진 형식의 데이터를 출력하면서, BufferedOutputStream으로 출력 성능을 향상시킬 수 있습니다.

 

 

버퍼가 불필요한 경우

  1. 입출력 대상의 크기가 작은 경우

  2. 입출력 대상이 빈번하게 발생하는 경우
    => 버퍼가 가득 차기 전에 매번 출력을 수행해야 하므로, 버퍼링이 더 이상 성능을 향상시키지 못할 수 있습니다. 

  3. 입출력 속도가 매우 빠른 경우
    => 입출력 속도가 매우 빠른 경우에는, 버퍼링이 오히려 성능을 저하시킬 수 있습니다. 

  4. 버퍼 크기가 너무 큰 경우
    => 메모리를 과도하게 사용하게 되어 성능 저하를 초래할 수 있습니다.



PushbackInputStream

은 한 번 읽은 바이트를 다시 스트림에 “밀어 넣을 수(push back)” 있는 InputStream입니다
즉, 미리 읽어서 판단만 하고, 실제 처리는 다시 처음부터 하고 싶을 때 쓰는 스트림

 

일반 InputStream은 한 번 읽으면 되돌릴 수 없기 때문에 이럴 때 PushbackInputStream을 사용한다.

 

package org.example;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;

public class TestMain {
    public static void main(String[] args) throws InterruptedException, IOException {
        InputStream in = new ByteArrayInputStream("ABC".getBytes());
        PushbackInputStream pb = new PushbackInputStream(in);

        int ch1 = pb.read();
        System.out.println((char) ch1);

        pb.unread(ch1);

        int ch2 = pb.read();
        System.out.println((char) ch2);
    }
}

 

 

CheckedInputStream

데이터를 읽는 동시에 체크섬(무결성 검증값)을 자동으로 계산해주는 InputStream입니다.주로 전송 중 데이터 손상 여부 확인이나 파일 무결성 검사에 사용됩니다.

package org.example;

import java.io.*;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;

public class TestMain {
    public static void main(String[] args) throws InterruptedException, IOException {
        InputStream in = new FileInputStream("test.txt");
        CheckedInputStream cis = new CheckedInputStream(in, new CRC32());

        while((cis.read()) != -1) {}

        long checkSum = cis.getChecksum().getValue();
        System.out.println("checkSum : "+ checkSum);
    }
}

 


 

문자 스트림/보조 스트림

  • Reader (문자 스트림: Character Stream): 문자, 텍스트 형태의 데이터 입력 처리를 위한 스트림(텍스트, 웹페이지, 키보드 등)
  • Writer (문자 스트림: Character Stream): 문자, 텍스트 형태의 데이터 출력 처리를 위한 스트림(텍스트, 웹페이지, 키보드)

 

 

InputStreamReader, OutputStreamWriter

 

InputStream을 통해 InputStreamReader 객체를 생성, BufferReader을 통해 읽을 수 있음

  • 바이트 기반 스트림을 문자 기반 스트림처럼 쓸 수 있게 해줌
  • 인코딩(encoding)을 변환하여 입출력 할 수 있게 해줌
  • 인코딩 변환하기
InputStream fis = new FileInputStream("C:\\javaStudy\\file\\MS949.txt");
// 맥 사용자들은 /로 경로만 설정해주면 됨
InputStreamReader isr = new InputStreamReader(fis, "MS949");

 

바이트 단위 입출력 스트림 : 그림, 멀티미디어, 문자등 모든 종류의 데이터들을 주고받을 수 있습니다.

문자 단위 입출력 스트림 : 오로지 문자만 주고받을 수 있게 특화되어 있습니다.

 

 

InputStream

 

InputStream은 바이트 기반 입력 스트림의 최상위 클래스로 추상 클래스입니다. 모든 바이트 기반 입력 스트림은 이 클래스를 상속받아서 만들어 집니다. InputStream 클래스에는 바이트 기반 입력 스트림이 기본적으로 가져야 할 메소드들이 정의 되어 있습니다.

 

 메서드 설명 
 read() 입력 스트림으로부터 1바이트를 읽고 읽은 바이트를 리턴합니다. 
 read(byte[ ] b)  입력 스트림으로부터 읽은 바이트들을 매개값으로 주어진 바이트 배열b에 저장하고 실제로 읽은 바이트 수를 리턴합니다.
 read(byte[] b, int off, int len) 입력 스트림으로부터 len개의 바이트만큼 읽고 매개값으로 주어진 바이트 배열 b[off]부터 len개까지 저장합니다. 그리고 실제로 읽은 바이트 수인 len개를 리턴합니다. 만약 len개를 모두 읽지 못하면 실제로 읽은 바이트 수를 리턴합니다. 
 close() 사용한 시스템 자원을 반납하고 입력스트림을 닫습니다. 

 

 

OutputStream

 

OutputStream은 바이트 기반 출력 스트림의 최상위 클래스로 추상클래스입니다. 모든 바이트 기반 출력 스트림 클래스는 이 클래스를 상속받아서 만들어집니다. OutputStream 클래스에는 모든 바이트 기반 출력 스트림이 기본적으로 가져야 할 메소드가 정의되어 있습니다.

 

 메서드 설명 
 write(int b)  출력 스트림으로부터 1바이트를 보냅니다.(b의 끝 1바이트) 
 write(byte[ ] b)  출력 스트림으로부터 주어진 바이트 배열 b의 모든 바이트를 보냅니다.
 write(byte[ ] b, int off, int len)  출력 스트림으로 주어진 바이트 배열 b[off]부터 len개까지의 바이트를 보냅니다. 
 flush()   버퍼에 잔류하는 모든 바이트를 출력합니다. 
 close()  사용한 시스템 자원을 반납하고 출력 스트림을 닫습니다. 

 

1. 1Byte 스트림 입·출력 구조 정리

더보기

1) 콘솔 출력

FileOutputStream fos = new FileOutputStream(FileDescriptor.out);
BufferedOutputStream bos = new BufferedOutputStream(fos);
DataOutputStream dos = new DataOutputStream(bos);

dos.writeByte(65); // 'A'
dos.flush();


2) 파일 출력

FileOutputStream fos = new FileOutputStream("test.bin");
BufferedOutputStream bos = new BufferedOutputStream(fos);
DataOutputStream dos = new DataOutputStream(bos);

dos.writeInt(100);
dos.writeUTF("hello");
dos.close();



3) 네트워크 출력

Socket soc = new Socket("localhost", 9000);
DataOutputStream dos =
    new DataOutputStream(
        new BufferedOutputStream(soc.getOutputStream())
    );

dos.writeUTF("PING");
dos.flush();

 

1) 콘솔 입력

DataInputStream dis =
    new DataInputStream(
        new BufferedInputStream(
            new FileInputStream(FileDescriptor.in)
        )
    );

byte b = dis.readByte();
System.out.println(b);


2) 파일 입력 (바이트 기반)

DataInputStream dis =
    new DataInputStream(
        new BufferedInputStream(
            new FileInputStream("test.bin")
        )
    );

int n = dis.readInt();
String msg = dis.readUTF();


3) 네트워크 입력

Socket soc = new Socket("localhost", 9000);
DataInputStream dis =
    new DataInputStream(
        new BufferedInputStream(soc.getInputStream())
    );

String msg = dis.readUTF();

 

 

2. 텍스트 입.출력

 텍스트의 입력과 출력은 앞서 본 스트림을 변형시켜 주는 클래스를 사용한다는 것 외에는 다른 점이 없다. 텍스트 스트림은 2byte의 문자 체계를 가진( 한글 ) 형태의 입력가 출력에 사용된다. 기본클래스는 Writer와 Reader가 있다. ( 자세한 건 API 참고 )

더보기

1) 콘솔 출력

PrintWriter pw =
    new PrintWriter(
        new BufferedWriter(
            new OutputStreamWriter(System.out)
        )
    );

pw.println("Hello");
pw.flush();



2) 파일 출력

PrintWriter pw =
    new PrintWriter(
        new BufferedWriter(
            new FileWriter("test.txt")
        )
    );

pw.println("파일 출력");
pw.close();


3) 네트워크 출력

Socket soc = new Socket("localhost", 9000);

PrintWriter pw =
    new PrintWriter(
        new BufferedWriter(
            new OutputStreamWriter(soc.getOutputStream())
        )
    );

pw.println("PING");
pw.flush();


1) 콘솔(키보드) 입력

BufferedReader br =
    new BufferedReader(
        new InputStreamReader(System.in)
    );

String line = br.readLine();
System.out.println(line);



2) 파일 입력

BufferedReader br =
    new BufferedReader(
        new FileReader("test.txt")
    );

String line;
while ((line = br.readLine()) != null) {
    System.out.println(line);
}



3) 네트워크 입력

Socket soc = new Socket("localhost", 9000);

BufferedReader br =
    new BufferedReader(
        new InputStreamReader(soc.getInputStream())
    );

String msg = br.readLine();

 

3. 객체의 입.출력
  객체 단위의 입력과 출력도 마찬가지로 스트림을 기반으로 한다. 그러나 이 클래스의 상속관계는 1byte 계열에서 이미 나타났었다. 그래서 여기에서 사용 형식을 바로 살펴보기로 했다. 또한 객체의 입력이나 출력은 콘솔과 키보드를 통해서는 이루어지지 않기 때문에 파일과 네트워크에 대해서만 설명하겠다.

더보기

1. 객체 출력 (ObjectOutputStream)
1) 파일 출력

ObjectOutputStream oos =
    new ObjectOutputStream(
        new BufferedOutputStream(
            new FileOutputStream("obj.dat")
        )
    );

oos.writeObject(new Person("park", 20));
oos.close();


2) 네트워크 출력

Socket soc = new Socket("localhost", 9000);

ObjectOutputStream oos =
    new ObjectOutputStream(
        new BufferedOutputStream(
            soc.getOutputStream()
        )
    );

oos.writeObject(new Person("hello"));
oos.flush();


2. 객체 입력 (ObjectInputStream)
1) 파일 입력

ObjectInputStream ois =
    new ObjectInputStream(
        new BufferedInputStream(
            new FileInputStream("obj.dat")
        )
    );

try {
    Object obj = ois.readObject();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}


2) 네트워크 입력

Socket soc = new Socket("localhost", 9000);

ObjectInputStream ois =
    new ObjectInputStream(
        new BufferedInputStream(
            soc.getInputStream()
        )
    );

try {
    Object obj = ois.readObject();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}


  한가지 주의 해야할 점은 객체의 입,출력에서는 직렬화라는 개념이 반드시 필요하다. 직렬화라는 것은 클래스의 구성을 파일이나 네트워크로 정상적으로 전달하기 위해 형태를 재구성하는 것이라고 생각하면 쉽다. 다시 말해 일렬로 줄을 지어서 전송하여야 한다는 것이다. 이것을 프로그래머가 매번 구현하려고 한다면 이것 또한 너무 힘든 일이 될 것이다. 그랫 자바 측에서는 이러한 번거로움을 해결하기 위하여 java.io.Serializabl이라는 인터페이스를 제공해 주었다.

 


 

FileInputStream vs FileReader

구분 FileInputStream FileReader
읽는 단위 바이트(byte) 문자(char)
대상 모든 파일 텍스트 전용
인코딩 모름 (그냥 바이트) 문자셋 해석함
깨짐 가능 있음 없음(인코딩 맞으면)

 

FileReader의 Charset은 고정이며

1️⃣ JVM 옵션 ( -Dfile.encoding=... )→ 2️⃣ OS 로케일 → 3️⃣ JVM 내부 기본값

으로 우선순위를 가집니다

스프링부트에서 Charset과는 연관이 없습니다.

server:
  servlet:
    encoding:
      charset: UTF-8
      force: true

 

  • Request body
  • Response body
  • JSON
    에만 적용됨.

 


https://noritersand.github.io/java/java-%EA%B0%9D%EC%B2%B4-%EC%A7%81%EB%A0%AC%ED%99%94-serialization/
 
https://coding-factory.tistory.com/281

https://devlog2829.tistory.com/94

https://m.blog.naver.com/slrkanjsepdi/90140711064

 

 

 

 

 

'WEB개발 > JAVA' 카테고리의 다른 글

[JAVA - NIO] Netty  (0) 2024.03.28
[JAVA] 메모리 스택, 힙  (0) 2022.10.31
[JAVA] Thread-Safe, Concurrent Collection class, Double Checked Locking  (0) 2021.09.23
JMX(MBean), JOLOKIA  (0) 2021.09.01
[JAVA] AtomicInteger (thread-safe)  (0) 2021.08.13