Skip to main content

Spring Security의 큰그림

· 6 min read

이 글을 읽은 후엔 자바 기반 웹 애플리케이션이 Http 요청을 처리하고 응답하는 구조에 대해 이해하고, 서블릿 컨테이너 내의 필터 부분을 확장하는 Spring Security의 구조에 대해 알 수 있습니다.

Request를 처리하는 과정

웹의 기본 구조는 단순하다. 사용자는 브라우저를 통해 HTTP 요청을 보내고, 웹 서버는 이 요청을 받아 그에 맞는 HTTP 응답을 돌려준다.

웹 서버의 주된 역할은 정적인 컨텐츠를 제공하는 것이다. 예를 들어 index.html을 요청하면 웹 서버는 단순히 그 파일을 찾아 그대로 응답하여 전달하는 것이다.

하지만 이 구조에는 한계가 있다. 웹 서버는 파일을 전달하기만할 뿐, 요청마다 각기 다른 데이터를 계산하거나 생성할 수는 없다.

그래서 WAS가 등장하게 되었다. 사용자는 검색어마다 달라지는 결과 페이지나 실시간으로 댓글이 달리는 게시물과 같은 동적 컨텐츠를 소비할 수 있게 되었다.

WAS는 웹 서버가 처리하지 못하는 복잡한 비즈니스 로직을 수행하는데 특화되어 있다. 사용자가 정적인 컨텐츠를 요구한다면 웹 서버가 리소스를 찾아서 주고, 만약 동적 컨텐츠를 요구하면 WAS에게 요청을 처리하도록 맡긴다. 이제 서버는 사용자가 원하는 컨텐츠를 자유롭게 제공할 수 있게 된다.

WAS? 서블릿 컨테이너?

WAS는 “웹 서버가 전달하는 요청을 처리하는 서버”라는 개념이고, 이를 구현하는 방식은 언어마다 조금씩 다르다. Java에서는 서블릿을 기반으로 구현하고, JavaScript에선 이벤트 루프와 미들웨어를 통해 구현된다. 이후 설명할 Spring Security 구조를 이해하기 위해 Java로 구현된 WAS에 대해 좀 더 알아보자.

자바 진영에서는 Jakarta EE(과거 Java EE)라는 표준 기술 명세로 구체화한다. Jakarta EE는 Java 기반 WAS라면 이러한 기능들을 제공해야 한다고 규칙을 정한 것이므로 Java WAS의 큰 구조는 대부분 비슷하다. Tomcat, Jetty 등은 이 명세에 맞춰 개발한 대표적인 WAS라고 할 수 있다.

Java 기반 WAS의 핵심은 서블릿 컨테이너이다. 서블릿 컨테이너는 웹 서버로부터 들어온 동적인 요청을 처리하기 위해 존재하며, Filter와 Servlet을 관리하고 Filter → Servlet 순으로 요청을 전달한다.

Filter

필터란 요청과 응답에 대해 정해진 작업을 수행하는 객체이다.[1] 서블릿에 도달하기 전후에 요청과 응답을 가로채서 요청 또는 응답에 포함된 정보를 변환하거나 사용한다.[2] 쉽게 말해, 최종 목적지(서블릿/컨트롤러)에 도착하기 전에 거치는 첫 번째 관문이다.

필터는 클럽 앞 문지기처럼 모든 요청에 대해 공통적으로 처리해야 할 작업들을 맡는다.

  • 인증/인가: 로그인된 사용자인지, 이 페이지에 접근할 권한이 있는지 확인한다
  • 문자 인코딩: 모든 요청의 문자 인코딩을 UTF-8 등으로 통일한다
  • 로깅: 모든 요청에 대한 기록을 남겨 나중에 추적하거나 분석할 수 있게 한다
  • 데이터 압축 및 암호화: 응답 데이터를 압축하여 전송 속도를 높이거나, 데이터를 암호화하여 보안을 강화한다

Servlet

서블릿이란 요청을 처리하고 응답으로 회신하는 클래스를 말한다.[3] 서블릿 내에서 핵심 비즈니스 로직이 동작하는 주체이다. 서블릿은 다음과 같은 순서로 동작한다.

  1. 클라이언트가 서버로 HTTP 요청을 보낸다.
  2. 요청은 먼저 Filter를 거쳐 필요한 전처리를 수행한다.
  3. 이후 요청이 서블릿(Container에 등록된 Servlet 클래스) 에 도달하면, 서블릿의 service() 또는 doGet(), doPost() 메서드가 실행되어 비즈니스 로직을 수행한다.
  4. 서블릿은 처리 결과를 HttpServletResponse 객체에 담아 응답을 생성한다.
  5. 생성된 응답은 다시 필터를 거쳐 후처리가 이루어지고, 마지막으로 클라이언트에게 전달된다.

서블릿 내부에서는 HTML 폼 데이터를 읽어 사용자 입력을 수집하거나, 데이터베이스를 조회해 결과를 보여주고, JSP나 템플릿을 통해 동적인 웹 페이지를 생성할 수 있다. 이 덕분에 서블릿은 웹 애플리케이션의 엔진 역할을 담당한다고 할 수 있다.

