관리 메뉴

거니의 velog

231208_SPRING 2 (13-2) 본문

대덕인재개발원/대덕인재개발원_웹기반 애플리케이션

231208_SPRING 2 (13-2)

Unlimited00 2023. 12. 8. 08:08

* 인터셉트(intercept)

- 클라이언트에서 서버로 요청을 보낼 때, DispatcherServlet이 맨 먼저 받는다. 각각의 핸들러, 리퀘스트 매핑 정보 등을 조합하여 응답에 대한 뷰를 만들어서 디스패처가 내보낸다.

- 여기서, 필터라고 하는 녀석을 이용해서 인코딩 설정, 로그 기록, 세션 관리 등을 진행했다.

- 문제 : 클라이언트와 서버 라고 하는 전반적인 영역 안에서 필터의 위치가 어딜까?

- 필터가 스프링 자원을 가용하기 위한 옵션 설정이 매우 어렵다. 그래서 AOP라는 녀석을 활용했다.

- 서비스 타겟 실행 이전에 AOP가 실행. 중간에 프록시가 동작. 위치는 어디? 서버 안에 타겟이 들어 있다. 그래서 스프링 자원을 충분히 가용 가능.

- 단, AOP는 대규모 시스템에서는 사용이 가능하나, 소규모 시스템은 부적절할 수 있다.

- 세션, 로깅 처리를 좀 더 보편적으로 만들어내기 적합한 것은 인터셉터. 디스패처 서블릿과 컨트롤러 사이에 있는 것.

- web.xml 에 이 인터셉터들을 설정해 주면, 특정 URL에 해당하는 규칙들이 정해짐. 이게 동작하는 규칙이 설정되면 인터셉터가 가동됨.

- 총 3 가지 동작 시점이 있다.


package kr.or.ddit.controller.intercept;

public class InterceptorController {

