회사에서 진행하고 있는 프로젝트에서 문제가 하나 생겼다.
기존 환경에서 세션 클러스터링으로 세션을 공유하고 있던 프로젝트들이 쪼개지면서 일부 프로젝트에서 정책상 직접 레디스에 접근할 수 없게 되었다. 그래서 로그인 상태를 확인하고 정보를 가져오는데 문제가 발생했다.
이 문제를 어떻게 풀어갈 것인가...
해결 방안은 의외로 간단하게? 나왔다.
레디스를 직접 바라보는게 아니라 레디스 접근이 가능한 프로젝트에 요청을 보내 간접적으로 로그인 정보를 얻어오자는 것이다.
좋은 우회 방안이라고 생각했다. 그런데 여기서 또 다시 문제가 생겼다.
해당 프로젝트들에서는 레디스를 이용한 스프링 세션을 사용하고 있었는데 사실 기존에 단순히 세션에 Attribute를 추가하거나 꺼내오는식으로 사용했기 때문에 직접 Key를 가지고 조회할 일이 없었다.
얼떨결에 내가 그 중심이 되는 프로젝트에서 직접 세션을 조회하는 코드를 구현하는 임무를 맡아서 우여곡절 끝에 성공했던 이야기를 해보고자 한다.
문제 상황 다시 구현해보기
한 가지 문제가 있다.
그것은 현재 프로젝트가 폐쇄망 환경이라 코드를 일부 참고해서 작성하거나 할 수 없다는 것이다.
그렇기 때문에 최대한 문제 상황과 비슷하게 집에서 스프링 부트로 작성하였다.
해당 프로젝트는 스프링인데 차이점은 뒤에서 한번 짚어보도록 하겠다.
SessionTestController.java
@Slf4j
@RequiredArgsConstructor
@RestController
public class SessionTestController {
private final RedisTemplate<Object, Object> redisTemplate;
@GetMapping("/set")
public String set(HttpServletRequest request) {
request.getSession().invalidate();
request.getSession().setAttribute("asdf", new TestUser("mega", "5346346"));
return "hi";
}
@GetMapping("/get")
public String get(HttpServletRequest request) {
return String.valueOf(request.getSession().getAttribute("asdf"));
}
@GetMapping("/get2")
public String get2(@RequestParam String id) {
TestUser o = (TestUser) redisTemplate.opsForHash().get("megamaker:sessions:" + id, "sessionAttr:asdf");
Map<Object, Object> entries = redisTemplate.opsForHash().entries("megamaker:sessions:" + id);
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
log.debug("key: {}, value: {}", entry.getKey(), entry.getValue());
}
return null;
}
}
TestUser.java
@ToString
@Getter
@AllArgsConstructor
public class TestUser implements Serializable {
private String id;
private String pwd;
}
/set을 호출하면 세션에 임의의 유저를 저장한다.
/get을 호출하면 기존에 세션에 저장되었던 정보를 가져온다. 여기까지가 기존 세션을 사용하는 방식이다.
/get2는 쿠키에 저장되어 있는 세션 ID를 파라미터로 받아 직접 레디스에서 값을 조회한다.
지금 사용하고 있는 버전에서는 쿠키의 세션 ID가 Base64로 인코딩 되어 있다.
따로 설정을 추가하면 기존 방식대로 사용할 수 있지만 현재 테스트에서 중요한 부분은 아니기에 일단 직접 디코딩해서 사용하기로 했다.
application.yaml
spring:
application:
name: springstudy
data:
redis:
host: localhost
port: 6379
password: 1234
session:
redis:
namespace: megamaker
server:
servlet:
session:
cookie:
name: MEGAMAKERSESSION
logging:
level:
com.megamaker.springstudy: debug
테스트용으로 간단하게 설정을 해주었다.
namespace 설정으로 레디스에 저장되는 키 구조를 약간 변경시켜 주었고,
쿠키에 저장되는 세션 ID의 이름도 변경해주었다.
이렇게 하면 기존 디폴트 값인 SESSION과 달라져서 직접 설정했다는 것을 명확하게 확인할 수 있을 것이다.
문제 상황 3가지
그런데 문제는 여기서 발생한다.
- 키 값을 어떻게 알아서 가져올 것인가?
- 레디스의 어떤 타입으로 가져와야 하는가?
- 가져온 값이 깨져서 보인다...
첫 번째 문제는 의외로 간단하다.
크게 두 가지 방법이 있는데
직접 레디스에서 모든 Key를 조회하거나(키가 엄청 많으면 운영 환경에 영향을 줄 수 있으니 개발 환경에서만 사용하자...)
스프링에서 사용하는 세션 조회 방식을 직접 확인하고 거기서 사용되는 Key를 확인하는 방법이다.
스프링 세션을 사용하면 자동으로 SessionRepositoryFilter가 서블릿 필터 체인에 추가된다.
해당 필터에서 세션을 저장하거나 조회한다.
직접 조회하기

해당 필터에서는 SessionRepository를 주입받는데 해당 인터페이스는 아래와 같다.

Spring Session Data Redis를 추가하면 위 인터페이스의 구현체가 추가되어 주입된다.

RedisSessionRepository를 확인해보면

sessionKey가 어떻게 지정되는지 확인할 수 있는 코드가 있다.
여기를 직접 확인해보자.

조회를 해보니 megamaker:sessions:어쩌구저쩌구 로 Key가 설정되는 것을 확인할 수 있다.
megameker는 내가 이전에 application.yaml에서 namespace 설정을 해주었기 때문에 이렇게 표시된다.
keys()로 조회하기
@GetMapping("/get")
public String get(HttpServletRequest request) {
Set<Object> keys = redisTemplate.keys("*");
keys.forEach(k -> {
log.debug("key : {}", k);
});
return String.valueOf(request.getSession().getAttribute("asdf"));
}
직접 Redis에 접속하여 조회할 수도 있지만 redisTemplate의 keys() 메서드로도 저장되어 있는 Key 목록을 조회할 수 있다.

