웹 애플리케이션을 구축할 때 결코 타협할 수 없는 가장 중요한 요소는 바로 ‘보안(Security)’입니다. 자바 백엔드 생태계에서 스프링 부트(Spring Boot)로 개발을 진행할 때, 회원가입, 로그인, 접근 권한 제어 등 보안 기능을 구현하기 위해 필수적으로 사용하는 프레임워크가 바로 스프링 시큐리티(Spring Security)입니다.
스프링 시큐리티는 막강하고 유연한 기능을 제공하지만, 내부 아키텍처와 구동 원리를 모른 채 설정 코드만 복사해서 사용하면 아주 작은 에러나 커스텀 요구사항 앞에서도 무너지기 쉽습니다.
이번 글에서는 스프링 시큐리티의 핵심 근간인 서블릿 필터 체인(Filter Chain)의 구조부터, 사용자가 로그인을 시도할 때 내부에서 일어나는 인증(Authentication) 및 인가(Authorization)의 전체 흐름을 심도 있게 파헤쳐 보겠습니다.
1. 스프링 시큐리티의 핵심 기반: 필터 체인 (Filter Chain)
지난 7편 글에서 서블릿 필터(Filter)는 스프링 컨텍스트 바깥(서블릿 컨테이너)에서 동작한다고 설명해 드렸습니다. 그렇다면 스프링 컨텍스트 내부의 빈(Bean)과 기술을 적극적으로 활용하는 스프링 시큐리티는 어떻게 웹 요청의 최전선인 필터 영역에서 보안 검사를 수행할 수 있을까요?
그 비밀은 바로 DelegatingFilterProxy와 FilterChainProxy라는 두 클래스의 협업에 있습니다.
- DelegatingFilterProxy: 서블릿 컨테이너(Tomcat 등)가 구동될 때 생성되는 서블릿 필터입니다. 이 필터는 자체적으로 보안 로직을 수행하지 않고, 스프링 컨텍스트 내부에 등록된 특별한 스프링 빈에게 보안 처리를 위임(Delegate)하는 다리 역할을 합니다.
- FilterChainProxy: DelegatingFilterProxy로부터 요청을 위임받는 스프링 빈입니다. 이 클래스는 스프링 시큐리티가 제공하는 여러 개의 보안 필터 리스트인 Security Filter Chain을 순서대로 실행하며 본격적인 검증을 시작합니다.
스프링 시큐리티는 사용자의 요청이 컨트롤러에 도달하기도 전에 최소 수십 개의 보안 필터를 거치게 하여 원천적으로 안전한 환경을 보장합니다.
2. 반드시 구분해야 할 두 개념: 인증(Authentication)과 인가(Authorization)
스프링 시큐리티의 모든 아키텍처는 크게 두 가지 핵심 단계로 나뉩니다. 두 개념의 차이를 명확히 아는 것이 시큐리티 학습의 시작입니다.
- 인증 (Authentication): “당신은 누구십니까?”를 검증하는 단계입니다. 로그인 아이디와 비밀번호를 입력받아 시스템에 등록된 올바른 사용자가 맞는지 신원을 확인하는 과정입니다.
- 인가 (Authorization): “당신은 이 자원에 접근할 권한이 있습니까?”를 검증하는 단계입니다. 인증을 마친 사용자가 관리자 페이지(Admin)나 결제 페이지 등 특정 시스템 자원에 접근할 수 있는 권한(Role)이 있는지 체크하는 과정입니다.
3. 폼 로그인으로 보는 스프링 시큐리티 인증(Authentication) 흐름 8단계
사용자가 아이디와 패스워드를 입력하고 로그인 버튼을 누르는 순간, 스프링 시큐리티 내부에서는 백엔드 면접 단골 질문인 인증 시스템 아키텍처 구조가 역동적으로 동작합니다.
1단계: 로그인 요청 수신
사용자가 아이디와 비밀번호가 담긴 HTTP POST 요청을 보냅니다. 이 요청은 시큐리티 필터 체인 중 하나인 UsernamePasswordAuthenticationFilter에 도달합니다.
2단계: 인증 토큰(Authentication) 생성
필터는 요청에서 아이디(username)와 패스워드(password)를 추출하여 인증을 받기 전의 미완성 객체인 UsernamePasswordAuthenticationToken을 생성합니다.
3단계: AuthenticationManager에게 토큰 전달
필터는 생성된 인증 토큰을 인증을 총괄하는 컨트롤러 타워인 AuthenticationManager(실제 구현체는 ProviderManager)에게 전달하며 인증 처리를 위임합니다.
4단계: 적절한 AuthenticationProvider 탐색
ProviderManager는 자신이 가진 여러 개의 AuthenticationProvider들을 순회하며, “현재 들어온 토큰(폼 로그인, 소셜 로그인, JWT 등)을 처리할 수 있는 적절한 해결사가 누구인지” 찾아서 토큰을 넘깁니다.
5단계: UserDetailsService를 통한 사용자 정보 조회
폼 로그인을 담당하는 AuthenticationProvider는 사용자의 신원을 확인하기 위해 UserDetailsService 인터페이스의 loadUserByUsername() 메서드를 호출합니다. 이 메서드는 DB에서 해당 아이디를 가진 유저 정보를 조회합니다.
6단계: UserDetails 반환 및 패스워드 검증
DB 조회가 성공하면 유저 정보와 권한 목록이 담긴 UserDetails 객체가 반환됩니다. AuthenticationProvider는 이 UserDetails에 담긴 암호화된 비밀번호와 사용자가 입력한 비밀번호가 일치하는지 PasswordEncoder를 이용해 매칭 검증을 수행합니다.
7단계: 인증 완료된 Authentication 객체 생성
비밀번호가 일치하여 인증이 성공하면, AuthenticationProvider는 사용자의 신원 정보와 권한(Authorities) 리스트가 포함된 ‘인증이 완료된 완전히 새로운 꽉 찬 토큰 객체’를 생성하여 위로 반환합니다.
8단계: SecurityContextHolder에 인증 정보 저장
최초의 필터로 돌아온 완선된 인증 객체는 스프링 시큐리티의 메모리 저장소인 SecurityContextHolder 내부의 SecurityContext에 안전하게 보관됩니다. 이 작업이 끝나면 비로소 세션에 로그인 정보가 저장되고 인증 단계가 종료됩니다.
4. 자원을 보호하는 인가(Authorization) 흐름
인증을 무사히 마친 사용자가 마이페이지나 관리자 기능에 접근하려고 하면, 필터 체인의 가장 마지막 단에 위치한 AuthorizationFilter(구 버전의 FilterSecurityInterceptor)가 작동합니다.
- 인증 정보 확인:
AuthorizationFilter는SecurityContextHolder에서 현재 사용자의 인증 객체를 꺼냅니다. - 권한 매칭: 사용자가 요청한 주소(예:
/admin/)에 설정된 권한 요구사항(hasRole('ADMIN'))과 사용자가 가진 실제 권한 목록을 대조합니다. - 접근 제어: 만약 사용자가 권한을 가지고 있지 않다면
AccessDeniedException예외를 발생시키고 403 Forbidden 응답을 내려 요청을 차단합니다.
5. 핵심 요약: 스프링 시큐리티 핵심 컴포넌트 역할 총정리
| 컴포넌트 명칭 | 핵심 역할 |
| SecurityContextHolder | 현재 인증된 사용자 정보(Authentication)가 저장되는 스레드 로컬 메모리 공간 |
| Authentication | 유저의 신원 정보(Principals), 비밀번호(Credentials), 권한(Authorities)을 담는 토큰 객체 |
| AuthenticationManager | 인증 처리를 총괄하는 인터페이스 (실제 검증은 Provider들에게 위임) |
| AuthenticationProvider | 실제 구체적인 인증 로직(비밀번호 비교, 토큰 검증 등)을 직접 수행하는 해결사 |
| UserDetailsService | 데이터베이스나 인메모리 등에서 유저 정보를 조회해 오는 인터페이스 |
| UserDetails | 스프링 시큐리티가 이해할 수 있는 형태의 커스텀 유저 정보 데이터 모델 |
6. 결론
스프링 시큐리티는 수많은 컴포넌트가 정교한 톱니바퀴처럼 맞물려 돌아가는 거대한 성벽과 같습니다. 단순하게 환경 설정 파일만 다루는 수준을 넘어, Filter -> Token -> Manager -> Provider -> UserDetailsService로 이어지는 유기적인 흐름 아키텍처를 이해하고 있어야만, 향후 실무에서 사용될 JWT(Json Web Token) 기반 커스텀 필터 구현이나 OAuth2 소셜 로그인 연동을 막힘없이 완수할 수 있습니다.