	/*
	 * [ 17장: 인터셉터 ]
	 * 
	 * 		- 인터셉터는 웹 애플리케이션 내에서 특정한 URI 호출을 가로채는 역할을 한다.
	 * 
	 * 		1. 인터셉터 설명
	 * 
	 * 			필터와 인터셉터
	 * 			- 서블릿 기술의 필터와 스프링 MVC의 인터셉터는 특정 URI에 접근할 때 제어하는 용도로 사용된다는 공통점이 있다.
	 * 				하지만 실행 시점에 속하는 영역(Context)에 차이점이 있다.
	 * 				인터셉터의 경우 스프링에서 관리하기 때문에 스프링 내의 모든 객체에 접근이 가능하지만 필터는 웹 애플리케이션 영역 내의
	 * 				자원들을 활용할 수 있지만 스프링 내의 객체에는 접근이 불가능하다.
	 * 
	 * 			스프링 AOP와 인터셉터
	 * 			- 특정 객체 동작의 사전 혹은 사후 처리는 AOP 기능을 활용할 수 있지만, 컨트롤러의 처리는 인터셉터를 활용하는 경우가 많다.
	 * 				AOP의 어드바이스와 인터셉터의 가장 큰 차이는 파라미터의 차이라고 할 수 있다.
	 * 				어드바이스의 경우 JoinPoint나 ProceedingJoinPoint 등을 활용해서 호출 대상이 되는 메소드의 파라미터 등을
	 * 				처리하는 방식이다. 인터셉터는 필터와 유사하게 HttpServletRequest, HttpServletResponse를 파라미터로
	 * 				받는 구조이다.
	 * 
	 * 			HandlerInterceptorAdaptor 클래스
	 * 			- HandlerInterceptorAdaptor 는 HandlerInterceptor를 쉽게 사용하기 위해서 인터페이스의 메소드를 미리 구현한 클래스이다.
	 * 
	 * 			HandlerInterceptor의 메소드는 아래와 같다.
	 * 			- preHandle
	 * 				> 지정된 컨트롤러의 동적 이전에 가로채는 역할을 한다.
	 * 			- postHandle
	 * 				> 지정된 컨트롤러의 동작 이후에 처리, DispatcherServlet이 화면을 처리하기 전에 동작한다.
	 * 			- afterCompletion
	 * 				> DispatcherServlet의 화면 처리가 완료된 상태에서 처리한다.
	 * 
	 * 		2. 인터셉터 구현
	 * 
	 * 			클라이언트의 요청을 처리하다 보면 요청 경로마다 접근 제어를 다르게 하거나, 특정 URL에 대한 접근 내역을 기록하고 싶을 떄가
	 * 			있다. 이런 기능은 특정 컨트롤러에 종속되기 보다는 여러 컨트롤러에서 공통적으로 적용되는 기능들이라 하겠다.
	 * 			이런 기능을 각 컨트롤러에서 개별적으로 구현하면 중복 코드가 발생하므로, 코드 중복 없이 기존의 컨트롤러에 수정을 가하지 않고
	 * 			적용할 수 있는 방법이 필요하다. 지금까지 배운 내용들을 떠올려보면 이를 해결할 수 있는 방식 2가지가 존재하는데...!
	 * 			(작성하면서 어떤 방법이 있는지 머릿 속에 떠오르길 바란다!)
	 * 
	 * 			그 첫 번째로 Filter라는 서블릿 스펙에 따른 객체를 사용하는 방법이 있다. 필터를 통해 DispatcherServlet이나
	 * 			컨트롤러에게 요청이 위임되기 전/후에 공통적인 어떤 기능을 수행하도록 하면, 기존 컨트롤러나 DispatcherServlet에
	 * 			어떤 수정 사항을 가하거나 코드 중복 없이 이슈를 해결할 수 있을 것이다. 그러나 이 방법은 한 가지 단점이 있다.
	 * 			바로 Filter가 DispatcherServlet 보다 먼저 객체가 생성되고 스프링 컨테이너 밖에 존재하기 때문에 컨테이너를 통한
	 * 			DI를 받을 수 없다는 점이다. 물론 아예 불가능 하지는 않다. 설정이나 이런 부분이 복잡하고 귀찮을 뿐이다...
	 * 			DelegatingFilterProxy 필터를 사용하면 필터링 작업을 스프링 컨테이너에 존재하는 빈에게 위임할 수도 있다.
	 * 			해당 DelegatingFilterProxy라는 필터는 원래 필터 기반의 보안 처리를 지원하는 Spring Security 프레임워크에서
	 * 			제공되었던 필터인데 하도 널리 쓰이기 시작하면서 아예 스프링 코어 웹 모듈로 편입된 타입이다. 그런데 이 필터를 이용해서
	 * 			스프링 빈에게 필터링을 위임하는 방법도 몇 가지 불편한 점들이 있다.
	 * 			반드시 위임을 받을 빈을 Filter를 구현하고 있어야 하고, root-context.xml에서 관리되는 빈이어야만 한다는 것이다.
	 * 
	 * 			두 번째로  AOP방법론에 따른 공통 기능을 정의한 Advice를 구현하고, pointcut을 통해 적절한 target 컨트롤러를 골라낸 다음
	 * 			두 설정으로 Aspect를 생성해야 런타임에 컨트롤러와 위빙하도록 하는 방법을 생각해 볼 수 있다. 실제 보안 프레임워크들에서도
	 * 			AOP 방법론에 따라 위빙을 위한 어노테이션을 활용하고 있기는 하다.
	 * 
	 * 			그렇지만, 첫번째/두번째 모두 우리가 처리하고 싶은 기능을 구현하는 데 제약이 따른다.
	 * 			우리가 지금 처리하고 싶은 기능은 특정 URL에 대한 접근 내역을 기록하거나 특정 경로에 대한 접근 제어를 하는 등 웹이라는
	 * 			공간과 환경에 제한된 공통 기능을 처리하고 싶은 것이다. AOP는 너무 범용적인 방법이라 할 수 있고, Filter 방식은
	 * 			제약이 많다. (사용할 자원의 환경이 다르기 때문이다)
	 * 
	 * 			이러한 경우에 사용하기 위한 전략으로 스프링은 HandlerInterceptor라는 추상화를 제공하고 있으며, 이를 사용하면
	 * 			Spring MVC에 맞게 공통 기능을 다수의 URL에 적용할 수 있게 된다.
	 * 			HandlerInterceptor 인터페이스를 사용하면 아래와 같은 시점에 대해 공통 기능을 넣을 수 있다.
	 * 				> 컨트롤러 실행 전(preHandle)
	 * 				> 컨트롤러 실행 후, 아직 뷰를 실행하기 전 단계이다. (postHandle)
	 * 				> 뷰를 실행한 이후(afterCompletion)
	 * 
	 * 			preHandle() 메소드는 컨트롤러 객체를 실행하기 전에 필요한 기능을 구현할 때 사용되며, handler 파라미터는 웹 요청을
	 * 			처리할 컨트롤러 객체이다. 이 메소드를 사용하면 컨트롤러를 실행하기 전에 컨트롤러에서 필요로 하는 정보를 생성하거나
	 * 			접근 권한이 없는 경우, 리턴값을 false를 반환하여 컨트롤러가 실행되지 않도록 하는 작업이 가능하다.
	 * 			postHandle() 메소드는 컨트롤러가 정상적으로 실행된 이후에 추가 기능을 구현할 때 사용되는데, 만약 컨트롤러에서
	 * 			예외가 발생했다면 postHandle() 메소드는 실행되지 않는다.
	 * 			afterCompletion() 메소드는 클라이언트의 뷰를 전송한 뒤에 실행되며, 만약 컨트롤러를 실행하는 관점에서 예외가
	 * 			발생했다면, 이 메소드의 네 번째 파라미터로 전달된다.
	 * 			예외가 발생하는 경우는 null 값이 들어올 것이다. 따라서 컨트롤러 실행 이후 예기치 않은 예외에 대해 로깅을 한다거나
	 * 			실행 시간을 기록하는 등의 후처리를 하기에 적합한 메소드이다.
	 * 
	 * 			정리하자면 HandlerInterceptor는 세 가지 메소드를 통해 AOP 방법론 시점에 따른 여러 Advice들의 역할을 
	 * 			하나의 HandlerInterceptor 객체가 전담할 수 있는 구조를 가지고 있다!!!
	 * 
	 * 		3. 인터셉터 설정
	 * 
	 * 			- 인터셉터 클래스를 정의하고 스프링 웹 설정 파일에 인터셉터를 지정한다.
	 * 
	 * 				인터셉터 지정
	 * 				- servlet-context.xml에서 설정
	 * 					> loginInterceptor 아이디로 빈 등록
	 * 					> interceptor 태그 설정
	 */
	
}

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
		<beans:property name="order" value="2" />
	</beans:bean>
	
	<!-- Tiles 설정을 위한 Bean 등록 시작 -->
	<beans:bean id="tilesViewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
		<beans:property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView" />
		<beans:property name="order" value="1" />
	</beans:bean>
	
	<beans:bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
		<beans:property name="definitions">
			<beans:list>
				<beans:value>/WEB-INF/spring/tiles-config.xml</beans:value>
			</beans:list>
		</beans:property>
	</beans:bean>
	<!-- Tiles 설정을 위한 Bean 등록 끝 -->
	
	<context:component-scan base-package="kr.or.ddit.controller" />
	
	<!--  
		서블릿 표준용 MultipartResolver 를 스프링 빈으로 정의
		- StandardServletMultipartResolver 사용 시 설정
			> Servlet 3.0의 Part를 이용한 MultipartFile 데이터 처리
	-->
