본문 바로가기

Spring Security 세션 Redis 저장 시 SerializationException 발생 원인과 해결

@6uiw2025. 10. 27. 23:41

 

🚀 Trouble Shooting

 

 

 

사전지식으로 먼저 Jackson이 JSON과 JAVA 객체를 변환하는 방식에 대해 이해하고 넘어가자.

Jackson은 객체를 JSON으로 직렬화하거나, JSON을 객체로 역직렬화할 때 다음과 같이 동작한다.


✔️ 직렬화(객체 → JSON)

  • 객체의 getter 메서드 또는 public 필드를 읽어서 JSON 문자열로 변환

✔️ 역직렬화(JSON → 객체)

  • JSON 데이터를 바탕으로 객체를 새로 생성해야 함
  • 기본적으로 Jackson은 기본 생성자 + setter 또는 @JsonProperty를 사용해서 필드를 채움

 

📌 문제 상황과 원인 파악

(로그인 동작 과정에서)

Redis에 저장된 session 데이터를 쉽게 확인하기 위해 Redis 직렬화를 JSON으로 설정하려할 때 역직렬화에 대한 문제가 발생했다.

 

<직렬화 설정 코드>

@Configuration
@EnableRedisHttpSession
public class RedisConfig {
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return RedisSerializer.json();
    }
}

 

<에러 로그>

org.springframework.data.redis.serializer.SerializationException:
Could not read JSON:
Cannot construct instance of `org.springframework.security.web.savedrequest.DefaultSavedRequest`
(no Creators, like default constructor, exist)

 

 

 

RedisConfig에서 JSON 직렬화를 지정하여 Spring Session이 세션 데이터를 Redis에 저장하고 가져올 때 JAVA <-> JSON으로 직렬화/역직렬화가 이루어지는데, 이때 DefaultSavedRequest 객체의 역직렬화 때문에 문제가 발생한다.

(DefaultSavedRequest는 내부 구조가 복잡해서 드물게 직렬화 자체에도 문제가 생길 수 있다.)

DefaultSavedRequest

로그인 전에 사용자가 가고 싶었던 페이지(URL) 정보를 잠시 기억해두는 객체

예시) 로그인하지 않은 사용자가 /admin 페이지를 요청 → Spring Security가 로그인 페이지로 리다이렉트
이때 사용자가 원래 가고 싶었던 요청 정보를 DefaultSavedRequest 객체에 저장해 놓음 

 

 

 

DefaultSavedRequest 객체는 다음과 같은 특징을 가진다.

  • 기본 생성자 없음
  • 대부분 필드가 final

따라서 Jackson으로 JSON 직렬화/역직렬화가 불가능하다.  위의 오류 코드를 다시보면

Cannot construct instance of `org.springframework.security.web.savedrequest.DefaultSavedRequest`
(no Creators, like default constructor, exist) 라고 적혀있는 것을 볼 수 있다. (로그가 매우 친절하다 ...)

 

 

📌 해결 방법 

1. 기본 JDK 직렬화 사용

가장 원초적인 방법은 역시 JSON 직렬화 설정을 하지 않는 것이다. (이게 해결 방법이라고 할 수 있는 건가...?)

세션 데이터는 보통 사용자의 로그인 상태를 유지시키거나, 로그인 전 요청 URL을 기억하는 등 서버가 애플리케이션 로직을 위해 관리하는 정보이므로, 대부분의 경우 사람이 읽을 필요가 없다. 따라서 특별한 이유가 없다면 굳이 JSON으로 변환할 필요 없이 기본 JDK 직렬화를 사용해 바이너리 데이터로 저장하면 된다.

 

 

2. DefaultSavedRequest를 세션에 저장하지 않도록 로그인 성공 리다이렉트를 고정

(이것도 해결 방법은 아닌 것 같은데..?)

http
    .formLogin()
        .defaultSuccessUrl("/index", true) // 항상 index 페이지로 리다이렉트

 

DefaultSavedRequest를 세션에 저장하지 않도록 리다이렉트 페이지를 고정한다...

하지만 이렇게 하면 로그인 전, 원래 요청 URL로 갈 수 없다는 치명적인 단점이 생겨버린다.

 

 

3. DefaultSavedRequest를 DTO로 변환 후 저장

JSON으로 직렬화를 가능하게하면서 세션을 확인할 수 있는 방법이다.

하지만 DTO를 생성하여 거쳐야한다는 번거로움과 DefaultSavedRequest의 일부 정보만 이용할 수 있다는 단점이 있다.

public class SavedRequestDTO {
    private String redirectUrl;
    private Map<String, String[]> parameters;
    // 생성자, getter, setter
}

// 세션에 저장할 때
SavedRequestDTO dto = new SavedRequestDTO(savedRequest.getRedirectUrl(), savedRequest.getParameterMap());
redisTemplate.opsForValue().set("savedRequest", dto);

 

 

 

 

4. Jackson Mix-in 사용

  • Jackson에게 직렬화/역직렬화 방법을 알려주는 Mix-in 어노테이션 사용

DefaultSavedRequest를 직접 수정하지 않고도 JSON 변환이 가능하다.

커스텀 Deserializer를 만들어 직렬화 가능한 형태로 변환 후 객체 복원하는 방법이다. 

 

Jackson에 대한 이해도 필요하고 까다로워서 한 2년 뒤에 써 볼거다.

@JsonDeserialize(using = DefaultSavedRequestDeserializer.class)
abstract class DefaultSavedRequestMixIn {}

ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(DefaultSavedRequest.class, DefaultSavedRequestMixIn.class);
 

 

Redis를 사용하면서 직렬화를 설정할 일이 참 많은 것 같다. 데이터의 종류와 쓰임에 따라 어떻게 직렬화해야할지 전략을 세우고 고민하는 과정이 필요했는데, 이와 더불어 직렬화/역직렬화 과정에서 생길 수 있는 문제점도 고려해야한다는 것을 깨달았다.

오늘 이 문제에 직면하면서 spring security의 DefaultSavedRequest 객체에 대해서도 한층 더 깊게 공부할 수 있었고, 직렬화와 역직렬화에 대해서도 복습할 수 있는 의미있는 시간이었던 것 같다. 

6uiw
@6uiw :: LOG.INFO("MING's DEVLOG")

개발을 하면서 공부한 기록을 남깁니다

목차