스프링 부트에서 인터셉터 사용하기

김태훈·2023년 9월 26일

spring boot

목록 보기
5/6

1. interceptor를 왜 사용해야 할까?

보통 유명한 포털 사이트를 들어가서 특정 링크를 눌러 상세 페이지로 들어가려 하면 로그인을 해야 이용가능한 서비스라는 알림창이 뜨는 경우를 많이 접한다. 이때 '예'를 누르면 로그인 페이지로 자동으로 이동을 하게 된다. 자동으로 이동한 로그인 페이지에서 로그인을 완료한 경우 사용자가 처음에 요청한 페이지로 바로 이동을 시켜주는 방식으로 사용자 편의성을 높인 것이다. 이러한 기능을 통틀어서 구현 해줄 수 있는 클래스가 interceptor이다. 이번 포스팅에서는 간단한 회원관리 프로젝트에서 interceptor를 활용한 사례를 살펴보자.

2. html 페이지 와 컨트롤러의 상호작용

nav.html에 로그인 페이지로 이동 가능한 링크를 다음과 같이 만들어 놓았다. a href로 주소 지정을 했으므로 지정한 컨트롤러 주소로 Get request를 한다. 따라서 컨트롤러에서는 GetMapping으로 해당 요청을 받아준다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div th:fragment="nav" id="nav">
    <ul class="menu">
        <li class="menu-item">
            <a href="/">index</a>
        </li>
        <li class="menu-item">
            <a href="/member/save">회원가입</a>
        </li>
        <li class="menu-item">
            <a href="/members">회원목록</a>
        </li>
        <li class="menu-item" id="login-area">
            <!--세션값의 유무에따라 로그인, 로그아웃 링크가 달라진다. -->
        </li>
    </ul>
    <script th:inline="javascript">
        const loginArea = document.getElementById("login-area");
      
        // 다음의 구문에서 '[[]]' 처럼 대괄호를 두번 감싸서 타임리프의
        // 표현식을 자바스크립트로 인식하도록 한다.
      
        const loginEmail = [[${session.loginEmail}]];
        console.log(loginEmail);
        if (loginEmail != null) {
            // 로그인 했음
            loginArea.innerHTML = "<a href='/mypage'>" + loginEmail + "님 환영해요!</a>" +
                "<a href='/member/logout'>logout</a>";
        } else {
            // 로그인 안했음
            loginArea.innerHTML = "<a href='/member/login'>로그인</a>";
        }
    </script>
</div>

</body>
</html>

다음은 컨트롤러의 GetMapping부분이다. 분명 @RequestParam으로 프론트엔드에서 받아줄 파라미터가 없다고 생각이 들 수 있는데 defaultValue로 특정 컨트롤러 주소를 지정을 했으므로 다음 모듈의 의미는 다음과 같다.

  1. redirectURI 파라미터를 요청에서 찾아낸다.
  2. 요청에서 redirectURI 파라미터가 없는 경우, defaultValue로 지정된 기본 값인 "/member/mypage"을 사용한다.
  3. 따라서 요청에 redirectURI 파라미터가 없다면 "/member/mypage" 값이 redirectURI 변수에 할당된다.
  4. model에는 redirectURI를 담아서 로그인페이지로 간다. 이는 나중에 로그인 정보를 제출시에도 redirectURI 값을 전달하기 위함이다.
  5. Get 요청이 성공적으로 수행이 되면 /memberPages/memberLogin 에 해당하는 경로의 html파일을 랜더링해준다.
 @GetMapping("/member/login")
    public String login(@RequestParam(value = "redirectURI", defaultValue = "/member/mypage") String redirectURI,
                        Model model) {
        model.addAttribute("redirectURI", redirectURI);
        return "/memberPages/memberLogin";
    }

다음은 위 코드에서 defaultvalue로 지정한 "/member/mypage" 컨트롤러 경로의 memberLogin.html파일을 리턴하는 컨트롤러의 코드이다.

