김코딩

반복되는 로그인 체크, 필터 하나로 끝내기(Filter 사용법) 본문

스프링

반복되는 로그인 체크, 필터 하나로 끝내기(Filter 사용법)

김코딩딩 2025. 5. 22. 14:20

이번에 진행한 일정 관리 API 구현에서는
로그인한 사용자만 다음과 같은 기능을 사용할 수 있도록 인증 로직을 구현했습니다.

 

  • 회원 정보 조회, 수정, 삭제
  • 일정(게시물) 작성, 조회, 수정, 삭제

세션 기반 로그인 인증을 통해, 사용자가 로그인한 경우에만 민감한 기능에 접근할 수 있도록 제한했는데요.

그런데 개발을 진행하다 보니, 매번 컨트롤러의 민감한 API마다 세션을 꺼내서 확인하고, 예외를 던지는 인증 코드를 직접 구현하는 방식이 반복적이고 비효율적이라는 생각이 들었습니다.

"매번 똑같은 인증 로직, 자동으로 처리할 수는 없을까?"

 

이런 고민 끝에, 저는 서블릿 필터(Filter) 를 도입하게 되었습니다.


서블릿 필터(Filter)란?

서블릿 필터는 웹 애플리케이션에서 HTTP 요청과 응답을 가로채어 사전 작업을 처리할 수 있는 컴포넌트입니다.

즉, 사용자의 요청이 컨트롤러에 도달하기 전에 필터를 먼저 거치게 되며,
이 과정에서 공통적인 사전 처리(예: 인증, 로깅, 인코딩 설정 등)를 수행할 수 있습니다.

 

쉽게 말해, 필터는 웹 애플리케이션의 입구에서 '문지기 역할'을 수행합니다.
로그인하지 않은 사용자가 민감한 API에 접근하려고 하면,
필터는 이를 차단하고 "로그인이 필요합니다" 같은 응답을 보낼 수 있습니다.

필터의 흐름

HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> Dispatcher Servlet -> 컨트롤러

  • 기본적으로 필터는 dispatcherServlet 이전에 호출된다.
  • 필터는 체인(chain)으로 구성되어 여러 개의 필터를 자유롭게 추가할 수 있다.

Filter 인터페이스

  • 주요 메서드
    1. init()
      • Filter를 초기화하는 메서드이다.
      • Servlet Container가 생성될 때 호출된다.
      • default method이기 때문에 implements 후 구현하지 않아도 된다.
    2. doFilter()
      • Client에서 요청이 올 때 마다 doFilter() 메서드가 호출된다.
        • doFilter() 내부에 필터 로직(공통 관심사 로직)을 구현하면 된다.
      • WAS에서 doFilter() 를 호출해주고 하나의 필터의 doFilter()가 통과된다면
      • Filter Chain에 따라서 순서대로 doFilter() 를 호출한다.
      • 더이상 doFilter() 를 호출할 Filter가 없으면 Servlet이 호출된다.
    3. destroy()
      • 필터를 종료하는 메서드이다.
      • Servlet Container가 종료될 때 호출된다.
      • default method이기 때문에 implements 후 구현하지 않아도 된다.

간단하게 말해서 Filter를 구현할 때 init, destroy 메서드는 default 메서드이기 때문에 구현을 하지 않아도 되고, doFilter 메서드만 오버라이딩하여 구현을 하면 됩니다.


필터를 사용하지 않았더라면

로그인 인증이 필요한 URL은 어떤 것들일까?

구분 HTTP Method URI 인증 필요 여부
회원 가입 POST /api/v2/members 필요 없음
로그인 POST /api/v2/members/login 필요 없음
전체 회원 조회 GET /api/v2/members (필요)
회원 단건 조회 GET /api/v2/members/{id} (필요)
회원 정보 수정 PATCH /api/v2/members/{id} (필요)
회원 탈퇴 DELETE /api/v2/members/{id} (필요)
전체 일정 조회 GET /api/v2/schedules (필요)
사용자별 일정 조회 GET /api/v2/members/{memberId}/schedules (필요)
일정 단건 조회 GET /api/v2/schedules/{id} (필요)
일정 생성 POST /api/v2/members/{memberId}/schedules (필요)
일정 수정 PATCH /api/v2/schedules/{id} (필요)
일정 삭제 DELETE /api/v2/schedules/{id} (필요)

모든 컨트롤러에 인증 로직을 넣어야 한다면?

위의 표에서 볼 수 있듯이,
회원가입과 로그인을 제외한 거의 모든 API가 인증을 필요로 합니다.

즉, Controller의 모든 민감한 메서드에 다음과 같은 코드가 반복되어야 한다는 뜻입니다.

HttpSession session = httpRequest.getSession(false);

if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
    throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
}

이런 방식은 다음과 같은 문제를 갖습니다:

  • 중복 코드가 너무 많아진다
  • 컨트롤러의 코드 가독성이 떨어진다
  • 실수로 인증을 빼먹는 문제가 생길 수 있다

필터 사용

이런 문제를 해결하기 위해 저는 서블릿 필터를 도입했고,
공통 인증 로직을 한 곳에서 처리할 수 있도록 구성했습니다.

필터는 컨트롤러보다 먼저 실행되기 때문에,
로그인하지 않은 사용자는 아예 컨트롤러에 도달하지 못하게 막을 수 있습니다.

 

LoginFilter

@Slf4j
public class LoginFilter implements Filter {

