본문 바로가기
WEB/SpringBoot

Spring Boot HTTP 요청 처리 과정

by 정권이 내 2026. 3. 3.

🚀 Spring Boot 요청 처리 과정, 왜 알아야 할까요?

오늘은 웹 개발시 내부에서 어떤 일들이 벌어지는지 정확히 파악하기는 힘든 Spring Boot의 HTTP 요청 처리 과정에 대해 깊이 파헤쳐 보려고 합니다.

컨트롤러에 작성한 메서드가 실행되기까지, 클라이언트의 HTTP 요청은 보이지 않는 수많은 관문을 거칩니다. 실무를 하다 보면 "모든 요청의 로그를 남겨야 하는데 어디서 처리해야 하지?", "사용자 인증 검사는 컨트롤러마다 중복으로 적어야 하나?", "특정 API의 실행 시간만 측정하고 싶은데 코드를 다 뜯어고쳐야 할까?" 와 같은 고민에 반드시 부딪히게 됩니다.

이러한 문제들을 효율적으로 해결하기 위해서는 스프링 부트가 요청을 받아들이고 응답을 반환하기까지의 전체 라이프사이클을 완벽하게 이해해야 합니다.

각 관문(필터, 인터셉터, AOP 등)이 어떤 특징을 가지고 있고 어느 시점에 실행되는지 알아야 유지보수하기 좋은 클린 코드를 작성할 수 있기 때문입니다.

😅 시행착오: 흔히 하는 실수들

이 요청 처리 과정을 잘 모를 때 실무에서 가장 흔하게 겪는 시행착오 두 가지를 소개합니다.

1. HTTP 요청 본문을 여러 번 읽으려다 발생하는 오류

API의 요청 값(Request Body)과 응답 값을 로깅하기 위해 필터나 인터셉터에서 HttpServletRequest의 입력 스트림(InputStream)을 읽어 로그를 남기는 코드를 작성합니다. 코드를 완성하고 테스트를 해보면 정상적으로 로그가 찍힙니다.

하지만 정작 컨트롤러에서는 요청 본문이 비어있거나, 스트림이 이미 닫혔다는 에러(Stream closed)가 발생하며 애플리케이션이 뻗어버립니다.

이것은 톰캣과 같은 WAS가 제공하는 HTTP 요청 스트림이 단 한 번만 읽을 수 있도록 설계되어 있기 때문입니다. 필터에서 이미 스트림을 소비해버렸기 때문에, 정작 데이터를 처리해야 할 디스패처 서블릿과 컨트롤러에서는 데이터를 읽을 수 없게 된 것입니다.

이럴 때는 스프링이 제공하는 래퍼 클래스(ContentCachingRequestWrapper)를 사용하여 스트림의 내용을 메모리에 캐싱해두고 여러 번 읽을 수 있도록 처리해야 합니다.

2. 웹 로직과 비즈니스 로직의 혼동 (인터셉터 vs AOP)

사용자의 권한을 체크하는 로직을 AOP로 구현하거나, 반대로 서비스 계층의 트랜잭션 로깅을 인터셉터에서 처리하려고 시도하는 경우입니다.

동작 자체는 어떻게든 만들어낼 수 있지만, 이는 구조적으로 큰 문제를 야기합니다. 인터셉터는 웹 영역(Spring MVC)에 속해 있어 HTTP 요청과 응답 객체에 직접 접근하기 좋지만, AOP는 비즈니스 로직(Spring Context)에 침투하여 메서드의 실행 전후를 제어하는 데 특화되어 있습니다.

기술의 목적과 위치를 혼동하면 객체 지향적인 설계를 망치고 결합도를 높이는 최악의 결과를 낳게 됩니다.

🧠 핵심 : HTTP 요청이 Controller에 닿기까지와 그 이후

이제 클라이언트가 API를 호출했을 때, 서버 내부에서 데이터가 어떤 흐름을 타는지 순서대로 살펴보겠습니다.

