위 내용은 스프링 개발자로서의 관점에서 작성하였습니다.
서로 다른 비즈니스 서비스 간의 통신은 인터페이스를 통해 구현되어야 합니다. 두 서비스 간에 데이터 객체를 공유하려면 객체를 이진 스트림으로 변환한 후 네트워크를 통해 다른 서비스로 전송하고, 다시 객체로 변환하여 서비스 메서드에서 사용할 수 있어야 합니다. 이 인코딩 및 디코딩 과정을 직렬화(Serialization) 및 역직렬화(Deserialization).
➡ 객체 데이터를 저장하거나 전송할 때 필요
➡ 저장된 객체 데이터를 다시 사용하거나, 전송받은 데이터를 객체로 변환할 때 필요
사용 사례 | 직렬화 | 역직렬화 |
---|---|---|
캐싱 | 객체 데이터를 Redis에 저장 | Redis에서 불러와 객체로 변환 |
네트워크 통신 | 객체 → JSON 변환 (API 응답) | JSON → 객체 변환 (API 요청 처리) |
파일 저장 | 객체 데이터를 파일에 저장 | 파일에서 객체를 불러와 복구 |
Serializable
을 구현하도록 클래스에 선언하면 직렬화 가능한 클래스가 된다. 데이터 호환성 문제 때문에 SerialVersionUID 명시하는게 좋다.구현 핵심 원칙
1. transient 필드 선언 : 직렬화 대상에서 제외
public class SecureData implements Serializable {
private transient String secretKey; // 직렬화 제외
private String publicData;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
oos.writeObject(encrypt(publicData)); // 데이터 암호화
}
private void readObject(ObjectInputStream ois)
throws ClassNotFoundException, IOException {
ois.defaultReadObject();
publicData = decrypt((String) ois.readObject());
}
private static final long serialVersionUID = 20230704L; // 명시적 버전 지정
장점
단점
장점
단점
=> transient 키워드로 민감한 정보나 참조를 직렬화에서 제외 가능
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
private Object readResolve() {
return INSTANCE; // 싱글턴 보장
}
}
public enum Elvis {
INSTANCE;
public void sing() {
System.out.println("🎤 Hound Dog");
}
}
추천 사례
장점
단점
import java.io.*;
(원래 직렬화할 객체)
class Person implements Serializable {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
📌 **이게 핵심! 직렬화를 직접 하지 않고, 대신 이 "프록시" 클래스를 이용함.**
🔹 `Person` 객체를 **그대로 직렬화하면 보안 취약점**이 생길 수도 있음.
🔹 대신 **"프록시"**를 사용하면 안전하게 직렬화 가능!
🔹 프록시는 `name`, `age` 필드만 가지고 있음. (`Person` 객체와 동일한 데이터)
// 직렬화 프록시 클래스
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 1L;
private final String name;
private final int age;
SerializationProxy(Person p) {
this.name = p.name;
this.age = p.age;
}
// 역직렬화 시 원래 객체로 변환
private Object readResolve() {
return new Person(name, age);
}
}
// 직렬화할 때 호출되는데, 프록시를 반환하게 하고 있다.
// 이로 인해 바깥 클래스의 직렬화된 인스턴스를 생성할 수 없다.
private Object writeReplace() {
return new SerializationProxy(this);
}
// 직접 역직렬화 방지
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("프록시를 사용해야 합니다.");
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class Main {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("Alice", 30);
// 직렬화
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(person);
out.close();
// 역직렬화
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream in = new ObjectInputStream(bis);
Person deserializedPerson = (Person) in.readObject();
in.close();
System.out.println(deserializedPerson);
}
}
단계 | 실행되는 메서드 | 설명 |
---|---|---|
1. 직렬화 시작 | writeObject(person) | Person 객체를 직렬화하려고 함 |
2. 직렬화 프록시 반환 | writeReplace() | Person 대신 SerializationProxy 를 직렬화함 |
3. 파일 저장 | - | SerializationProxy 데이터가 파일(person.ser )에 저장됨 |
4. 역직렬화 시작 | readObject() | SerializationProxy 객체가 파일에서 읽힘 |
5. 원래 객체로 변환 | readResolve() | SerializationProxy → Person 객체로 변환 |
6. 복원된 객체 반환 | - | 최종적으로 Person 객체가 반환됨 |
오늘날 대부분의 백엔드 서비스는 마이크로서비스 아키텍처를 기반으로 구현된다. 서비스는 비즈니스 기능에 따라 분리되어 디커플링을 실현하지만, 이로 인해 새로운 과제가 생긴다
동시 요청이 많은 상황에서는 직렬화가 느리면 요청 응답 시간이 길어질 수 있으며, 직렬화된 데이터 크기가 크면 네트워크 처리량이 감소할 수 있다. 따라서 뛰어난 직렬화 프레임워크는 시스템의 전체 성능을 향상할 수 있다.
serialVersionUID
는 클래스 버전 확인용 출입증public class Person implements Serializable {
private final String name; // 불변 필드
public Person(String name) {
this.name = name;
// 생성자에서 초기화 및 유효성 검사
if (name == null) {
throw new NullPointerException("Name cannot be null");
}
}
// 역직렬화 시 생성자가 호출되지 않으므로, name이 null일 수 있음
}
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject(); // 기본 직렬화 수행
// ✅ 방어적 복사 (기존 객체를 그대로 사용하지 않음)
start = new Date(start.getTime());
end = new Date(end.getTime());
// ✅ 유효성 검사 (이상한 값이 들어오면 예외 발생)
if (start.compareTo(end) > 0) {
throw new InvalidObjectException("시작 날짜가 종료 날짜보다 늦음!");
}
}
라이브러리 | 주요 특징 및 장점 | 주요 단점/제약 사항 |
---|---|---|
Protobuf | - 다중 언어 지원 - 고성능/소형 데이터 - 스키마 기반 구조 - gRPC 표준 | - 스키마 사전 정의 필수 - 복잡한 구조 변환 시 번거로움 |
JSON (Jackson) | - 인간 친화적 포맷 - 디버깅 용이 - 웹 API 표준 - 언어 독립성 | - 바이너리 대비 속도 느림 - 데이터 크기 큼 - 타입 안전성 낮음 |
Kryo | - JVM 내 최고 성능 - 최소 크기 데이터 - 커스텀 직렬화 지원 | - Java 전용 - 스키마/버전 관리 미흡 - 비JVM 환경 비호환 |
Avro | - 빅데이터 시스템 표준 - 스키마 진화 지원 - 분산 시스템 최적화 - 다중 언어 | - 복잡한 스키마 관리 필요 - 러닝 커브 존재 - 실시간 처리 비효율적 |
다중 언어 환경 → Protobuf/Avro
JVM 내 고성능 → Kryo
웹 API/간편성 → JSON
스키마 버전 관리 → Protobuf/Avro
대용량 분산 처리 → Avro
https://devloo.tistory.com/entry/%EC%9E%90%EB%B0%94-%EC%A7%81%EB%A0%AC%ED%99%94-%EC%82%AC%EC%9A%A9%EC%9D%84-%ED%94%BC%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0
Effective Java 도서 12장
https://youtu.be/qrQZOPZmt0w?si=fu3KnpCbA0lfnklQ
https://youtu.be/Qi2LJ_NfzC0?si=3FR5CY1Rgqv9PsDo
https://youtu.be/M0TtOja18M8?si=iLAOR722UXDoYnSa
https://youtu.be/o5rspNdJ-fE?si=jrW9btKjFkxJvdZH
https://youtu.be/0qYA_vTb7Lw?si=kT4wc1yE0bX59z_z