    private static final String[] WHITE_LIST = {
            "/api/v2/members",
            "/api/v2/members/login"
    };

    @Override
    public void doFilter(
            ServletRequest request,
            ServletResponse response,
            FilterChain chain
    ) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String requestURI = httpRequest.getRequestURI();
        log.info("로그인 필터 실행 URI = {}", requestURI);

        if (!isWhiteList(requestURI)) {
            HttpSession session = httpRequest.getSession(false);

            if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
                throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
            }
        }

        chain.doFilter(request, response);
    }

    private boolean isWhiteList(String requestURI) {
        return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
    }
}

위에서 말한 서블릿 필터 개념을 제 프로젝트에 적용한 것이 바로 LoginFilter입니다.
이 필터는 화이트리스트 URL을 제외한 모든 요청에 대해 세션을 검사하고,
로그인하지 않은 사용자의 요청은 차단하는 역할을 수행합니다.

 

화이트리스트란?

LoginFilter를 보면 이런 코드가 있습니다:

private static final String[] WHITE_LIST = {
        "/api/v2/members",
        "/api/v2/members/login"
};

화이트리스트(White List)란 말 그대로 "허용된 목록"을 뜻합니다.
즉, 위의 URI 목록은 로그인하지 않아도 접근할 수 있도록 예외로 인정한 경로라는 의미예요.

 

왜 화이트리스트가 필요할까?

예를 들어,

  • 회원가입 API(/api/v2/members)
  • 로그인 API(/api/v2/members/login)

이런 기능은 로그인하지 않은 사용자도 접근할 수 있어야 하잖아요?
그런데 만약 모든 요청에 대해 필터가 “로그인 했는지 검사”를 해버리면,
아직 회원가입도 하지 않은 사용자는 어떤 API도 이용할 수 없게 돼요.
-> 순환참조

그래서 이런 공개 API들은 화이트리스트에 등록해서,
LoginFilter가 인증을 검사하지 않고 그냥 통과시키도록 설정하는 거예요.

 

화이트리스트 체크

 
private boolean isWhiteList(String requestURI) {
    return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
}

이 메서드는 현재 요청된 URI가 화이트리스트에 포함되어 있는지 확인하는 역할을 합니다.

  • WHITE_LIST는 로그인 없이 접근을 허용할 URI들의 목록입니다.
  • requestURI는 현재 사용자가 요청한 URI입니다.
  • PatternMatchUtils.simpleMatch()는 두 값을 비교하여 일치하면 true, 일치하지 않으면 false를 반환합니다.

즉, 이 메서드는 다음과 같은 의미를 가집니다:

요청 URI가 로그인 없이 접근해도 되는 URI(화이트리스트)에 포함되어 있는가?

 

그러면 이제 LoginFilter를 다시 확인해보면 화이트리스트에 없는 값들은 로그인 인증이 필요한 URL이니 if 문을 활용하여 로그인 인증을 해줍니다.


LoginFilter 등록하기

자바에서는 서블릿 필터를 등록하는 두 가지 방법이 있습니다:

  1. @Component를 활용한 자동 빈 등록 방식
  2. 자바 설정(자바 코드 기반)으로 직접 등록하는 방식 – 요즘 스프링 부트에서는 이 방식을 더 선호합니다.

@Component 를 활용한 자동 빈 등록 방식

1. 필터 클래스에 @Component 붙이기

@Component
@Order(1)
public class LoginFilter implements Filter {

    private static final String[] WHITE_LIST = {
            "/api/v2/members",
            "/api/v2/members/login"
    };

    @Override
    public void doFilter(
            ServletRequest request,
            ServletResponse response,
            FilterChain chain
    ) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String requestURI = httpRequest.getRequestURI();
        log.info("로그인 필터 실행 URI = {}", requestURI);

        if (!isWhiteList(requestURI)) {
            HttpSession session = httpRequest.getSession(false);

            if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
                throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
            }
        }

        chain.doFilter(request, response);
    }

    private boolean isWhiteList(String requestURI) {
        return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
    }
}
  • @Component 애노테이션을 사용하여 필터를 자동 등록해줍니다.
  • @Order(순서)를 활용하여 필터의 순서를 지정합니다.

 

자바 설정 방식

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean loginFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LoginFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");

        return filterRegistrationBean;
    }
}

 

  • @Configuration: 스프링 설정 클래스라는 의미입니다.
  • FilterRegistrationBean: 필터를 등록하기 위한 스프링 제공 클래스입니다.
  • setFilter(): 우리가 만든 LoginFilter를 등록합니다.
  • setOrder(): 필터가 여러 개일 경우 우선순위를 정합니다. 숫자가 낮을수록 먼저 실행됩니다.
  • addUrlPatterns(): 필터를 적용할 URL 패턴을 지정합니다.

마무리

이번 프로젝트에서는 반복되던 로그인 인증 로직을 필터로 통합함으로써 코드 중복을 줄이고, 컨트롤러의 가독성과 유지보수성을 크게 향상시킬 수 있었습니다.

 

핵심 요약

  • 인증이 필요한 요청을 필터에서 선처리하면 컨트롤러는 비즈니스 로직에 집중할 수 있다.
  • 화이트리스트는 로그인 없이 접근 가능한 API를 예외 처리하기 위한 중요한 장치다.
  • 자바 설정 방식은 필터 우선순위 설정과 스프링 빈 주입이 가능해 더 유연하다.