1️⃣ Filter (필터): 웹 애플리케이션의 최전선

필터는 스프링 프레임워크가 아닌 자바 서블릿(Servlet) 스펙에서 제공하는 기능입니다. 따라서 스프링 컨텍스트(디스패처 서블릿)에 진입하기 전과 후에 동작하며, 웹 애플리케이션의 가장 바깥쪽 문지기 역할을 합니다.

주로 스프링과 무관하게 전역적으로 처리해야 하는 웹 보안(CORS 처리, XSS 방어), 인코딩 변환, 전체 요청에 대한 로깅 등을 담당합니다.

Spring Security 역시 이 필터를 기반으로 강력한 보안 체계를 구축하고 있습니다.

동일한 요청에 대해 한 번만 실행됨을 보장하는 스프링의 OncePerRequestFilter를 주로 상속받아 사용합니다.

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class RequestLoggingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {

        long startTime = System.currentTimeMillis();
        System.out.println(">>> [Filter] 요청 진입: " + request.getRequestURI());

        // 다음 필터 또는 디스패처 서블릿으로 요청 전달 (이 부분을 잊으면 안 됩니다!)
        filterChain.doFilter(request, response);

        long endTime = System.currentTimeMillis();
        System.out.println("<<< [Filter] 응답 반환: 걸린 시간 " + (endTime - startTime) + "ms");
    }
}

2️⃣ DispatcherServlet (디스패처 서블릿): 교통 정리

필터를 무사히 통과한 요청은 스프링 MVC의 심장인 디스패처 서블릿에 도착합니다.

디스패처 서블릿은 프론트 컨트롤러(Front Controller) 패턴이 적용된 클래스로, 애플리케이션으로 들어오는 모든 HTTP 요청을 가장 먼저 받아 알맞은 컨트롤러로 위임(Dispatch)하는 역할을 합니다.

URL, HTTP 메서드 등을 분석하여 핸들러 매핑(HandlerMapping)을 통해 어떤 컨트롤러의 어떤 메서드가 이 요청을 처리할지 찾아냅니다.

3️⃣ Interceptor (인터셉터): 스프링 컨텍스트의 시작

디스패처 서블릿이 컨트롤러를 찾아 호출하기 직전과 직후에 개입하는 것이 바로 인터셉터입니다.

필터와 비슷해 보이지만, 인터셉터는 스프링이 제공하는 기능이기 때문에 스프링 빈(Bean)으로 등록된 객체들에 자유롭게 접근할 수 있다는 강력한 장점이 있습니다.

인터셉터는 주로 인증 및 인가 검사(세션 체크, JWT 토큰 유효성 검증), 특정 컨트롤러에서만 필요한 공통 로직 처리 등에 사용됩니다.

HandlerInterceptor 인터페이스를 구현하여 작성하며, 컨트롤러 실행 전(preHandle), 실행 후(postHandle), 응답 완료 후(afterCompletion)의 세 가지 시점을 제어할 수 있습니다.

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println(">>> [Interceptor] 컨트롤러 호출 전: 헤더의 토큰 검증 시작");
        String token = request.getHeader("Authorization");

        if (token == null || !isValidToken(token)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return false; // false를 반환하면 더 이상 컨트롤러로 진입하지 않고 요청 처리를 중단합니다.
        }
        return true; // true를 반환하면 다음 단계(컨트롤러)로 넘어갑니다.
    }

    private boolean isValidToken(String token) {
        // 실제로는 JWT 파싱 및 서명 검증 로직이 들어갑니다.
        return "Bearer valid-token".equals(token);
    }
}

4️⃣ ArgumentResolver : 컨트롤러 맞춤형 데이터 가공

인터셉터를 통과하면 드디어 컨트롤러의 메서드가 실행될 준비를 마칩니다. 하지만 컨트롤러 메서드에는 다양한 파라미터가 존재합니다.