<!-- 	<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
		
	</beans:bean> -->
	
	<!-- 
		인터셉터 설정 
		- loginInterceptor 클래스를 빈으로 정의한다.
			> 설정한 클래스는 해당 위치에 존재해야 함(설정에 맞는 위치에 있어야 한다)	
	-->
	<beans:bean id="loginInterceptor" class="kr.or.ddit.controller.intercept.LoginInterceptor" />
	<interceptors>
		<interceptor>
			<mapping path="/login1" />
			<beans:ref bean="loginInterceptor" />
		</interceptor>
	</interceptors>
	
</beans:beans>

package kr.or.ddit.controller.intercept;

import java.lang.reflect.Method;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.ui.ModelMap;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class LoginInterceptor extends HandlerInterceptorAdapter {

	/*
	 * LoginInterceptor 상황 시나리오
	 * 
	 * 		'http://localhost/login1' 을 요청해 로그인 페이지를 요청한다.
	 * 		이 때, 해당 컨트롤러 메소드가 실행되기 전에 LoginInterceptor의 preHandle이 동작하고, 'userInfo' 세션명을
	 * 		가진 세션 정보를 조회한다. 조회된 정보가 존재한다면, 세션을 삭제처리한다.
	 * 		그리고 타겟을 거쳐 다시 인터셉터로 넘어올 때 데이터 전달자가 전달해준 회원 정보가 존재한다면 'userInfo' 키로
	 * 		회원 정보를 세션에 등록하고 '/'로 리다이렉트한다. 그렇지 않은 경우에는 해당 타겟을 정상 실행 후 리턴하는 결과 페이지로 이동한다.
	 */
	
	private static final String USER_INFO = "userInfo";
	
	// 지정된 컨트롤러의 동작 이전에 가로채는 역할로 사용한다.
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		log.info("preHandle() 실행...!");
		
		String requestURL = request.getRequestURL().toString(); // http://localhost/login1
		String requestURI = request.getRequestURI().toString(); // login1
		
		log.info("requestURL : " + requestURL);
		log.info("requestURI : " + requestURI);
		
		HandlerMethod method = (HandlerMethod) handler;
		Method methodObj = method.getMethod();
		
		// kr.or.ddir.controller.login.LoginController@312r2dta
		log.info("Bean : " + method.getBean());
		// public java.lang.String.kr.or.ddir.controller.login.LoginController.loginForm()
		log.info("method : " + methodObj);
		
		HttpSession session = request.getSession();
		if(session.getAttribute(USER_INFO) != null) {
			session.removeAttribute(USER_INFO);
		}
		
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		log.info("postHandle() 실행...!");
		
		String requestURL = request.getRequestURL().toString(); // http://localhost/login1
		String requestURI = request.getRequestURI().toString(); // login1
		
		log.info("requestURL : " + requestURL);
		log.info("requestURI : " + requestURI);
		
		HandlerMethod method = (HandlerMethod) handler;
		Method methodObj = method.getMethod();
		
		// kr.or.ddir.controller.login.LoginController@312r2dta
		log.info("Bean : " + method.getBean());
		// public java.lang.String.kr.or.ddir.controller.login.LoginController.loginForm()
		log.info("method : " + methodObj);
		
		HttpSession session = request.getSession();
		
		// 컨트롤러 메소드를 거쳤다가 postHandle 로 넘어오면서 전달된 user라는 키에 value로 member가 담긴 값이
		// Model에 담겨져 있다. 그 중에 'user'로 넘긴 값이 로그인 후 인증된 회원 1명의 정보가 담긴 MemberVO 자바빈즈 객체가 되고
		// 객체가 null이 아닌 경우 메인 화면을 리다이렉트 처리한다.
		ModelMap modelMap = modelAndView.getModelMap();
		Object member = modelMap.get("user");
		if(member != null) {
			log.info("member : " + member);
			log.info("member != null");
			session.setAttribute(USER_INFO, member);
			response.sendRedirect("/");
		}
	}

	// DispatcherServlet의 화면 처리가 완료된 상태에서 처리한다.
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		log.info("afterCompletion() 실행...!");
		
		String requestURL = request.getRequestURL().toString(); // http://localhost/login1
		String requestURI = request.getRequestURI().toString(); // login1
		
		log.info("requestURL : " + requestURL);
		log.info("requestURI : " + requestURI);
		
		HandlerMethod method = (HandlerMethod) handler;
		Method methodObj = method.getMethod();
		
		// kr.or.ddir.controller.login.LoginController@312r2dta
		log.info("Bean : " + method.getBean());
		// public java.lang.String.kr.or.ddir.controller.login.LoginController.loginForm()
		log.info("method : " + methodObj);
	}
	
}

