스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 대시보드 - 인프런 | 강의 (inflearn.com)
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의
웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있
www.inflearn.com
공통 관심사 (Cross-cutting concern)
애플리케이션 여러 로직에서 공통으로 관심이 있는 것
-> 등록, 수정, 삭제, 조회 등 여러 로직에서 공통으로 인증에 대해서 관심을 가짐
필터 제한
로그인 O: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
로그인 X: HTTP 요청 -> WAS -> 필터 (서블릿 호출 X)
필터 적용하면 필터 호출한 뒤에 서블릿 호출됨. 모든 고객의 요청 로그 남길 때 필터를 사용하면 됨
필터에 특정 URL 패턴 적용 가능
필터 인터페이스
필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.
① init(): 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
② doFilter(): 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
③ destroy(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
※ 싱글톤 -> 하나만 등록됨
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
public default void destroy() {
}
}
|
cs |
로그 필터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("로그 필터 초기화");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("로그 필터 doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 요청 URI 확인
String requestURI = httpRequest.getRequestURI();
// 요청을 사용자별로 구분
String uuid = UUID.randomUUID().toString();
try {
log.info("REQUEST [{}][{}]", uuid, requestURI);
// 있으면 다음 필터
// 없으면 서블릿 호출
chain.doFilter(request,response);
} catch (Exception e){
throw e;
} finally {
log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("로그 필터 소멸");
}
}
|
cs |
인증 체크 필터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// 로그인 안해도 접근 가능한 uri
private static final String[] whiteist = {"/", "/members/add", "/login", "/logout", "/css/*"};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
HttpServletResponse httpResponse = (HttpServletResponse) response;
try{
log.info("인증 체크 필터 시작{}", requestURI);
// 요청 url이 white list에 없으면 인증 체크 필요
if (isLoginCheckPath(requestURI)) {
log.info("인증 체크 로직 실행 {}", requestURI);
HttpSession session = httpRequest.getSession(false);
if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null){
log.info("미인증 사용자 요청 {}", requestURI);
// 로그인 페이지로 보냄
// 로그인 성공 시 요청한 URI로 다시 redirect
// LoginController에서 처리하는 로직 추가 필요
httpResponse.sendRedirect("/login?redirectURL="+requestURI);
return;
}
}
chain.doFilter(request, response);
} catch (Exception e){
throw e;
} finally {
log.info("인증 체크 필터 종료 {}", requestURI);
}
}
/**
* 화이트 리스트의 경우 인증 체크 X
*/
private boolean isLoginCheckPath(String requestURI){
return !PatternMatchUtils.simpleMatch(whiteist, requestURI);
}
|
cs |
※ chain.doFilter(request, response) 호출 안하면 다음으로 진행 안됨!!
필터 등록
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1); // 필터간 순서 지정
filterRegistrationBean.addUrlPatterns("/*"); // 적용할 URL 지정
return filterRegistrationBean;
}
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginFilterCheck());
filterRegistrationBean.setOrder(2);
filterRegistrationBean.addUrlPatterns("/*"); // 필터의 white list에 적어놨기 때문에 모든 경로로 설정해도 됨 (유지보수 편리)
return filterRegistrationBean;
}
|
cs |
인증 성공 시, 처음 요청한 URL로 redirect
1
2
3
4
5
6
7
8
9
|
@PostMapping("/login")
public String loginV4(@Valid @ModelAttribute("loginForm") LoginForm form,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request){
...
return "redirect:"+redirectURL;
}
|
cs |
스프링 인터셉터
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 1(비 로그인 사용자는 여기서 종료) -> 인터셉터 2 -> ... -> 컨트롤러
호출 순서
HTTP 요청 -> Dispatcher Servlet -> preHandle() -> 핸들러 어댑터 -> 핸들러 (컨트롤러) -> postHandle() -> View -> afterCompletion()
예외 발생 시
preHandle: 컨트롤러 호출 전에 호출됨
postHandle: 컨트롤러에서 예외 발생하면 postHandle 호출 안됨
afterComletion: 예외가 발생해도 항상 호출 됨. Exception ex를 파라미터로 받아서 어떤 예외가 발생했는 지 출력 가능
인터셉터 인터페이스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
}
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 {
}
}
|
cs |
로그 인터셉터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
// afterCompletion에서 사용하기 위해 request에 전달
request.setAttribute(LOG_ID, uuid);
// @RequestMapping: HandlerMethod
// 정적 리소스: ResourceHttpRequestHandler
if(handler instanceof HandlerMethod){
// 호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다
HandlerMethod hm = (HandlerMethod) handler;
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
// preHandle에서 생성한 uuid를 request를 통해서 전달받음
Object logId = request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", logId, requestURI, handler);
// 에러 출력
if(ex!=null){
log.error("afterCompletion error!!", ex);
}
}
}
|
cs |
● 종료 로그를 postHandle이 아니라 afterCompletion에서 실행하는 이유
-> 예외 발생하면 postHandle이 호출되지 않기 때문
● 요청 로그 구분하기 위해 uuid 생성
서블릿 필터는 지역 변수로 해결 가능하지만, 스프링 인터셉터는 호출 시점이 분리되어 있음. preHandle에서 생성한 값을 postHandle, afterCompletion에서 사용하려면 request에 담아두어야 함. (싱글톤처럼 사용되기 때문에 멤버 변수에 담아두면 위험함)
request.getAttribute(LOG_ID)로 찾아서 사용
인증 체크 인터셉터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("인증 체크 인터셉터 실행 {}", requestURI);
HttpSession session = request.getSession();
if(session==null || session.getAttribute(SessionConst.LOGIN_MEMBER) ==null){
log.info("미인증 사용자 요청");
// 로그인으로 redirect
response.sendRedirect("/login?redirectURL="+requestURI);
return false;
}
return true;
}
|
cs |
인터셉터 등록
1
2
3
4
5
6
7
8
9
10
11
12
|
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/", "/members/add", "/login", "/logout", "/css/**", "/*.ico");
}
|
cs |
ArgumentResolver 활용
전
1
2
3
4
|
@GetMapping("/")
public String homeLoginV3ArgumentResolver(
@SessionAttribute(name=SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
Model model) {
|
cs |
후
1
2
3
4
|
@GetMapping("/")
public String homeLoginV3ArgumentResolver(
@Login Member loginMember,
Model model) {
|
cs |
@Login이 붙어있으면 ModelAttribute가 동작하는 것이 아니라 argumentResolver가 동작하도록 변경
LoginMemberArgumentResolver
① supportsParameter(): 해당 어노테이션 지원 여부 확인. 지원하면 ArgumentResolver 사용
- @Login 붙어있는가?
- 파라미터가 Member 타입인가?
② resolveArgument(): 컨트롤러 호출 직전에 호출되어서 필요한 파라미터 정보 생성. 세션에 있는 로그인 회원 정보인 member 객체 찾아서 반환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
// 해당 어노테이션 지원 여부 확인
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
// @Login이 붙어있는가?
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
// 파라미터가 Member 타입인가?
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
// 세션에 저장된 정보 찾기
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if(session==null){
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
|
cs |
ArgumentResolver 등록
1
2
3
4
|
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
|
cs |
필터보다 인터셉터가 사용하기 편함
ArgumentResolver 사용하면 호출 편리
'오늘 배운 것' 카테고리의 다른 글
[스프링 MVC 2] API 예외 처리 (0) | 2021.07.24 |
---|---|
[스프링 MVC 2] 예외 처리와 오류 페이지 (0) | 2021.07.23 |
[스프링 MVC 2] 로그인 처리1 - 쿠키, 세션 (0) | 2021.07.20 |
[스프링 MVC 2] 검증2 - Bean Validation (0) | 2021.07.19 |
2021.07.14 (수) (0) | 2021.07.14 |