실행흐름 2차📚 전체 맵실행흐름 심화
06d · 3차 프로젝트 · Spring Security

SECURITY
FLOW

SecurityFilterChain 기동 순서 · JwtAuthenticationFilter 코드 실행 · SecurityContext Thread-Local · @AuthenticationPrincipal 주입 흐름

Boot — Security
기동 시 — SecurityFilterChain이 Heap에 올라가는 순서

3차는 2차 기동 순서에서 SecurityConfig의 filterChain() 빈이 추가됩니다. 이 빈이 만들어지는 순서와 무엇이 Heap에 올라가는지 봅니다.

1
SecurityConfig · Heap
filterChain() @Bean 메서드 실행
SecurityConfig.java — 기동 시 실행 순서
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // [1] Stack: filterChain() 프레임 push (기동 시 1회만)

    // [2] CSRF 비활성화 — REST API이므로
    http.csrf(csrf -> csrf.disable());

    // [3] 세션 정책 STATELESS — Session 객체 생성 안 함
    http.sessionManagement(s ->
        s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

    // [4] CORS 설정 객체 생성 → Heap
    http.cors(cors -> cors.configurationSource(corsConfigurationSource()));

    // [5] URL 권한 규칙 등록
    http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/api/user/**").permitAll()
        .requestMatchers("/api/admin/**").hasRole("ADMIN")
        .anyRequest().authenticated()
    );

    // [6] JwtAuthenticationFilter Bean을 Heap에서 꺼내서 등록
    http.addFilterBefore(
        jwtAuthenticationFilter,          // 이미 Heap에 있는 Bean
        UsernamePasswordAuthenticationFilter.class
    );

    // [7] SecurityFilterChain 객체 생성 → Heap
    return http.build();
    // Heap: SecurityFilterChain 객체 (서버 살아있는 한 유지)
    // 안에 Filter 순서 목록이 들어있음
}
HEAP — SecurityFilterChain, CorsConfigurationSource (Singleton)
2
Security · Heap
JwtAuthenticationFilter도 Heap에 올라가는 시점
@Component가 붙어있으므로 Bean 스캔 시 이미 Heap에 생성됨.
filterChain()에서 addFilterBefore(jwtAuthenticationFilter, ...)로 등록할 때는
새로 생성하는 게 아니라 이미 Heap에 있는 Bean의 참조를 FilterChain에 등록하는 것.

CustomUserDetailsService, JwtUtil도 마찬가지 — 기동 시 Heap에 생성, DI로 연결.
HEAP — JwtAuthenticationFilter, CustomUserDetailsService, JwtUtil (모두 Singleton)
📌 SecurityFilterChain 내부 Filter 순서 (Heap에 이렇게 등록됨)

SecurityContextHolderFilter (1순위)
CorsFilter (2순위)
JwtAuthenticationFilter (3순위 — addFilterBefore로 삽입)
UsernamePasswordAuthenticationFilter (4순위 — 우리는 미사용)
AuthorizationFilter (마지막 — URL 권한 체크)

모든 요청은 이 순서대로 Filter를 거칩니다.

Thread-Local
SecurityContextHolder — Thread-Local 메모리란

3차에서 가장 중요한 메모리 개념. request.setAttribute()와 어떻게 다른지 이해하면 Security 전체가 보입니다.

📌 Thread-Local이란

일반 변수는 여러 스레드가 공유할 수 있습니다.
ThreadLocal은 각 스레드마다 독립된 저장공간을 가집니다.

스레드 A가 ThreadLocal.set("value-A") 하면 → 스레드 A만 그 값 접근 가능.
스레드 B는 ThreadLocal.get() 해도 null — 스레드 A 값 못 봄.

SecurityContextHolder가 바로 이 ThreadLocal을 사용합니다.
요청마다 스레드가 다르므로 → 요청마다 완전히 독립된 인증 저장소가 생깁니다.

SecurityContextHolder — Thread-Local 동작 원리
// SecurityContextHolder 내부 (Spring Security 소스)
public class SecurityContextHolder {
    // 각 스레드마다 독립된 저장소
    private static final ThreadLocal<SecurityContext> contextHolder
        = new ThreadLocal<>();
}

// 요청 1 스레드 (홍길동 로그인)
SecurityContextHolder.getContext().setAuthentication(auth_홍길동);
// → 스레드1의 ThreadLocal에 저장

// 요청 2 스레드 (동시에 들어온 김영희 요청)
SecurityContextHolder.getContext().setAuthentication(auth_김영희);
// → 스레드2의 ThreadLocal에 저장 — 스레드1과 완전히 독립

// 요청 2가 스레드1의 인증에 접근하는 것은 불가능
Scenario 01
게시글 작성 — JwtAuthenticationFilter 코드 실행 순서 전체
1
POST /api/board/write — FilterChain → SecurityContext → @AuthenticationPrincipal
1
JwtAuthenticationFilter — OncePerRequestFilter
doFilterInternal() 실행 — 코드 한 줄씩
JwtAuthenticationFilter.java — 실행 순서
@Override
protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain) throws ... {

    // [1] Stack: doFilterInternal() 프레임 push

    // [2] 헤더에서 토큰 추출
    String authHeader = request.getHeader("Authorization");
    // authHeader → Stack 지역변수

    if (authHeader == null || !authHeader.startsWith("Bearer ")) {
        filterChain.doFilter(request, response); // 건너뜀
        return;
    }

    // [3] "Bearer " 제거
    String token = authHeader.substring(7);

    // [4] 토큰 유효성 검증 — jwtUtil(Heap Bean) 사용
    if (jwtUtil.validateToken(token)) {

        // [5] 토큰에서 username 추출
        String username = jwtUtil.getUsername(token);

        // [6] DB에서 CustomUserDetails 로드
        // customUserDetailsService(Heap Bean).loadUserByUsername()
        UserDetails userDetails =
            customUserDetailsService.loadUserByUsername(username);
        // Heap: new CustomUserDetails(user) 생성
        // userDetails → Stack 지역변수 (Heap 참조)

        // [7] Authentication 객체 생성 → Heap
        UsernamePasswordAuthenticationToken auth =
            new UsernamePasswordAuthenticationToken(
                userDetails,    // principal (CustomUserDetails)
                null,           // credentials (null — 이미 검증됨)
                userDetails.getAuthorities()  // [ROLE_USER] 또는 [ROLE_ADMIN]
            );
        // auth → Stack 지역변수 (Heap 참조)

        // [8] SecurityContext에 저장 — Thread-Local에 저장!
        SecurityContextHolder.getContext().setAuthentication(auth);
        // 이 스레드의 ThreadLocal에만 저장
        // 이후 같은 요청 처리 내 어디서든 꺼낼 수 있음
    }

    // [9] 다음 Filter로 넘김
    filterChain.doFilter(request, response);

    // [10] doFilterInternal() 프레임 pop
    // token, username, userDetails, auth 지역변수 소멸
}
STACK — token, username, userDetails, auth 지역변수 HEAP — new CustomUserDetails(), new UsernamePasswordAuthenticationToken() THREAD-LOCAL — SecurityContext에 auth 저장
2
AuthorizationFilter
Thread-Local에서 auth 꺼내서 URL 권한 체크
SecurityContextHolder.getContext().getAuthentication() → Thread-Local에서 auth 꺼냄 →
/api/board/write.anyRequest().authenticated() 체크 → auth 있으면 통과 → Controller로.
THREAD-LOCAL — auth 읽기 (소멸 안 함)
3
BoardController
@AuthenticationPrincipal — Thread-Local에서 자동 주입
BoardController.java — @AuthenticationPrincipal 주입 원리
@PostMapping("/write")
public ResponseEntity<?> write(
        @RequestBody BoardDto boardDto,
        @AuthenticationPrincipal CustomUserDetails userDetails) {

    // @AuthenticationPrincipal 처리 순서:
    // [1] Spring이 메서드 호출 전에 ArgumentResolver 실행
    // [2] SecurityContextHolder.getContext().getAuthentication() 호출
    // [3] authentication.getPrincipal() → CustomUserDetails 꺼냄
    // [4] 파라미터에 자동 주입
    // → 개발자가 null 체크, getAttribute() 코드 작성 불필요

    User loginUser = userDetails.getUser();
    // loginUser → Stack 지역변수 (Heap의 User 참조)

    boardService.write(boardDto, loginUser);
    return ResponseEntity.ok(...);
}
THREAD-LOCAL — getPrincipal()로 CustomUserDetails 꺼냄 STACK — userDetails, loginUser 지역변수
4
요청 처리 완료 후
SecurityContextHolderFilter가 Thread-Local 정리
응답이 완료되면 SecurityContextHolderFilter
SecurityContextHolder.clearContext() 실행 → Thread-Local에서 auth 삭제.
스레드가 ThreadPool로 반환될 때 깨끗한 상태가 됩니다.
(안 지우면 다음 요청이 같은 스레드를 재사용할 때 이전 인증 정보가 남아있는 버그 발생)
THREAD-LOCAL — clearContext() → 인증 정보 삭제
Scenario 02
관리자 API 접근 — hasRole("ADMIN") 체크 흐름
2
DELETE /api/admin/users/{id} — ROLE_ADMIN 체크 + @Transactional 삭제
1
JwtAuthenticationFilter
토큰에서 role 꺼내서 Authorities에 포함
CustomUserDetails.java — getAuthorities()
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    // user.getRole() → "ROLE_ADMIN" 또는 "ROLE_USER"
    return List.of(new SimpleGrantedAuthority(user.getRole()));
    // Heap: new SimpleGrantedAuthority("ROLE_ADMIN") 생성
}
// UsernamePasswordAuthenticationToken 생성 시 이 Authorities 포함
// → Thread-Local에 저장된 auth 안에 권한 정보가 들어있음
HEAP — new SimpleGrantedAuthority("ROLE_ADMIN") THREAD-LOCAL — auth.authorities = [ROLE_ADMIN]
2
AuthorizationFilter
/api/admin/** → hasRole("ADMIN") → Authorities 대조
Thread-Local에서 auth 꺼냄 → auth.getAuthorities() → [ROLE_ADMIN] 확인 →
hasRole("ADMIN") = hasAuthority("ROLE_ADMIN") → 통과.
ROLE_USER이면 → 403 Forbidden 즉시 반환 — AdminController 못 들어감.
3
AdminService · @Transactional
deleteUser() — @Transactional 코드 실행 순서
AdminService.java — deleteUser() @Transactional 흐름
@Transactional
public void deleteUser(Long userId) {
    // [1] AOP Proxy가 이 메서드 호출을 가로챔
    // [2] Proxy: 트랜잭션 시작 (DB 커넥션 획득, BEGIN 실행)
    // [3] Stack: deleteUser() 프레임 push

    User user = userRepository.findById(userId)
        .orElseThrow(() -> new RuntimeException("유저 없음"));
    // SELECT 쿼리 실행 → Heap에 User 객체

    // [4] 게시글 먼저 삭제 (FK 제약)
    boardRepository.deleteByUser(user);
    // DELETE FROM board WHERE user_id = ? 쿼리 실행
    // 아직 커밋 안 됨 — 트랜잭션 내에서 실행만 됨

    // [5] 유저 삭제
    userRepository.delete(user);
    // DELETE FROM users WHERE id = ? 쿼리 실행
    // 아직 커밋 안 됨

    // [6] Stack: deleteUser() 프레임 정상 종료
    // [7] Proxy: 예외 없이 종료 → COMMIT 실행
    // → 두 DELETE가 한 번에 DB에 반영

    // 만약 deleteByUser()나 delete() 중 예외 발생 시:
    // [7'] Proxy: ROLLBACK → 두 DELETE 모두 취소
}
HEAP — DB 커넥션 (트랜잭션 동안 점유, COMMIT 후 반납)
Memory Summary
3차 — 2차 대비 메모리 변화
영역2차3차 변화
Heap (추가) JwtFilter, JwtUtil, Bean들 SecurityFilterChain, JwtAuthenticationFilter, CustomUserDetailsService, CustomUserDetails 객체들 추가
Thread-Local (신규) 없음 SecurityContext — 요청마다 인증 저장, 완료 후 clearContext()
Heap (제거) request.setAttribute로 저장 request에 저장 불필요 — Thread-Local이 담당
Session 없음 (STATELESS) 없음 (STATELESS 명시 — SessionCreationPolicy.STATELESS)
실행흐름 2차📚 전체 맵실행흐름 심화