Serializable

2025. 12. 4. 16:07·JAVA

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;
    }
}

 

 

  1. serialVersionUID (버전 관리):
    • 문제점: 이를 명시하지 않으면 자바가 클래스의 구조를 보고 자동으로 해시값을 생성합니다. 만약 나중에 User 클래스에 필드를 하나 추가하면 이 값이 바뀌게 됩니다.
    • 결과: 이전에 저장해둔(직렬화된) 데이터를 불러오려(역직렬화) 할 때, 버전이 안 맞다며 InvalidClassException이라는 끔찍한 에러가 발생합니다.
    • 해결: 직접 1L 등으로 고정해두면, 클래스 구조가 조금 바뀌어도 호환성을 유지할 수 있습니다.
  2. 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), 새 필드를 추가"**하여 논리적으로 해결해야 합니다.

핵심 아이디어

  1. 기존 int age는 남겨둡니다. (과거 데이터 읽기용)
  2. 새로운 String ageGroup (또는 ageStr) 필드를 만듭니다.
  3. 데이터를 읽어올 때(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가 아니라 단순 캐시 용도라면, 굳이 옛날 데이터를 살리려 애쓸 필요가 없습니다. 과감하게 **"구버전 데이터는 버린다"**는 전략입니다.

핵심 아이디어

  1. User 클래스의 serialVersionUID를 2L로 변경합니다.
  2. 백엔드 코드에서 Redis 데이터를 불러올 때 try-catch로 감쌉니다.
  3. 직렬화 에러가 나면 "아, 이건 옛날 버전 데이터구나" 하고 무시한 뒤, 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
'JAVA' 카테고리의 다른 글
  • about JVM
  • JDK, JRE, JVM
  • Primitive Type과 Reference Type
  • 자바의 hashCode(), hash()에 대하여
이경빈
이경빈
이 블로그의 게시글을 바탕으로 학습하는 것을 권장하지 않습니다. 이 블로그의 게시글을 통해 무엇을 학습할 수 있고 무엇을 학습해야 하는지 알 수 있는 이정표 및 목차가 되었으면 합니다.
  • 이경빈
    메모용 블로그
    이경빈
    • 분류 전체보기 (44)
      • 기본개념 (0)
      • Git (2)
      • OOP (객체 지향 프로그래밍) (1)
      • CS (2)
      • JAVA (7)
        • 문법 (0)
        • stream api (1)
        • 자료구조 (1)
      • Spring (11)
        • Spring Context (1)
        • Spring beans (1)
        • Spring MVC (0)
        • RestTemplate (1)
        • JPA (1)
        • Mockito (1)
        • Spring Security (4)
        • Async (1)
      • Gradle (0)
      • 컴퓨터 네트워크 (6)
      • DBMS (2)
        • SQL (2)
      • 자료구조 (1)
      • 위클리페이퍼 (5)
      • AWS (0)
      • 프로젝트 (3)
        • 트러블슈팅 (1)
        • 회고 (2)
        • git hub 프로젝트 (0)
        • git in java (0)
      • 운영체제 (1)
      • Project (0)
        • 고민거리들 (0)
        • ai에게 요구한 수정사항 (0)
  • 전체
    오늘
    어제
  • hELLO· Designed By정상우.v4.10.3
이경빈
Serializable
상단으로

티스토리툴바