본문 바로가기
내가 공부하려고 올리는/스프링

스프링 인터셉터(Interceptor)

by 결딴력 2022. 7. 13.
반응형

스프링 프로젝트를 진행하면서 '회원 인증' 로직을 구현해야 했습니다.

'회원 인증' 로직을 구현하는 것은 어렵지 않았으나

회원 인증이 필요한 모든 페이지에 회원 인증 로직을 작성하는 것은

비효율적이고 중복이 발생하는 부분이었습니다.

 

이 부분을 해결하기 위해 저는 스프링 인터셉터를 활용했습니다.

스프링 인터셉터 말고도 필터(Filter)나 스프링의 AOP로도 해당 문제를 해결할 수 있습니다.

 

저는 인터셉터가 스프링 MVC가 제공하는 기술이고

이런 공통 관심 사항을 효과적으로 해결할 수 있기 때문에 선택했습니다.

 

 

 

✅ 스프링 인터셉터 흐름


스프링 인터셉터 흐름은 다음과 같습니다.

 

 

HTTP 요청
👇
WAS
👇
디스패처 서블릿
👇
스프링 인터셉터
👇
컨트롤러

 

요청에 대해 스프링 인터셉터가 실행되는 시점은 컨트롤러 호출 직전입니다.

따라서 스프링 인터셉터가 동작하고 있고 적절하지 않은 요청이 오면 컨트롤러는 호출되지 않습니다.

 

이런 스프링 인터셉터는 체인(Chain)으로 구성됩니다.

인터셉터의 수가 여러 개일 수 있다는 것으로,

하나의 인터셉터가 실행된 후에 두 번째 인터셉터가 실행되도록 할 수 있습니다.

 

 

 

스프링 인터셉터 구현하기


스프링 인터셉터를 사용하려면 자바 클래스를 만들고 'HandlerInterceptor' 상속하면 됩니다.

 

HandlerInterceptor 인터페이스의 모습입니다.

public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return true;
	}

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}

	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}

}

 

Interceptor 기능을 이용하려면 'preHandle, postHandle, afterCompletion' 메서드를 구현하면 됩니다.

물론 'default'로 구현되어 있기 때문에 필요한 메서드만 선택적으로 구현할 수 있습니다.

 

각 메서드를 간단히 설명해보겠습니다.

 

1. preHandle

  • 컨트롤러 호출 전에 동작하는 메서드입니다.
  • 리턴 타입이 'boolean'으로 preHandle의 호출 결과가 false이면 컨트롤러가 호출되지 않습니다.

 

2. postHandle

  • 컨트롤러가 호출된 이후에 동작하는 메서드입니다.
  • 컨트롤러 호출 이후 동작하기 때문에 ModelAndView를 파라미터로 받을 수 있습니다.

 

3. afterCompletion

  • 뷰가 렌더링 되고 동작하는 메서드입니다.
  • afterCompletion은 항상 호출된다.

 

 

afterCompletionException을 파라미터로 받고 있는데

이는 인터셉터에서 예외가 발생할 때 예외를 받아서 처리하기 위함입니다.

 

스프링 인터셉터에서 예외가 발생하는 경우

postHandle 메서드는 동작하지 않는 반면,

afterCompletion은 항상 호출되는 메서드여서 호출됩니다.

 

따라서 스프링 인터셉터에서 발생한 예외는 afterCompletion 메서드를 이용해 처리합니다.

 

 

 

인터셉터 구현


실제 인터셉터를 구현해보겠습니다.

 

우선 LoginCheckInterceptor를 구현하겠습니다.

제 프로젝트의 LoginCheckInterceptor 전체 코드입니다.

 

@Slf4j
@RequiredArgsConstructor
public class LoginCheckInterceptor implements HandlerInterceptor {

    private final AlarmService alarmService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String requestURI = request.getRequestURI();

        log.info("인증 체크 인터셉터 실행 {}", requestURI);

        HttpSession session = request.getSession(false);
        log.info("session 값 = " +session);

        if (session == null ||
                (session.getAttribute(SessionConst.LOGIN_MEMBER) == null &&
                 session.getAttribute(SessionConst.NAVER_MEMBER) == null &&
                 session.getAttribute(SessionConst.KAKAO_MEMBER) == null)) {
            log.info("미인증 사용자 요청");
            //로그인으로 redirect
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false;
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HttpSession session = request.getSession(false);

        Integer id = null;

        if(session!=null) {

            MemberDto naverDto = (MemberDto) session.getAttribute(SessionConst.NAVER_MEMBER);
            MemberDto kakaoDto = (MemberDto) session.getAttribute(SessionConst.KAKAO_MEMBER);
            MemberDto memberDto = (MemberDto) session.getAttribute(SessionConst.LOGIN_MEMBER);

            if (naverDto != null) {
                id = setSession(session, naverDto);
                getAlarm(session, id);
            } else if (kakaoDto != null) {
                id = setSession(session, kakaoDto);
                getAlarm(session, id);
            } else if (memberDto != null) {
                id = setSession(session, memberDto);
                getAlarm(session, id);
            }
        }
    }