@GetMapping("/member/mypage")
    public String mypage() {
        return "/memberPages/memberLogin";

이제 로그인 html파일을 잘 랜더링을 했으므로 로그인 html파일의 코드를 보자. 리다이렉트할 uri값은 굳이 사용자에게 보여줄 필요가 없고 로그인 버튼을 눌러서 post요청을 할때 서버에 보낼 값으로만 실질적으로 사용을 할 것이므로 type을 hidden으로 해준다.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <th:block th:replace="~{component/config :: config}"></th:block>
</head>
<body>

<div th:replace="~{component/header :: header}"></div>
<div th:replace="~{component/nav :: nav}"></div>

<h2>로그인 페이지</h2>
<form action="/member/login" method="post">
    <input type="hidden" name="redirectURI" th:value="${redirectURI}">
    <input type="text" name="memberEmail" placeholder="이메일"> <br>
    <input type="text" name="memberPassword" placeholder="비밀번호"> <br>
    <input type="submit" value="로그인">
</form>

<div th:replace="~{component/footer :: footer}"></div>

</body>
</html>

다음은 post요청을 받아줄 컨트롤러의 코드를 살펴보자.

@PostMapping("/member/login")
    public String login(@ModelAttribute MemberDTO memberDTO, HttpSession session,
                        @RequestParam("redirectURI") String redirectURI) {
        boolean loginResult = memberService.login(memberDTO);
        if(loginResult) {
            session.setAttribute("loginEmail", memberDTO.getMemberEmail());
            //사용자가 로그인 성공시 직전에 요청한 페이지로 이동시킴
            //별도로 요청한 페이지가 없다면 정상적으로 mypage로 이동시킴(redirect:/member/mypage)
            return "redirect:" + redirectURI;
        }else {
            return "/memberPages/memberLogin";
        }
    }

지금까지 컨트롤러와 프론트 역할을 하는 html파일들의 데이터 상호작용을 알아봤으니 본격적으로 로그인 상태에 따라 지정한 주소 요청을 가로채 특정 동작(이번 포스팅에서는 특정 페이지로 이동시키는 동작)을 하는 interceptor의 구현을 하는 방법을 보자.

3. WebConfig 클래스

package com.example.member.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration //해당 클래스에 정의한 정보를 스프링 컨테이너에 등록
public class WebConfig implements WebMvcConfigurer {
    @Override//인터셉터는 기능 구현 후에 나중에 만들기 그게 더 기능이 꼬이지 않게 함
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor()) // 인터셉터로 등록할 클래스
                .order(1) // 해당 인터셉터의 우선순위
                .addPathPatterns("/**") // 인터셉터로 체크할 주소(모든주소)
                .excludePathPatterns("/", "/member/save", "/member/login", "/member/login/axios",
                        "/js/**", "/css/**", "/images/**",
                        "/*.ico", "/favicon/**"); // 인터셉터 검증을 하지 않을 주소
    }

}

위의 코드는 Spring Framework에서 사용되는 인터셉터를 설정하는 WebConfig 클래스이다. 이 클래스는 WebMvcConfigurer 인터페이스를 구현하고 있어서 Spring MVC 애플리케이션에서 인터셉터를 추가하고 설정하는 역할을 한다. 이 코드를 상세히 살펴보자.

  1. @Configuration 어노테이션:

    이 클래스가 Spring의 설정 클래스임을 나타낸다. 스프링 컨테이너에 의해 관리되며, 빈(Bean) 구성을 정의하는 역할을 한다.

  2. WebMvcConfigurer 인터페이스 구현:

WebMvcConfigurer 인터페이스를 구현하여 Spring MVC 구성을 사용자 정의한다. 이를 통해 웹 애플리케이션의 설정을 커스터마이징할 수 있다.

  1. addInterceptors 메서드 오버라이드:

addInterceptors 메서드는 Spring MVC의 인터셉터를 추가하고 설정하는 데 사용된다. 이 메서드를 오버라이드하여 사용자 지정 인터셉터를 등록하고 설정한다.

  1. registry.addInterceptor(new LoginCheckInterceptor()):

LoginCheckInterceptor라는 사용자 정의 인터셉터를 등록한다. 이 인터셉터는 로그인 상태를 체크하고, 필요한 경우 요청을 가로채서 추가 작업을 수행할 수 있다.

  1. .order(1):

.order 메서드를 사용하여 인터셉터의 우선순위를 지정한다. 여러 개의 인터셉터가 등록되어 있을 때, 우선순위에 따라 실행 순서가 결정된다. 이 예시에서는 순서 1로 지정되어 있으므로, 다른 인터셉터보다 먼저 실행된다.

  1. .addPathPatterns("/**"):

.addPathPatterns 메서드를 사용하여 인터셉터가 적용될 URL 패턴을 지정한다. 이 경우에는 모든 URL에 대해 인터셉터를 활성화한다.

  1. .excludePathPatterns(...):

