[JAVA] 객체 직렬화를 통한 네트워크 전송
들어가기 앞서
객체 직렬화와 네트워크 전송에 대해 이해하려면 소켓 통신에 대한 이해가 필요하다.
만약 소켓 통신에 대해 모른다면 아래 글을 참고하면 된다.
https://lold2424.tistory.com/245
[JAVA] 소켓 통신의 기본 개념과 구조
Java에서는 네트워크 통신이 가능하도록 java.net 패키지를 통해 TCP/IP 기반 소켓 통신 기능을 제공한다.이번 글에서 TCP 기반의 소켓과 서버소켓을 사용한 1:1 구조를 알아보도록 하겠다.소켓 통신이
lold2424.tistory.com
직렬화
객체(Object)의 상태를 바이트 형태로 변환하여 전송하거나 저장할 수 있게 하는 기술을 직렬화라 한다.
이 반대의 과정을 역직렬화라 하며 바이트 데이터를 다시 객체로 복원한다.
직렬화된 바이트 데이터는 파일 저장, DB 저장, 메모리 저장 등 다양한 방식에 사용할 수 있다.
왜 필요할까?
일반적으로 프로그램 간 데이터를 주고받을 경우 문자열, 숫자와 같은 기본 데이터 타입 또는 JSON 문자열을 사용한다.
네트워크는 바이트 단위로 데이터를 전송한다.
때문에, 기본 데이터 타입이나 JSON 문자열의 경우에는 별 문제가 없으나 개발자가 직접 만든 사용자 정의 객체(클래스)를 통째로 주고받을 수 없기 때문에 직렬화가 이때 사용된다.
직렬화의 종류
1. Serializable 인터페이스
public class User implements Serializable {
private String name;
private int age;
}
Serializable
은 마커 인터페이스(Marker Interface)다.
마커 인터페이스는 메서드가 없고, "이 객체는 직렬화 가능하다"는 의사 표시 역할을 한다고 생각하면 된다.
JVM은 이 인터페이스를 통해 직렬화 가능 여부를 판단한다.
구현하지 않으면 NotSerializableException
예외가 발생하기 때문에 반드시 구현해야 한다.
2. ObjectOutputStream / ObjectInputStream
직렬화와 역직렬화를 위한 입출력 클래스다.
직렬화 (쓰기)
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.writeObject(user); // 객체 → 바이트
역직렬화 (읽기)
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
User user = (User) in.readObject(); // 바이트 → 객체
내부적으로 클래스 이름, 필드 값, 버전 정보 등을 포함한 바이트로 변환된다.
역직렬화 시 해당 클래스가 JVM에 존재하고 구조가 같아야 정상적으로 복원된다.
3. serialVersionUID 필드
private static final long serialVersionUID = 1L;
클래스 구조가 변경되었는지 판별하는 버전 식별자다.
클래스 구조(필드, 메서드 등)를 바꾸면 JVM이 자동으로 새 UID를 생성하지만, 자동 생성된 값이 다르면 역직렬화가 되지 않는다.
따라서, 직접 명시하는 것이 좋다.
버전 호환을 유지해주기 때문에 서버-클라이언트 간의 클래스가 일치하는것을 보장해준다.
4. transient 키워드
private transient String password;
transient
는 직렬화 대상에서 제외하겠다는 의미를 가진다.
보안 데이터, 임시 캐시 등 민감하거나 재생성 가능한 정보에 사용한다.
실제 네트워크 전송 흐름
직렬화 과정 (클라이언트)
Message msg = new Message("Alice", "Hello Server!");
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.writeObject(msg);
객체는 JVM의 ObjectOutputStream
에 의해 바이트로 변환된다.
내부적으로 클래스 이름, 필드 타입, 값 등의 메타 정보 포함된다.
역직렬화 과정 (서버)
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Message received = (Message) in.readObject();
전송된 바이트를 JVM이 해석하여 객체로 재구성한다.
동일한 클래스가 메모리에 로딩되어 있어야 한다.
serialVersionUID
가 일치해야 한다.
JVM 내부 직렬화 흐름
직렬화 시 JVM이 아래 순서대로 동작한다.
- 객체가
Serializable
을 구현했는지 확인 - 내부 클래스 정보(
ObjectStreamClass
) 생성 - 직렬화 가능한 필드를 수집 (
transient
제외) ObjectOutputStream
에 필드 값을 순차적으로 기록- 바이트 스트림 생성 후 전송 또는 저장
코드 예시
사용자 정의 객체
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 100L;
private String name;
private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
public String toString() { return name + " (" + age + ")"; }
}
서버 코드
import java.io.*;
import java.net.*;
public class ObjectServer {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ServerSocket server = new ServerSocket(8888);
System.out.println("[서버] 연결 대기 중...");
Socket socket = server.accept();
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
Person p = (Person) in.readObject();
System.out.println("[서버] 수신 객체: " + p);
socket.close();
server.close();
}
}
클라이언트 코드
import java.io.*;
import java.net.*;
public class ObjectClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 8888);
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
Person p = new Person("홍길동", 30);
out.writeObject(p);
socket.close();
}
}
직렬화의 문제점과 단점
직렬화는 간단하고 빠르면서 객체 전송에 유용하지만 여러 한계와 위험성을 갖고 있기에 반드시 인지하고 있어야 한다.
1. 보안 취약점
- 자바 직렬화는 클래스 이름, 구조, 필드 정보까지 포함한 바이너리 형식이다.
- 역직렬화 시 의도치 않은 클래스가 실행될 수 있어 보안 위협이 발생할 수 있다.
- 예) CVE-2015-4852
2. 버전 호환성 문제
- 클래스 구조가 조금만 바뀌어도 이전 버전과 호환되지 않을 수 있다.
- 특히
serialVersionUID
를 잘못 관리하면 예외가 발생하게 된다.
3. 자바 환경에 종속적
- 직렬화 자체가 자바를 위해 존재하다 보니 JVM에서만 사용이 가능하다.
- 다른 언어나 플랫폼에서는 사용이 불가능한 자바 전용이다.
4. 데이터 포맷 비표준
- 직렬화 데이터는 바이너리로 저장되어 사람이 읽을 수 없다.
- JSON은 텍스트 형식으로 저장되어 사람이 읽을 수 있다.
- 분석, 디버깅, API 연동 등에서 불리하다
5. 불필요한 정보까지 포함
- 클래스 이름, 패키지, 메타데이터까지 저장된다.
- 네트워크나 저장소의 자원을 그만큼 잡아먹기 때문에 부하를 줄 수 있다.