본문 바로가기

(11.04) Spring Boot - Spring Boot의 스레드 모델, filter와 ThreadLocal, Spring Security, Spring Security 설정 방법

@starweb2025. 11. 4. 18:44

[ 12주차 - 1104 ] 

    금일 커리큘럼
        ├ 09:00 ~ 12:00 backend 프로그래밍 (Spring Boot의 스레드 모델, filter와 ThreadLocal)
        └ 13:00 ~ 18:00 backend 프로그래밍 (Spring Security, Spring Security 설정 방법)

1. Spring Boot의 스레드 모델

Spring Boot 스레드 모델은 주로 서블릿 컨테이너의 스레드 모델을 따름

  • 기본적으로 내장된 서블릿 컨테이너(Tomcat, Jetty, Undertow)를 사용하여 요청을 처리
  • 하나의 요청을 하나의 스레드가 처리하는 구조

Spring Boot 스레드 모델 특징

  • 요청이 들어오면 서블릿 컨테이너가 스레드풀에서 스레드를 할당함
  • 비동기 요청 처리를 지원하며, 별도의 비동기 작업 스레드 풀을 사용
  • 각 요청은 독립적인 스레드에서 처리되어 동시성 문제를 최소화

스레드 설정 (tomcat 기준)

tomcat은 기본적으로 최대 200개의 스레드, 최소 10개의 여유 스레드를 가짐

  • 스레드 수 관련 옵션
    • maxThreads: 최대 스레드 수
    • minSpareThreads: 최소 여유 스레드 수
    • acceptCount: 최대 대기 요청 수

설정 예시

  • application.properties
server.tomcat.max-threads=200
server.tomcat.min-spare-threads=10
server.tomcat.accept-count=100
  • application.yml
server:
  tomcat:
    max-threads: 200
    min-spare-threads: 10
    accept-count: 100

2. filter와 ThreadLocal 이해

Filter란?

  • 서블릿 요청과 응답을 가로채서 처리하는 컴포넌트
  • 주로 인증, 로깅, 인코딩 처리 등에 사용
  • 서블릿 컨테이너에서 제공하는 필터 체인에 등록되어 동작
public class SimpleFilter implements Filter {
    @Override
    public void doFilter(
        ServletRequest request,
        ServletResponse response,
        FilterChain chain
    ) throws IOException, ServletException {
        chain.doFilter(request, response); // 다음 필터 또는 서블릿으로 요청 전달
    }
    // 기타 init, destroy 메서드 오버라이드 등..
}

ThreadLocal이란?

  • 각 스레드마다 독립적인 변수를 저장할 수 있는 공간을 제공하는 클래스
  • 스레드 간에 데이터를 공유하지 않고, 스레드 로컬 변수를 통해 데이터 격리를 구현
  • 주로 요청 처리 중에 사용자 정보나 트랜잭션 정보를 저장하는 데 사용
public class ThreadLocalExample {
    // 각 스레드마다 독립적으로 값을 저장
    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("스레드별 값");
        System.out.println(threadLocal.get()); // 현재 스레드의 값 출력
        threadLocal.remove(); // 값 삭제
    }
}

Filter와 ThreadLocal의 관계

  • Filter에서 ThreadLocal을 사용하여 요청 처리 중에 스레드 로컬 데이터를 저장하고 관리할 수 있음
  • 인증 정보를 ThreadLocal저장하여 요청 처리 중에 쉽게 접근 가능하며,
    다른 계층에서도 해당 스레드로컬 값에 접근 가능
sequenceDiagram
    participant filter 1
    participant filter 2
    participant controller
    participant service
    participant ThreadLocal

    filter 1->>ThreadLocal: data 저장
    ThreadLocal->>filter 2: data 읽기
    ThreadLocal->>controller: data 읽기
    ThreadLocal->>service: data 읽기
  • filter1에서 요청처리된 data 값을 ThreadLocal 에 저장
  • 그다음 filter2 또는 controller, service 계층에서도 해당 ThreadLocal 의 데이터값 사용가능

Filter에서 ThreadLocal 사용 예제

// ThreadLocal 컨텍스트 설정 ------------------------
public class UserContext {
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    public static void setUser(User user) {
        userThreadLocal.set(user);
    }

    public static User getUser() {
        return userThreadLocal.get();
    }

    public static void clear() {
        // 요청이 끝난 후 해당 스레드풀 남아있는 상태에서 재사용되기 때문에 삭제필요
        userThreadLocal.remove();
    }
}

