조슈아 블로크 님의 "Effective Java" 책을 정리한 포스팅 입니다.
1. 자바 직렬화의 대안을 찾으라
자바 직렬화
- 객체의 상태를 바이트 스트림으로 변환하여 저장하거나 전송할 수 있게 하는 기술
- 역직렬화를 통해 객체로 복원 가능
위험성
- 보안
- 원격코드 실행
- 신뢰하지 못하는 스트림을 역직렬화 하면 공격으로 이어짐
- 성능
- DDoS: 큰 용량의 데이터를 역직렬화 하는데 많은 서버 리소스가 소모됨
직렬화 대안
JSON
public class JsonExample {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
// 객체를 JSON으로 직렬화
Person person = new Person("Noah", 30);
String jsonString = mapper.writeValueAsString(person);
System.out.println(jsonString); // {"name":"Noah","age":30}
// JSON을 객체로 역직렬화
Person deserializedPerson = mapper.readValue(jsonString, Person.class);
System.out.println(deserializedPerson);
}
}
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
- 브라우저-서버 통신용
- 텍스트 기반 (사람이 읽을 수 있음)
Protocol Buffer
Person person = Person.newBuilder().setName("Noah").setAge(30).build();
// 객체를 바이트 배열로 직렬화
byte[] serializedData = person.toByteArray();
// 바이트 배열을 객체로 역직렬화
Person deserializedPerson = Person.parseFrom(serializedData);
System.out.println(deserializedPerson.getName()); // Noah
- 서버 간 통신용
- 이진 표현 (성능 좋음)
- 스키마용 사용 가능
역직렬화 필터링
화이트리스트 사용
SWaT
- 직렬화 위험 요소를 분석하고 문제를 탐지 및 차단하는 툴
2. Serializable을 구현할지는 신중히 결정하라
Serializable
- 클래스 직렬화 시 사용하는 마커 인터페이스
- 직렬화 지원을 하는데 간단해 보이지만, 매우 값비싼 일임
고려사항
릴리즈 한 뒤 수정 어려움
- 직렬화된 데이터는 클래스의 구조에 강하게 의존함
- 클래스 구조를 변경하면 호환성 문제 발생
버그와 보안 구멍이 생길 위험이 높음
- 신뢰하지 못하는 스트림을 역직렬화 시 보안 취약점이 초래됨
신버전을 릴리즈할 때 테스트할 것이 늘어남
- 모든 변경사항에 대해 직렬화 호환성 테스트가 필요
주의사항
상속용으로 설계된 클래스는 구현하면 안됨
- 상위 클래스 직렬화도 하위 클래스에서 수행해야 하므로 직렬화 부담이 늘어남
- 하위 클래스는 직렬화 호환성을 유지해야 하므로 설계가 제한됨
내부 클래스는 직렬화 구현하면 안됨
- 내부 클래스는 외부 클래스의 참조를 포함하고 있어 직렬화하기 어렵고 비효율적
3. 커스텀 직렬화 형태를 고려해보라
기본 직렬화 형태의 문제점
공개 API가 현재의 내부 표현 방식에 영구히 묶임
- 기본 직렬화는 객체의 모든 필드를 직렬화 함
- 클래스 설계를 변경하면 지곤의 직렬화된 데이터와의 호환성 문제 발생
너무 많은 공간을 차지할 수 있음
- 직렬화에 불필요한 데이터도 포함될 수 있음
시간이 많이 걸림
- 모든 필드를 직렬화하므로 객체 그래프를 탐색해야 함
스택 오버플로우를 일으킬 수 있음
- 객체 그래프가 순환 참조를 포함하면 무한루프에 빠짐
고려 사항
writeObject()
public class CustomExample implements Serializable {
private transient String tempData; // 직렬화 제외
private String permanentData;
private void writeObject(ObjectOutputStream out) throws Exception {
out.defaultWriteObject(); // 기본 직렬화
out.writeObject(tempData != null ? tempData.toUpperCase() : null); // 사용자 정의 직렬화
}
}
- 직렬화 데이터를 사용자 정의하여 효율적으로 저장
readObject()
public class SafeExample implements Serializable {
private String data;
private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject(); // 기본 역직렬화
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("Invalid data"); // 불변식 보장
}
}
}
- 불변식 보장과 보안을 위해 사용
transient
- 직렬화에서 제외할 필드를 표시하는 데 사용되는 키워드
- 객체의 논리적 상태와 무관한 필드라고 확신할 때만 사용
synchronized
- 객체 전체 상태를 읽는 메서드에 동기화 적용하기
- 직렬화 시 일관성이 깨지지 않도록 하기 위함
serialVersionUID
- 직렬화된 클래스의 버전을 관리하기 위한 고유 식별자
- 호환성
- serialVersionUID으로 버전을 관리
- serialVersionUID이 일치하면 구조 변경이 있어도 역직렬화 가능
- 효율적
- 명시적으로 설정하면, 자동 생성 시 연산 비용을 아낄 수 있음
'Java' 카테고리의 다른 글
[Effective Java] 12-2. 직렬화 (2) | 2025.01.03 |
---|---|
[Effective Java] 11-2. 동기화 (2) | 2025.01.03 |
[Effective Java] 11-1. 동기화 (0) | 2025.01.01 |
[Effective Java] 10-2. 예외 (0) | 2025.01.01 |
[Effective Java] 10-1. 예외 (0) | 2025.01.01 |