.excludePathPatterns 메서드를 사용하여 인터셉터가 적용되지 않을 URL 패턴을 지정한다. 즉, 인터셉터가 검증하지 않아야 할 주소를 나열한다. 이 경우에는 루트 경로(/), 회원 가입 및 로그인 관련 URL, 정적 자원(css, js, 이미지 등), 파비콘(favicon) 등을 제외하고 나머지 주소에 대해 인터셉터가 적용된다.
이렇게 설정된 인터셉터는 로그인 상태를 체크하고, 특정 URL에 대한 요청에 대해 추가 작업을 수행할 수 있다. 이제 위의 클래스에서 매개변수로 가져온 LoginCheckInterceptor에 대해 살펴보자.

4. LoginCheckInterceptor 클래스

package com.example.member.config;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

// 로그인 여부를 확인하고 로그인 상태라면 사용자가 요청한 주소로 보내고
// 로그인하지 않은 상태라면 컨트롤러의 로그인 요청 주소로 넘김
public class LoginCheckInterceptor implements HandlerInterceptor {
    //prehandle이 결국 재정의를 하는 것이어서 세션을 매개변수로 받으면 안되고 동작 부분에서 따로 세션을 가져온다.
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws IOException {
        // 사용자가 요청한 주소 확인
        String requestURI = request.getRequestURI();
        System.out.println("requestURI = " + requestURI);
        // 세션객체 생성
        HttpSession session = request.getSession();
        // 세션에 저장된 로그인 정보 확인
        if (session.getAttribute("loginEmail") == null) {
            // 로그인하지 않았다면 로그인페이지로 사용자를 보내면서
            // 처음에 사용자가 요청한 주소값도 같이 보냄
            response.sendRedirect("/member/login?redirectURI=" + requestURI);
            return false;
        } else {
            // 로그인 상태라면 요청한 페이지로 보냄(거르지 않음)
            return true;
        }
    }
}
  1. public class LoginCheckInterceptor implements HandlerInterceptor:

HandlerInterceptor 인터페이스를 구현한 사용자 정의 인터셉터 클래스이다.

  1. preHandle 메서드:

이 메서드는 컨트롤러의 요청 처리 전에 실행되는 메서드이다. 즉, 클라이언트가 특정 URL로 요청을 보내면 먼저 이 메서드가 호출된다.

  1. HttpServletRequest request, HttpServletResponse response:

HTTP 요청과 응답에 대한 정보를 다루기 위한 매개변수이다.

  1. Object handler:

요청을 처리할 핸들러(Controller 또는 메소드)를 나타내는 객체이다. 이 인터셉터는 모든 핸들러에 적용될 수 있으므로 핸들러에 대한 정보를 받는 것이다.

  1. String requestURI = request.getRequestURI();:

클라이언트의 요청 URI를 가져온다. 이것은 클라이언트가 요청한 URL의 경로이다.

  1. HttpSession session = request.getSession();:

클라이언트의 세션 객체를 생성하거나 가져온다. 세션은 클라이언트와 서버 간의 상태를 유지하는 데 사용된다.

  1. if (session.getAttribute("loginEmail") == null) { ... }:

세션에서 "loginEmail" 속성을 가져와서 이 값이 null인지 확인한다. "loginEmail"은 로그인한 사용자의 이메일을 세션에 저장한 것을 가정한 것이다.

  1. response.sendRedirect("/member/login?redirectURI=" + requestURI);:

사용자가 로그인하지 않은 경우, 로그인 페이지로 리다이렉트한다. 동시에 redirectURI 매개변수를 사용하여 원래 요청한 페이지의 URI를 로그인 페이지로 넘겨준다. 이렇게 하면 사용자가 로그인한 후에 다시 원래 요청한 페이지로 돌아갈 수 있다.

  1. return false;:

preHandle 메서드가 false를 반환하면 요청을 중단하고 해당 요청을 더 이상 처리하지 않는다. 따라서 로그인하지 않은 사용자의 경우 리다이렉트 후 요청 처리가 종료된다.

  1. return true;:

preHandle 메서드가 true를 반환하면 요청을 계속하여 핸들러(Controller 또는 메소드)로 진행된다. 즉, 로그인한 사용자의 경우 요청한 페이지로 이동한다.

지금까지 인터셉터를 이용해서 사용자의 특정 주소 요청에 따라 원하는 페이지로 먼저 이동시켜서 조건을 만족하면 사용자가 요청했던 주소로 다시 리디렉션해주는 기능에 대해서 알아보았다.

다음은 인터셉트를 사용한 간단한 회원 관리 미니 프로젝트의 깃허브 주소이다.

회원관리 프로젝트

0개의 댓글