    /**
     * 세션에 정보 저장
     * @param session
     * @param memberDto
     * @return id
     */
    private Integer setSession(HttpSession session, MemberDto memberDto) {
        String nickName;
        Integer id;
        id = memberDto.getId();
        nickName = memberDto.getNickname();
        session.setAttribute("id", id);
        session.setAttribute("nickName", nickName);
        
        return id;
    }

    /**
     * 알람 가져오기
     * @param session
     * @param id
     * @throws Exception
     */
    private void getAlarm(HttpSession session, Integer id) throws Exception {
        session.removeAttribute("alarmList");
        List<AlarmDto> alarm = alarmService.getAlarm(id);
        if(alarm.isEmpty()) {
            session.setAttribute("alarmList", null);
        } else {
            session.setAttribute("alarmList", alarm);
        }
    }
}

 

우선 preHandle를 구현합니다.

요청 URI 정보를 'request' 객체를 이용해 저장합니다.

 

또한 request 객체를 이용해 session을 가져옵니다.

해당 프로젝트에서는 '일반/네이버/카카오' 회원을 분류하여 세션을 다르게 저장하고 있습니다.

따라서 각각의 이름으로 저장된 세션 정보가 있는지 찾습니다.

 

만약 해당 세션이 아무것도 존재하지 않는다면 '로그인' 페이지로 이동시킵니다.

이때 로그인을 하고 나서 요청을 했던 페이지로 리다이렉트(Redirect) 하기 위해

다음과 같이 코드를 작성합니다.

 

response.sendRedirect("/login?redirectURL=" + requestURI);

 

다음은 postHandle입니다.

postHandle 메서드를 이용해 로그인 정보와 알림 메시지를 가져옵니다.

로그인 때 저장한 세션 상수를 이용해 회원 정보를 세션에 저장합니다.

 

다음으로는 'JoinCheckInterceptor'를 구현해보겠습니다.

JoinCheckInterceptor는 이미 로그인한 회원이 회원가입 페이지에 접근할 경우 동작하는 인터셉터입니다.

 

@Slf4j
public class JoinCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        HttpSession session = request.getSession(false);

        if (session != null &&
                (session.getAttribute(SessionConst.LOGIN_MEMBER) != null ||
                 session.getAttribute(SessionConst.NAVER_MEMBER) != null ||
                 session.getAttribute(SessionConst.KAKAO_MEMBER) != null)) {
            log.info("로그인 회원의 잘못된 접근");
            //로그아웃으로 redirect
            response.sendRedirect("/");
            return false;
        }

        return true;
    }
}

 

request 객체를 이용해 session 정보를 받아옵니다.

세션에 저장된 회원 정보가 존재하는 경우

홈 화면으로 리다이렉트 합니다.

 

 

 

인터셉터 등록


이제 인터셉터가 동작할 수 있도록 설정 클래스를 작성해보겠습니다.

인터셉터의 경우 WebMvcConfigurer를 상속받아야 합니다.

 

저는 InterceptorConfig 클래스를 만들어 다음과 같이 구현했습니다.

 

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    AlarmService alarmService;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor(alarmService))
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/css/**", "/img/**", "/js/**", "/join/**", ... 중략);

        registry.addInterceptor(new JoinCheckInterceptor())
                .order(2)
                .addPathPatterns("/join");
    }
}

 

현재 구현된 인터셉터는 두 개이므로 '.order()'를 이용해 인터셉터가 동작할 순서를 정해줍니다.

 

'.addPathPatterns()'는 인터셉터가 동작할 URI를 지정하는 역할을 합니다.

LoginCheckInterceptor의 경우 기본적으로 모든 페이지에서 동작하도록 '/**'을 설정했습니다.

JoinCheckInterceptor의 경우 '/join'으로 오는 요청에만 인터셉터가 실행되면 되기 때문에 '/join'만 등록했습니다.

 

'.excludePathPatterns()'는 인터셉터가 예외적으로 동작하지 않도록 URI를 설정하는 역할을 합니다.

CSS 파일이나 JS파일 혹은 홈 화면같이 로그인을 하지 않아도 접근이 가능한 경로에도

로그인 인터셉터가 동작하면 프로그램이 정상적으로 동작하지 않기 때문에

로그인 체크를 하지 않아도 되는 경로를 excludePathPatterns에 등록합니다.

해당 부분은 본인의 프로그램에 맞춰 구현하면 됩니다.

 

 

실제 프로젝트


제 프로젝트에서 '/profile'로 오는 요청은 로그인이 필요한 상황입니다.

해당 URI를 로그인을 하지 않은 채로 요구하면 로그인 페이지로 리다이렉트 되며

URI가 다음과 같이 변경됩니다.

 

http://localhost:8080/login?redirectURL=/profile/10

 

이 상태에서 로그인을 하면 로그인을 하기 전에 요청했던 profile 페이지로 이동하게 됩니다.

인터셉터가 실제 동작하는 모습은 아래 영상으로 대체하겠습니다.

 

인터셉터가 동작하는 모습

 

반응형

댓글