java.io.Serializable 인터페이스는 "이 클래스는 직렬화해도 되는 클래스입니다"라고 자바 가상 머신(JVM)에게 알려주는 '마커 인터페이스(Marker Interface)'.
import java.io.Serializable;
public class User implements Serializable {
// 1. serialVersionUID를 명시적으로 선언 (매우 중요!)
private static final long serialVersionUID = 1L;
private String name;
private int age;
// 2. transient 키워드 사용
// 직렬화 대상에서 제외하고 싶은 민감한 정보에는 transient를 붙입니다.
private transient String password;
public User(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
}
- serialVersionUID (버전 관리):
- 문제점: 이를 명시하지 않으면 자바가 클래스의 구조를 보고 자동으로 해시값을 생성합니다. 만약 나중에 User 클래스에 필드를 하나 추가하면 이 값이 바뀌게 됩니다.
- 결과: 이전에 저장해둔(직렬화된) 데이터를 불러오려(역직렬화) 할 때, 버전이 안 맞다며 InvalidClassException이라는 끔찍한 에러가 발생합니다.
- 해결: 직접 1L 등으로 고정해두면, 클래스 구조가 조금 바뀌어도 호환성을 유지할 수 있습니다.
- transient (제외하기):
- 비밀번호나 주민등록번호 같은 민감한 데이터, 혹은 굳이 저장할 필요 없는 데이터는 transient 키워드를 붙여서 직렬화 과정에서 제외해야 합니다. (저장 시 null로 처리됨)
요즘은 Serializable 대신 JSON 을 사용한다.
1."다른 언어랑 대화가 안 돼요" (상호운용성)
- Java 직렬화: 자바 시스템끼리만 알아듣습니다. 만약 나중에 Node.js나 Python으로 만든 마이크로서비스(MSA)가 Redis에 접근해서 데이터를 읽으려 하면? "이게 무슨 외계어(바이트 코드)야?" 하고 에러를 뱉습니다.
- JSON: 자바, 파이썬, 자바스크립트 등 지구상의 거의 모든 언어가 이해할 수 있는 공용어입니다.
2. "눈에 보이지 않아요" (디버깅 난이도)
- Java 직렬화: Redis에 저장된 데이터를 조회해보면 \xac\xed\x00\x05... 같은 깨진 문자로 보입니다. 개발자가 눈으로 데이터가 잘 들어갔는지 확인할 수 없습니다.
- JSON: {"name": "홍길동", "age": 25} 처럼 사람이 읽을 수 있어 디버깅이 매우 쉽습니다.
3. "클래스 조금만 고쳐도 에러가 나요" (유연성)
- 아까 배운 serialVersionUID 문제입니다. 자바 직렬화된 데이터가 Redis에 남아있는데, 배포하면서 클래스에 필드를 하나 추가했다면? 기존 캐시 데이터를 불러올 때 에러가 터집니다. JSON은 필드가 추가되거나 빠져도 훨씬 유연하게 대처할 수 있습니다.
3번의 문제를 해결하기 위한 방법
해결책 1. "새 필드 추가 & 덮어쓰기" 전략 (가장 안전하고 추천하는 방법)
기존의 age 필드를 건드리면 사고가 납니다. **"기존 필드는 그대로 두고(Deprecated), 새 필드를 추가"**하여 논리적으로 해결해야 합니다.
핵심 아이디어
- 기존 int age는 남겨둡니다. (과거 데이터 읽기용)
- 새로운 String ageGroup (또는 ageStr) 필드를 만듭니다.
- 데이터를 읽어올 때(Getter), 옛날 필드 값이 있다면 새 필드로 변환해줍니다.
개선된 코드 예시
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 기존 버전 유지
// 1. 기존 필드: 절대 지우거나 타입을 바꾸면 안 됩니다!
@Deprecated // 개발자들에게 "이거 이제 안 씀" 표시
private int age;
// 2. 신규 필드: 타입을 String으로 새로 만듭니다.
private String ageString;
// 생성자에서는 신규 필드에 값을 채웁니다.
public User(String ageString) {
this.ageString = ageString;
// 필요하다면 호환성을 위해 age에도 임시로 값을 넣어줄 수 있음 (선택)
}
// 3. Getter에서 마법을 부립니다 (중요!)
public String getAge() {
// 신규 데이터가 있으면 그걸 반환
if (this.ageString != null) {
return this.ageString;
}
// 신규 데이터가 없고(null), 구 데이터(int)만 있다면? -> 문자열로 변환해서 반환!
return String.valueOf(this.age);
}
}
작동 원리:
- 옛날 데이터(Redis) 로딩: age=25, ageString=null 상태로 역직렬화됩니다. → getAge() 호출 시 String.valueOf(25)가 반환되어 정상 작동!
- 새 데이터 저장: ageString="20대" 상태로 저장됩니다
해결책 2. "캐시 갱신(Cache Eviction)" 전략 (운영 관점의 해결책)
만약 Redis가 DB가 아니라 단순 캐시 용도라면, 굳이 옛날 데이터를 살리려 애쓸 필요가 없습니다. 과감하게 **"구버전 데이터는 버린다"**는 전략입니다.
핵심 아이디어
- User 클래스의 serialVersionUID를 2L로 변경합니다.
- 백엔드 코드에서 Redis 데이터를 불러올 때 try-catch로 감쌉니다.
- 직렬화 에러가 나면 "아, 이건 옛날 버전 데이터구나" 하고 무시한 뒤, DB에서 새로 조회해서 다시 저장(캐싱)합니다.
코드 예시
User.java
public class User implements Serializable {
// 버전을 변경하여 "나 바뀌었어!"라고 선언
private static final long serialVersionUID = 2L;
private String age; // 마음 편하게 String으로 변경
}
UserService.java (서비스 로직)
public User getUser(String userId) {
try {
// 1. Redis에서 조회 시도
User user = redisService.get(userId);
if (user != null) return user;
} catch (SerializationException e) {
// 2. 버전 불일치 에러 발생 시! (우아하게 대처)
// 로그를 남기고, 문제의 구버전 캐시를 삭제해버림
redisService.delete(userId);
System.out.println("구버전 데이터 발견! 캐시 갱신 필요.");
}
// 3. DB에서 원본 데이터 조회 (Failover)
User userFromDb = userRepository.findById(userId);
// 4. 새로운 버전(String age)으로 다시 캐싱
redisService.set(userId, userFromDb);
return userFromDb;
}
3. 심화: 커스텀 직렬화 (readObject) - 비추천
readObject와 writeObject 메소드를 오버라이딩해서 바이트 단위를 직접 파싱 하는 방법도 있습니다. 하지만 이 방법은 난이도가 매우 높고, 실수하면 데이터가 꼬일 위험이 너무 큽니다. 신입 개발자에게는 권장하지 않으며, 저 역시 실무에서 정말 불가피한 경우(프레임워크 내부 구현 등)가 아니면 사용하지 않습니다.
'JAVA' 카테고리의 다른 글
| about JVM (0) | 2025.09.30 |
|---|---|
| JDK, JRE, JVM (0) | 2025.09.30 |
| Primitive Type과 Reference Type (0) | 2025.09.30 |
| 자바의 hashCode(), hash()에 대하여 (0) | 2025.03.10 |