티스토리 뷰
728x90
반응형
Serialization을 구현할지는 신중히 결정하라
- 어떤 객체를 직렬화하기 위해서는 클래스 선언부에 implements Serializble만 덧붙이면 됩니다. 너무 쉽게 적용할 수 있지만 사실 큰 책임이 따릅니다.
💡 Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵습니다.
- 직렬화를 하게되면 직렬화된 바이트 스트림 형태도 하나의 공개 API가 됩니다. 그래서 이 직렬화된 클래스가 널리 퍼진다면 그 바이트 스트림도 영원히 지원해야 하는 것입니다.
- 커스텀 직렬화 형태를 설계하지 않고 자바의 기본 방식을 사용한다면 직렬화 형태는 기존 구현 방식에 영원히 묶여버립니다. 이렇게 된다면 기본 직렬화 형태에서는 클래스의 private, package-private 인스턴스 필드들마저 API로 공개되는 형태가 됩니다.(캡슐화 깨짐)
- 추후에 클래스 내부 구현을 수정한다면 원래의 직렬화 형태와 달라지기 때문에 문제가 발생할 수 있습니다. 이러한 문제를 해결하기 위해 원래의 직렬화 형태를 유지하면서 내부 표현을 수정할 수 있지만, 어렵기도 하고 소스코드에 지저분한 훅을 남겨놓게 됩니다. (처음부터 충분한 시간을 들여 제대로 만드는게 필요)
- 모든 직렬화된 클래스는 고유 식별 번호를 부여받습니다. serialVersionUID라는 이름의 static final long 필드로, 이 번호를 명시하지 않으면 시스템이 런타임에 암호 해시 함수를 적용해 자동으로 클래스안에 생성해 줍니다.
이 값(UID)을 생성하는데는 클래스 이름, 구현한 인터페이스들, 컴파일러가 자동으로 생성해 넣은 것들을 포함한 대부분의 클래스 멤버들이 고려됩니다. 그래서 나중에 클래스 내부에 수정을 가하게 된다면 serialVersionUID도 변경이 이루어 집니다. 다시 말해 컴파일러에 의해 생성된 serialVersionUID에 의존하게 된다면 호환성이 쉽게 깨져버려 InvalidClassException이 발생할 수 있습니다.
🧨 문제가 발생하는 예제
- 처음에 Member 클래스에서 이름과 나이의 필드를 가지고 있었지만 추후에 전화번호 필드가 추가되었다는 가정입니다.
- 이러한 문제를 해결하기 위해서는 Member 클래스 내부에 private static final long serialVersionUID 변수를 명시하면 됩니다.
@AllArgsConstructor
@ToString
public class Member implements Serializable {
private String name;
private int age;
// private String phone;
}
public class WriteObject {
public static final String FILE_PATH = "src/member.txt"; // name, age 직렬화된 바이트 형태
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_PATH))) {
Member member = new Member("홀길동", 30); // 처음에는 phone 필드가 없는 상태로 직렬화
oos.writeObject(member);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// ReadObject 실행시 Member 클래스의 phone 필드 주석 풀고 실행
// WriteObject의 member 변수 주석 후 실행
public class ReadObject {
public static final String FILE_PATH = "src/member.txt";
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(FILE_PATH))) {
Member member = (Member) ois.readObject();
System.out.println(member);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
💡 버그와 보안 구멍이 생길 위험이 높아집니다.
- 객체는 생성자를 사용해 만드는 것이 기본입니다. 즉 직렬화를 사용하게 된다면 이러한 매커니즘을 우회하는 객체 생성 기법입니다.
- 이렇게 만들어진 생성자는 전면에 드러나지 않으므로 생성자에서 구축한 불변식을 모두 보장해야하고 객체 생성 도중 공격자가 내부 객체를 들여다 볼 수 없도록 해야한다는 사실을 잊어버리게 만듭니다. 즉 기본 역직렬화를 사용하게 된다면 불변식이 깨짐과 허가되지 않은 접근에 쉽게 노출될 수 있습니다.
💡 해당 클래스의 신버전을 릴리즈시 테스트 할 것이 늘어납니다.
- 직렬화 가능 클래스가 수정된다면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 그 반대도 가능한지 검사해야 합니다. 따라서 테스트 할 양이 직렬화 가능 클래스의 수와 릴리즈 횟수에 비례해 증가하게 됩니다.
💡 상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안되며 인터페이스 또한 마찬가지 입니다.
- 이 규칙을 따르지 않으면 해당 클래스나 인터페이스를 구현한 구현체에 큰 부담을 가지게 됩니다. 정말 Serializable을 구현한 클래스만 지원하는 프레임워크를 사용하는 경우가 아니라면 이 규칙을 지키도록 해야합닌다. 하지만 그럼에도 불구하고 우리가 작성한 클래스의 인스턴스가 직렬화, 확장이 모두 가능해야 한다면 다음 사항을 지켜야 합니다.
- 1. 인스턴스 필드 중 불변식을 보장해야 하는 필드가 있다면 하위 클래스에서 finalize 메서드를 재정의 할 수 없도록 해야합니다. 상위 클래스에서 finalize 메서드를 final로 선언하면 됩니다.
- 2. 인스턴스 필드 중 기본값(0, false, null)으로 초기화되면 안되는 불변식이 있는 클래스는 readObjectNoData 메서드를 반드시 추가 해야합니다.
private void readObjectNoData() throws InvalidObjectException {
throw new InvalidObjectException("스트림 데이터가 필요합니다.");
}
💡 내부 클래스에는 직렬화를 구현하지 말아야 합니다.
- 내부 클래스에서는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가됩니다. 이 필드들이 클래스 정의에 어떻게 추가되는지 정의되지 않았기 때문에 기본 직렬화 형태가 분명하지 않아 이용 불가합니다.
하지만 정적 멤버 클래스는 예외 입니다. - static 멤버 클래스 : Serializable 구현 가능
- 익명 내부 클래스, 지역 내부 클래스 : Serializable 구현 불가능
참고자료)
https://brunch.co.kr/@oemilk/179
728x90
반응형
'스터디 > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바 - Item90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라 (0) | 2022.08.28 |
---|---|
이펙티브 자바 - Item88. readObject 메서드는 방어적으로 작성하라 (0) | 2022.08.27 |
이펙티브 자바 - Item85. 자바 직렬화의 대안을 찾으라 (1) | 2022.08.27 |
이펙티브 자바 - Item83. 지연 초기화는 신중히 사용하라 (0) | 2022.08.25 |
이펙티브 자바 - Item82. 스레드 안전성 수준을 문서화하라 (0) | 2022.08.24 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- 서비스 기반 아키텍처
- spring boot excel download paging
- java ThreadLocal
- spring boot 엑셀 다운로드
- spring boot redisson destributed lock
- spring boot poi excel download
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- pipeline architecture
- spring boot redis 대기열 구현
- transactional outbox pattern
- JDK Dynamic Proxy와 CGLIB의 차이
- spring boot redisson sorted set
- space based architecture
- spring boot excel download oom
- transactional outbox pattern spring boot
- microkernel architecture
- 레이어드 아키텍처란
- spring boot redisson 분산락 구현
- polling publisher spring boot
- java userThread와 DaemonThread
- redis 대기열 구현
- pipe and filter architecture
- redis sorted set으로 대기열 구현
- service based architecture
- 공간 기반 아키텍처
- 자바 백엔드 개발자 추천 도서
- 트랜잭셔널 아웃박스 패턴 스프링부트
- @ControllerAdvice
- redis sorted set
- 람다 표현식
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
글 보관함