스프링은 HandlerMethodArgumentResolver를 사용하여 HTTP 요청 데이터(헤더, 쿠키, 본문 등)를 컨트롤러가 필요로 하는 객체로 변환하여 파라미터에 주입해 줍니다.

예를 들어, 사용자 인증 토큰에서 유저 ID를 추출하여 컨트롤러의 LoginUser 객체로 바로 매핑해주면, 컨트롤러 내부에서는 토큰을 파싱하는 불필요한 코드를 완전히 제거할 수 있습니다.

5️⃣ Controller, Service : 비즈니스 로직

요청 데이터가 완벽하게 세팅되면 컨트롤러 메서드가 실행됩니다.

컨트롤러는 요청을 해석하고 서비스(Service) 계층을 호출하여 핵심 비즈니스 로직을 수행하도록 지시합니다. 데이터베이스 작업이 필요하다면 리포지토리(Repository) 계층까지 내려가 데이터를 다루게 됩니다.

6️⃣ AOP (Aspect Oriented Programming): 관점지향 프로그래밍

서비스 계층이나 특정 메서드가 실행될 때, 핵심 비즈니스 로직과는 무관하지만 반드시 필요한 부가 기능(트랜잭션 관리, 성능 측정, 메서드 파라미터 로깅 등)을 분리하여 관리하는 기술이 바로 AOP입니다.

필터나 인터셉터가 HTTP URL 단위로 요청을 가로챈다면, AOP는 메서드 실행 단위로 개입합니다.

프록시(Proxy) 객체를 생성하여 타겟 메서드를 감싸는 방식으로 동작하며, 스프링의 @Transactional 어노테이션이 바로 이 AOP를 활용한 대표적인 기능입니다.

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PerformanceAspect {

    // com.example.service 패키지 하위의 모든 메서드에 적용
    @Around("execution(* com.example.service..*(..))")
    public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        System.out.println(">>> [AOP] 비즈니스 로직 실행 전: " + joinPoint.getSignature().getName());

        // 실제 비즈니스 메서드 실행
        Object result = joinPoint.proceed(); 

        long endTime = System.currentTimeMillis();
        System.out.println("<<< [AOP] 비즈니스 로직 실행 완료: " + (endTime - startTime) + "ms 소요");

        return result;
    }
}

7️⃣ ControllerAdvice와 ExceptionHandler: 전역 예외 처리

비즈니스 로직을 수행하다가 에러가 발생하면 어떻게 될까요?

스프링은 @RestControllerAdvice와 @ExceptionHandler를 통해 컨트롤러 계층에서 발생하는 예외를 전역적으로 수집하여 클라이언트에게 일관된 에러 응답 포맷(JSON 등)으로 반환할 수 있는 구조를 제공합니다. 이를 통해 지저분한 try-catch 블록 없이도 깔끔하게 예외 처리를 모듈화할 수 있습니다.

정리하자면, 클라이언트의 요청은 필터 -> 디스패처 서블릿 -> 인터셉터 -> 아규먼트 리졸버 -> 컨트롤러 -> AOP(서비스)의 순서로 들어가며, 응답은 그 역순으로 빠져나오게 됩니다.

이 거대한 파이프라인의 구조를 이해하면, 장애가 발생했을 때 어느 구역을 살펴봐야 할지 직관적으로 알아낼 수 있습니다.

📝 마무리: 오늘 내용 3줄 요약

  • 필터는 스프링 외부(서블릿)에서 전체 웹 요청의 전처리를, 인터셉터는 스프링 내부에서 컨트롤러 진입 전후의 처리를 담당합니다.
  • AOP는 웹 계층을 넘어 비즈니스 로직(메서드 단위)에 부가 기능(트랜잭션, 성능 측정 등)을 분리하여 주입하는 핵심 기술입니다.
  • 요청 처리의 전체 파이프라인 흐름을 이해하면, 중복 코드를 제거하고 적합한 계층에 로직을 배치하는 안정적인 아키텍처를 설계할 수 있습니다.
반응형

댓글