package kr.or.ddit.controller.intercept;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import kr.or.ddit.vo.crud.CrudMember;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
public class LoginController {

	@RequestMapping(value = "/login1", method = RequestMethod.GET)
	public String loginForm() {
		return "login/loginForm";
	}
	
	@RequestMapping(value = "/login1", method = RequestMethod.POST)
	public String login(String userId, String userPw, Model model) {
		CrudMember member = new CrudMember();
		member.setUserId(userId);
		member.setUserPw(userPw);
		member.setUserName("홍길동");
		model.addAttribute("user", member);
		return "login/success";
	}
	
}

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>Login Form</title>
	</head>
	<body>
		<h2>Login Form</h2>
		<form action="/login1" method="post">
			아이디 : <input type="text" name="userId" /><br />
			비밀번호 : <input type="text" name="userPw" /><br />
			<button type="submit">전송</button>
		</form>
	</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>SUCCESS</title>
	</head>
	<body>
		<h1>로그인 되었습니다!</h1>
	</body>
</html>

- http://localhost/login1

@Slf4j
@Controller
public class HomeController {
	
	private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
	
	@RequestMapping(value = "/", method = RequestMethod.GET)
	public String home(Locale locale, Model model) {
		logger.info("Welcome home! The client locale is {}.", locale);
		
		Date date = new Date();
		DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
		
		String formattedDate = dateFormat.format(date);
		
		model.addAttribute("serverTime", formattedDate );
		
		return "home";
	}

* 정상적으로 동작하고 있지 않다. 왜? 현재 어느 위치에 있는가? ddit 아래. 이 녀석 빈 등록되었나? AOP 도입하면서 basepackage 구조가 바뀌었다. 즉, controller 패키지 안에 들어가야 한다.


[servlet-context.xml]