이 Filter-Servlet 구조를 기반으로 확장된 것이 바로 현대적인 프레임워크들이다.

  • Filter를 확장한 프레임워크 → Spring Security
  • Servlet을 확장한 프레임워크 → Spring MVC

즉, Spring Security는 필터 체인을 중심으로 인증·인가 과정을 관리하고, Spring MVC는 서블릿 기반으로 컨트롤러가 요청을 처리하며 응답을 반환하는 구조를 따른다.

Servlet Container에서 Filter

사실 앞서말한 “Filter 부분”은 FilterChain을 의미한다.

서블릿 컨테이너는 등록된 모든 필터를 FilterChain으로 묶어 순차적으로 실행한다.

  • 각 필터에서 chain.doFilter()를 호출하면 다음 필터로 넘어간다.
  • 만약 chain.doFilter()를 호출하지 않으면 요청 처리가 그 지점에서 중단된다.

Filter는 클라이언트의 요청이 서블릿에 도달하기 전, 그리고 서블릿이 생성한 응답이 클라이언트에게 전달되기 전에 동작한다.

모든 필터는 Filter인터페이스를 구현해야하며, 이 인터페이스는 다음의 세 가지 메서드를 정의한다.

public interface Filter {
void init(FilterConfig filterConfig);
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
void destroy();
}
  • init() : 필터 객체가 생성될 때 한 번 호출되고, 초기 설정이나 리소스 준비에 사용된다.
  • doFilter() : 요청이 들어올 때마다 호출되고, 요청 전후에 원하는 로직을 수행할 수 있다.
    이 메서드 안에서 chain.doFilter(request, response)를 호출하면 ****다음 필터 또는 서블릿으로 제어권이 넘어간다.
  • destroy() : 컨테이너 종료 시 호출되고, 리소스를 정리한다.

이제 기본적인 개념 이해는 끝났으니 시큐리티의 핵심중의 핵심인 필터 체인에 대해 알아보자.

Spring Security에서 Filter

Security는 앞서 배운 구조에서 Filter 부분을 확장한다. 앞서 말했듯 필터 체인은 필터를 묶어둔 것이다. 각 필터에는 정해진 순서가 있고, Spring Security(이하 Security) 내부적으로도 미리 정해둔 우선순위(order)에 따라 실행된다.

Security에서의 필터체인은 SecurityFilterChain이고, 어떤 SecurityFilter들이 사용되어야하는지를 결정한다. 필요한 Filter들만 조합하여 사용할 수 있는 장점이 있다.

출처: Spring Security Docs

Security에서는 여러 개의 필터 체인을 생성할 수 있다. 덕분에 요청의 성격에 따라 다양한 필터 조합을 구성하고, 요청마다 다른 필터 체인을 적용할 수 있다.

만약 필터 체인이 하나뿐이라면 모든 요청이 동일한 체인을 거치게 되겠지만, 여러 체인을 설정해두면 요청에 적합한 필터 체인을 선택적으로 적용할 수 있다.

출처: Spring Security Docs

그리고 이러한 과정을 담당하는 것이 바로 FilterChainProxy라는 특별한 필터이다. FilterChainProxy는 들어온 요청을 검사하여 어떤 FilterChain을 사용할지 결정하고, 해당 체인을 실행하도록 위임한다.

그렇다면 시큐리티에서는 요청에 맞게 동적으로 필터체인을 고르는데, 어떻게 서블릿의 Filter로 등록될 수 있을까?

이는 DelegatingFilterProxy라는 녀석이 도와준다. DelegatingFilterProxy는 Filter 구현체로, 서블릿 컨테이너와 스프링 컨텍스트를 이어주는 연결자 역할을 한다.

출처: Spring Security Docs

일반적으로 서블릿 컨테이너는 스프링 빈을 직접 인식하지 못하지만, DelegatingFilterProxy는 스프링 컨텍스트 내부에 등록된 FilterChainProxy 빈을 찾아 실제 필터 동작을 위임한다.

즉, 서블릿 컨테이너 입장에서는 단 하나의 필터(DelegatingFilterProxy)만 등록되어 있는 것처럼 보이지만, 실제 필터링 로직은 FilterChainProxy를 통해 스프링 시큐리티의 모든 필터 체인이 동작하게 되는 것이다.

전체적인 구조만 알아보았다.

Client로부터 요청이 들어왔을 때 서블릿 컨테이너는 어떻게 동작하고, Spring Security는 서블릿 컨테이너 위에서 필터링을 제공하는지 알아보았다.

사실 정말 중요한 인증, 인가에 대한 내용이 빠지게 되어 아쉽지만 자바 웹 애플리케이션의 요청 처리 과정과 Spring Security가 그 속에서 얼마나 똑똑하게 부가적인 기능을 제공하는지를 보여주기엔 충분했다고 생각한다.

공부한게 아쉬우니 다음엔 인증, 인가에 대해 알아보도록 하자!