// user 정보 필터 ------------------------
@WebFilter(urlPatterns = "/hello") // 특정 URL 패턴에 필터 적용
public class UserFilter implements Filter {
    @Override
    public void doFilter(
            ServletRequest request,
            ServletResponse response,
            FilterChain chain
    ) throws IOException, ServletException {
        System.out.println("[UserFilter] doFilter - start");
        // try-finally 로 마지막 요청이 끝난 후 정리 보장
        try {
            String name = request.getParameter("name"); // 요청 파라미터에서 name 추출 (임시확인)
            UserContext.setUser(new User(name, 11));

            chain.doFilter(request, response);
            System.out.println("[UserFilter] doFilter - end");
            System.out.println(Thread.currentThread().getName());
        } finally {
            UserContext.clear(); // 필수!
        }
    }
    // 기타 init, destroy 메서드 오버라이드 등..
}
// 컨트롤러 ------------------------
@RestController
@Slf4j
public class UserController {
    private final UserService userService = new UserService();
    @GetMapping(value = {"/hello"})
    public String hello(){
        log.info("hello() 실행 !");
        userService.helloService();
        return "hello World";
    }
}
// 서비스 ------------------------
@Service
public class UserService {
    public void helloService() {
        System.out.println("[UserService] helloService() 실행");
        System.out.println(Thread.currentThread().getName());
        System.out.println(UserContext.getUser().getName());
    }
}
# 실행결과 "http://localhost:8080/hello?name=hong"
[UserFilter] init
[UserFilter] doFilter - start
00:00:00 [http-nio-8080-exec-2] INFO  packageSrc.UserController - hello() 실행 !
[UserService] helloService() 실행
http-nio-8080-exec-2    # 스레드 이름
hong                    # ThreadLocal에 저장된 name 값
[UserFilter] doFilter - end
http-nio-8080-exec-2    # 스레드 이름

주의사항

  • ThreadLocal에 저장된 데이터는 요청이 끝난 후 반드시 삭제해야 함
  • 스레드풀 환경에서 스레드가 재사용될 때 이전 요청의 데이터가 남아 있을 수 있음
  • 메모리 누수 문제를 방지하기 위해 clear() 메서드를 호출하여 ThreadLocal 데이터를 제거해야 함
  • 필터 끼리 데이터 사용시에도 try-finally문을 사용한 경우 정리 보장됨

3. Spring Security

Spring Security는 스프링 기반 애플리케이션에 보안 기능을 제공하는 프레임워크

  • 인증(Authentication)과 권한 부여(Authorization)를 처리
  • 다양한 보안 요구사항을 충족시키기 위한 기능 제공
  • 주로 모듈화, 확장성, 그리고 선언적 보안 설정에 기반을 둠

Spring Security의 주요 기능

  1. 인증(Authentication)
    • 사용자 신원 확인하는 과정
    • 다양한 인증 메커니즘 지원 (폼 로그인, HTTP Basic, OAuth2, JWT 등)
  2. 권한 부여(Authorization)
    • 인증된 사용자가 특정 자원에 접근할 수 있는지 결정
    • 역할 기반 접근제어 (RBAC)를 포함한 다양한 접근 제어방 식 지원
  3. 세션관리
    • 사용자의 세션의 생성, 유지, 만료 등을 관리하여 보안성 높임
  4. 보안 이벤트 로깅
    • 로그인 시도, 실패 등 보안 관련 이벤트를 로깅하여 감사 추적 가능

4. Spring Security 설정 방법

build.gradle 의존성 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

시큐리티 계정 설정

  • 기본값은 user / 랜덤 비밀번호 (server run 시 log에 출력됨)
  1. application.properties 설정 방법
spring.security.user.name=admin123     # 시큐리티 사용자명 설정
spring.security.user.password=admin123 # 시큐리티 비밀번호 설정
spring.security.user.roles=ADMIN       # 시큐리티 권한 설정
  1. application.yml 설정 방법
spring:
  security:
    user:
      name: admin123      # 시큐리티 사용자명 설정
      password: admin123  # 시큐리티 비밀번호 설정
      roles: ADMIN        # 시큐리티 권한 설정