	<!-- 
		인터셉터 설정 
		- loginInterceptor 클래스를 빈으로 정의한다.
			> 설정한 클래스는 해당 위치에 존재해야 함(설정에 맞는 위치에 있어야 한다)	
	-->
	<beans:bean id="loginInterceptor" class="kr.or.ddit.controller.intercept.LoginInterceptor" />
	<interceptors>
		<interceptor>
			<mapping path="/login1" />
			<beans:ref bean="loginInterceptor" />
		</interceptor>
	</interceptors>
	
	<beans:bean id="accessLoggingInterceptor" class="kr.or.ddit.controller.intercept.AccessLoggingInterceptor" />
	<interceptors>
		<interceptor>
			<mapping path="/**" />
			<exclude-mapping path="/resources/**"/>
			<beans:ref bean="accessLoggingInterceptor" />
		</interceptor>
	</interceptors>

package kr.or.ddit.controller.intercept;

import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AccessLoggingInterceptor extends HandlerInterceptorAdapter {

	PrintWriter writer;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		log.info("preHandle() 실행...!");
		
		File file = new File("C:/logs/ddit-logging.log");
		writer = new PrintWriter(new FileWriter(file, true), true);
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		log.info("postHandle() 실행...!");
		
		String requestURI = request.getRequestURI();
		log.info("requestURI : " + requestURI);
		
		HandlerMethod handlerMethod = (HandlerMethod) handler;
		Method method = handlerMethod.getMethod();
		
		// class kr.or.ddit.controller.BoardController 와 같은 녀석
		Class clazz = method.getDeclaringClass();
		// kr.or.ddit.controller.BoardController 와 같은 녀석
		String className = clazz.getName();
		// BoardController 와 같은 녀석
		String classSimpleName = clazz.getSimpleName();
		// boardList와 같은 메서드
		String methodName = method.getName();
		
		writer.printf("현재일시 : %s %n", getCurrentTime());
		writer.printf("Access Controller : %s %n", className + "." + methodName);
		writer.println("==========================================");
	}

	// 현재 일시 가져오기
	private String getCurrentTime() {
		DateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
		Calendar cal = Calendar.getInstance();
		cal.setTimeInMillis(System.currentTimeMillis());
		return formatter.format(cal.getTime());
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
			throws Exception {
		log.info("afterCompletion() 실행...!");
		super.afterCompletion(request, response, handler, ex);
	}
	
}

- http://localhost/board/tag/list.do


 

'대덕인재개발원 > 대덕인재개발원_웹기반 애플리케이션' 카테고리의 다른 글

231212_SPRING 2 (15-1)  (0) 2023.12.11
231211_SPRING 2 (14-1)  (0) 2023.12.11
231208_SPRING 2 (13-1)  (0) 2023.12.08
231207_SPRING 2 (12-2)  (1) 2023.12.07
231207_SPRING 2 (12-1)  (1) 2023.12.07