본문 바로가기
팀 프로젝트/플러스 프로젝트

Actuator가 자동으로 Session을 생성한다

by pon9 2025. 4. 2.

개요

Redis Hot Key Issue를 겪어보려고 Jmeter로 부하테스트를 돌리는 도중에 이슈가 발생했다.

정확히는 Hot Key 부하테스트와 관련은 없지만, Redis Insight를 뒤적거리다 발견했다.

어플리케이션을 켜놓고 가만히 놔뒀는데, 세션이 스스로 생성되었다..

하나에 632B라 무시할만한 수준이라고 생각할 수도 있지만, 15초에 한번 Metrics를 수집하며 이것의 TTL이 30분이니까 서버가 지속될 때 최대 약 74KB * 인스턴스 개수 만큼 Redis에 쌓인다.

그리고 어쨋든 버그기도 하고.. 거슬린다. 대체 왜 생긴걸까?

 

 

파악

너무 예전에 만들었던 Session + Security 구조라 기억이 잘 안 나니까 우선 세션이 어디에서 생성되는지 로그를 찍어보자.

@Slf4j
@Component
public class SessionTraceFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpSession before = req.getSession(false);

        chain.doFilter(request, response);

        HttpSession after = req.getSession(false);
        if (after != null) {
            if (before == null) {
                log.info("세션이 새로 생성됨 URI: {}", req.getRequestURI());
            }
        }
    }
}

필터를 하나 만들고, SecurityFilterChain에 추가해주었는데 

http.addFilterAt(new SessionTraceFilter(), UsernamePasswordAuthenticationFilter.class);

반응이 없다.... 왤까?

 

현재 Spring Boot 어플리케이션에는 두 개의 필터 체인이 동작하고 있다.

하나는 Servlet Container Filter Chain이다. Tomcat이나 Jetty같은 서버 수준에서 실행되는 필터들이고, 여기에 SessionRepositoryFilter 같은 게 포함된다. 이 필터는 실제로 Redis Session을 생성하고 관리하는 주체다.

다른 하나는 Spring Security FIlter Chain이다.

 

따라서, Security 체인 안에서 세션을 감지하기에 너무 늦다. 세션은 이미 서블릿 필터체인 단에서 생성된 상태고, getSession(false)로 감지해도 항상 null이 아닌 상태기 때문이다.

@Configuration
public class SessionTraceConfig {

    @Bean
    public FilterRegistrationBean<SessionTraceFilter> sessionTraceFilter1() {
        FilterRegistrationBean<SessionTraceFilter> reg = new FilterRegistrationBean<>();
        reg.setFilter(new SessionTraceFilter());
        reg.setOrder(-100);
        return reg;
    }
}

그래서 세션 생성을 감지하려면, 서블릿 레벨에서 감지해야 한다.

FilterRegistrationBean을 사용하여 전역적으로 필터를 등록해주자.

Tomcat이 필터를 구성할 때 setOrder()값을 기준으로 실행 순서를 정하기 때문에, SessionRepositoryFilter보다 더 앞에 배치할 수 있다.(-100) 즉, 세션이 만들어지기 전 상태에서 요청을 관찰할 수 있다.

그럼 이렇게.. ㅋ actuator에서 세션을 지맘대로 생성하고 있다는 걸 발견했다.

 

 

해결

1. 익명 사용자도 SecurityContext 생성, 이를 저장하려고 HttpSession이 생성됨

2. /actuator/prometheus는 인증이 필요한 자원이 아니지만, SecurityContext저장이 트리거되면서 세션이 생김

방법은 간단하다. 그냥 Actuator만을 위한 stateless SecurityFilter를 하나 더 만들어주자

@Configuration
public class ActuatorSecurityConfig {

    @Bean
    public SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .securityMatcher("/actuator/**")
                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }
}

actuator 경로에 한해 Session Stateless설정을 해주었다.

이제 로그도 안 쌓이고, actuator 경로도 정상 작동한다!
하지만 이 방법엔 문제가 있다. 일반 사용자가 actuator로 너무 편하게 접근이 가능하다.(.permitAll())

 

해결법을 찾아보니, HttpBasic을 적용하면 된다. prometheus에서도 지원한다.

spring.security.user.name=prometheus
spring.security.user.password=secret

application.properties 또는 yml에 이렇게 username과 password를 추가해주고,

scrape_configs:
  - job_name: 'app'
    metrics_path: '/actuator/prometheus'
    basic_auth:
      username: 'prometheus'
      password: 'secret'
    static_configs:
      - targets: ['host.docker.internal:8080']

prometheus.yml(프로메테우스 설정파일) 에서 해당 닉네임과 비밀번호를 작성해준다.

.httpBasic(Customizer.withDefaults())

그리고 securityFilterChain에 해당 설정을 추가해주면 된다.

그냥 이 상태로 어플리케이션을 실행하면,

해당 에러가 뜨는데, 비밀번호엔 인코딩된 값을 넣어줘야 한다..ㅎ

System.out.println(new BCryptPasswordEncoder().encode("secret"));

간단하게 이렇게 인코딩한 값을 넣어주면 된다.

9090포트로 접속하면 메트릭을 잘 수집하고 있는 것을 볼 수 있다.

 

 

그런데

사실 근본적인 문제는 저 ALWAYS 설정이긴 하다.

예전에 무슨 생각이었는진 모르겠지만 익명 사용자에게도 세션을 부여하고 싶었나보다.

근데 좀 찾아보니까 익명사용자에게 세션이 필요하면 ALWAYS가 아니라, IF_REQUIRED로 설정하고

controller에서 명시적으로 req.getSession을 한다고 한다..

 

오늘도 뭐 대충 삽질했다고 볼 수 있다