security config 사용 예시

  1. 시큐리티 기본 설정 방법
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // 1. 기본 Spring Security 설정
        return http
                //  HTTP 요청에 대한 접근 권한을 설정
                .authorizeHttpRequests(auth -> auth
                    .anyRequest() // 모든요청
                    .authenticated() // 인증요구
                )
                .formLogin(Customizer.withDefaults()) // 기본 로그인 폼 사용
                .httpBasic(Customizer.withDefaults()) // HTTP Basic 인증 지원
                .csrf(Customizer.withDefaults()) // CSRF 보호 활성화
                .build();

    }
}
  1. 커스텀 URL 접근 권한 설정 방법
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 2. 특정 경로는 인증 없이 허용하는 설정
        return http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/", "/home", "/public/**").permitAll() // 인증 없이 허용
                        .requestMatchers("/admin/**").hasRole("ADMIN") // ADMIN 권한 필요
                        .anyRequest().authenticated() // 나머지는 인증 필요
                )
                .formLogin(Customizer.withDefaults())
                .build();
    }
}
  1. 커스텀 로그인 폼 설정 방법
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 3. 커스텀 로그인 폼 설정
        return http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/loginForm", "/").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        // 개발자가 작업한 로그인폼 화면 사용 (없으면 시큐리티 폼로그인 사용됨)
                        // .loginPage("/loginForm")
                        .loginProcessingUrl("/login_proc") // form의 action 경로 설정
                        .usernameParameter("email") // 사용자명 input name 지정 (기본 값: username)
                        .passwordParameter("password") // 비밀번호 input name 지정 (기본 값: password)
                        // 로그인 성공시 이동할 페이지 (true: 무조건 이동, false: 이전 페이지로 이동)
                        .defaultSuccessUrl("/dashboard", true)
                        .failureUrl("/fail") // 로그인 실패시 이동할 페이지
                )
                .build();
    }
}
  1. 로그인/로그아웃 핸들러 추가 방법
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 4. 로그인/로그아웃 처리 핸들러 추가
        return http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/loginForm", "/").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginPage("/loginForm")
                        // 로그인 성공 핸들러
                        .successHandler((request, response, authentication) -> {
                            System.out.println("로그인 성공: " + authentication.getName());
                            response.sendRedirect("/dashboard");
                        })
                        // 로그인 실패 핸들러
                        .failureHandler((request, response, exception) -> {
                            System.out.println("로그인 실패: " + exception.getMessage());
                            response.sendRedirect("/fail");
                        })
                )
                .logout(logout -> logout
                        // 로그아웃 처리 URL (없으면 시큐리티 기본 /logout 사용)
                        // .logoutUrl("/logout_process") // POST 전용 - GET방식은 보안 취약
                        .logoutSuccessUrl("/") // 로그아웃 성공 후 이동할 페이지
                        .addLogoutHandler((request, response, authentication) -> {
                            System.out.println("로그아웃 처리: 세션 및 쿠키 삭제");
                        })
                        .deleteCookies("JSESSIONID") // 쿠키 삭제
                )
                .build();
    }
}

시큐리티 설정 옵션 정리

  • authorizeHttpRequests()
    • requestMatchers(): 특정 URL 패턴에 대한 접근 권한 설정
    • permitAll(): 모든 사용자에게 접근 허용
    • anyRequest(): 모든 요청에 대해 접근 권한 설정
    • authenticated(): 인증된 사용자만 접근 허용
    • hasRole(): 특정 역할을 가진 사용자만 접근 허용
  • formLogin()
    • loginPage(): 커스텀 로그인 페이지 URL 설정
    • loginProcessingUrl(): 로그인 처리 URL 설정
    • usernameParameter(): 사용자명 input name 설정
    • passwordParameter(): 비밀번호 input name 설정
    • defaultSuccessUrl(): 로그인 성공 후 이동할 URL 설정
    • failureUrl(): 로그인 실패 후 이동할 URL 설정
    • successHandler(): 로그인 성공 시 커스텀 핸들러 설정
    • failureHandler(): 로그인 실패 시 커스텀 핸들러 설정
  • csrf()
    • 특정 URL 패턴에 대해 CSRF 보호 비활성화
  • httpBasic()
    • HTTP Basic 인증 활성화
    • HTTP Basic ? : 클라이언트가 요청 시 사용자명과 비밀번호를 HTTP 헤더에 포함시켜 인증하는 방식
  • logout()
    • logoutUrl(): 로그아웃 처리 URL 설정
    • logoutSuccessUrl(): 로그아웃 성공 후 이동할 URL 설정
    • addLogoutHandler(): 로그아웃 처리 시 커스텀 핸들러 설정
    • deleteCookies(): 로그아웃 시 삭제할 쿠키 설정

쿠키 삭제 확인 예제

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        return http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/hello").permitAll() // hello만 인증없이 허용
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginProcessingUrl("/login_proc") // form의 action 지정
                        .usernameParameter("email") // id 인풋 name 지정
                        .passwordParameter("passpw") // pw 인풋 name 지정
                        .successHandler((request, response, authentication) -> {
                            System.out.println("로그인 성공 !!" + authentication.getName());
                            response.sendRedirect("/info");
                        })
                        .failureHandler((request, response, exception) -> {
                            System.out.println("로그인 실패 !!" + exception.getMessage());
                            // 로그인 페이지로 다시 보내면서 error=true 전달
                            response.sendRedirect("/login?error=true");
                        })
                )
                .logout(logout -> logout
                        .logoutSuccessUrl("/hello") // 로그아웃시 해당 url로 이동
                        // 로그아웃 처리 핸들러 추가
                        .addLogoutHandler((request, response, authentication) -> {
                            System.out.println("로그아웃!!  세션, 쿠키도 삭제");
                            request.getSession(false).invalidate(); //세션 삭제
                        })
                        .deleteCookies("JSESSIONID") // JSESSIONID 쿠키 삭제
                )
                .build();
    }
}

 

로그아웃 전 쿠키 상태

로그아웃 후 쿠키 상태


starweb
@starweb :: starweb 님의 블로그

starweb 님의 블로그 입니다.

공감하셨다면 구독도 환영합니다!

목차