이런식으로 키가 어떤 방식으로 저장되는지 확인할 수 있다.
데이터 타입 조회하기
다음은 찾아올 때 어떤 타입으로 가져와야 하는가를 살펴보겠다.
간단하게 문자열만 키와 값으로 저장하려면 strings로 저장하거나 하면 되지만 우리는 Spring Session Data Redis가 어떤 데이터 타입으로 Redis에 저장하는지 모른다.
SessionRepositoryFilter에서 세션을 만드는 부분을 살펴보자.

세션을 만드는 부분 역시 SessionRepository의 메서드를 이용하고 있다.

RedisSession이라는 것을 만들어서 반영하는 것을 볼 수 있다.



계속 따라가봤다.
결국 저장하는 부분에서 Hash 데이터 타입을 이용한다는 것을 알 수 있었다.
그러면 실제로 해당 타입으로 저장되는지 직접 조회해서 확인해보자.
@GetMapping("/get2")
public String get2(@RequestParam String id) {
DataType type = redisTemplate.type("megamaker:sessions:" + id);
log.debug("type : {}", type);
}

확실히 Hash로 저장되는 것을 알 수 있었다.
가져온 값이 이상하게 깨지는 문제
가장 큰 위기를 안겨주었던 문제이다.
조회하는 것까지는 성공했었으나 가져온 값이 인코딩이 이상하게 적용된 것처럼 깨져서 보여졌다.
일단 원인부터 바로 알아보면 RedisTemplate의 Serializer 설정이 Spring Session Data Redis에서 사용한 설정과 달랐기 때문이다.
아래는 RedisHttpSessionConfiguration에서 SesisonRepository를 스프링 빈으로 등록하는 부분이다.

여기서 생성되는 RedisTemplate을 살펴보면

위와 같이 Serializer 설정이 되어있는 것을 볼 수 있다.
여기서 우리는 Hash를 사용하기 때문에 hashKeySerializer와 hashValueSerializer를 보면 된다.
실제로 문제가 되었던 부분은 여기서 생성되는 RedisTemplate 빈과 주입받아 사용하는 RedisTemplate의 빈이 달라서 그런 것이었다.
공통 팀에서 만들어준 설정이 위와 달랐고, 나는 이를 위처럼 hashKeySerializer 설정을 해달라고 요청하여 문제를 해결할 수 있었다.
RedisConfig.java
@RequiredArgsConstructor
@Configuration
public class RedisConfig {
private final RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
RedisTemplate 설정은 위와 같이 했는데 사실 나는 집에서 테스트 용으로 스프링 부트로 만들어서 문제가 됐던 상황과는 조금 다르다.
위에서 SessionRepository를 생성할 때 만들어지는 RedisTemplate은 따로 빈으로 등록되지 않고 바로 저장소에 할당되지만, 회사 프로젝트에서는 Spring Session Data Redis의 버전을 1.3.1Release로 사용하였기 때문에 위 부분이 조금 다르다.
spring-session/spring-session/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionCo
Spring Session. Contribute to spring-projects/spring-session development by creating an account on GitHub.
github.com
확인만 할거기 때문에 라이브러리를 따로 추가하지 말고 코드만 들어가서 봐보자.

이 버전에서는 sessionRedisTemplate이라는 빈을 등록해서

SessionRepository에 해당 빈을 주입하는 것을 알 수 있다.
그렇다는것은 즉, 저 빈을 @Qualifier로 직접 지정하여 주입받아 opsForHash()나 뭐 여러 가지를 수행할 수 있었다는 얘기다.
그래도 어차피 따로 새롭게 빈을 공통팀에서 등록한 상황이니 만들어 주는 것을 사용하는게 일관성있고 유지보수하기 쉽지 않을까 싶다.
위의 존재를 모르는 누군가가 나중에 위 내용을 찾아 헤맬 수도 있기 때문에...
참고로 세션 값을 조회하면

이것들이 들어 있다.
그러니까 한 번 더 안에서 저 sessionAttr:asdf 녀석을 가져와야 한다는 것이다.
요거는 아래처럼 entries() 메서드를 이용해서 조회할 수 있다.
@GetMapping("/get2")
public String get2(@RequestParam String id) {
DataType type = redisTemplate.type("megamaker:sessions:" + id);
log.debug("type : {}", type);
TestUser o = (TestUser) redisTemplate.opsForHash().get("megamaker:sessions:" + id, "sessionAttr:asdf");
Map<Object, Object> entries = redisTemplate.opsForHash().entries("megamaker:sessions:" + id);
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
log.debug("key: {}, value: {}", entry.getKey(), entry.getValue());
}
return null;
}
get()에다가 key와 hashKey 까지 입력하면 한번에 가져올 수 있다.
이렇게 들어가는 이유는...?


아까 봤던 RedisSession에서 그렇게 들어가기 때문이다.
'공부 > 기타' 카테고리의 다른 글
| [Jenkins] 2. Github Webhook 연결하기 (0) | 2025.11.09 |
|---|---|
| [Jenkins] 1. 젠킨스(Jenkins) 설치 및 초기 설정하기 (0) | 2025.10.25 |
| [HTTPS] 하나의 IP에서 여러 도메인 HTTPS 처리하기 (0) | 2024.11.18 |
| [Git] 잔디 안 심어지는 문제 해결하기 (0) | 2024.10.09 |
| [Ubuntu] NGINX로 포트포워딩 구성하기 (0) | 2024.10.05 |