서론 : 인터셉터를 사용하게 된 계기 (더보기를 눌러서 볼 수있습니다)
게시판을 만들다가 다음과 같은 문제점이 발생했다.
코드의 무한 중복
@GetMapping("/board/openBoardDetail")
public String boardDetail(@SessionAttribute(name = SessionConstants.LOGIN_MEMBER,required = false)MemberDto loginMember,@RequestParam("board_idx") int boardIdx,Model model) throws Exception{
//로그인 된 유저가 접근하는지 확인
if (loginMember == null){
return "redirect:/";
}
//게시글 상세 내용 가져오기
BoardDto boardDto = boardService.getBoardDetail(boardIdx);
model.addAttribute("boardDto",boardDto);
//게시글 수정권한 확인
boolean modificationAuthority = boardService.checkModificationAuthority(loginMember.getLoginId(),boardDto.getCreatorId());
model.addAttribute("modificationAuthority" , modificationAuthority);
//페이지에서 멤버 정보 표시 위해 멤버 어트리 뷰트 추가
model.addAttribute("member",loginMember);
return "board/boardDetail";
}
게시판 컨트롤러 중 하나를 가져와 봤다.
기존의 나의 코드는 이렇게 작성되었다.
- 로그인하지 않아도 접속이 가능한 컨트롤러와
- 로그인을 해야만 접속이 가능한 컨트롤러를 다른 폴더에 만들었다
- 그리고는 로그인한 멤버만 접속해야 되는 컨트롤러 마다 위아래로 코드를 붙여주었다.
//로그인 된 유저가 접근하는지 확인
if (loginMember == null){
return "redirect:/";
}
로그인을 확인하는 코드를 항상 추가하고, 로그인이 안되면 홈으로 돌려보냈다.
//페이지에서 멤버 정보 표시 위해 멤버 어트리 뷰트 추가
model.addAttribute("member",loginMember);
로그인이 된 페이지는 항상 회원의 이름을 보여줘야 했기때문에 MemberDto를 항상 추가시켜줘야 했다.
문제는..
너무 코드가 지저분해지고 유지보수가 사실상 불가능 했다.
그리고 빼먹는 경우도 많았고,
하나를 바꾸려고 하거나 홈의 mapping을 바꾸는게 말도 안되게 힘들었다.
그래서 아래와 같은 방법을 생각해보았다.
서론2 : 뭔가 한번에 적용할 수 있는 방법이 없을까?
(filter, interceptor, aop 자세히 읽으려면 펼펴보세요)
라는 생각에 전에 읽던 스프링 부트 튜토리얼 책에서 filter, interceptor, aop에 관한 부분을 보았다.
셋이 비슷하면서 달랐다.
일단 filter는 스프링 밖에서 실행되므로 논외였고, interceptor와 aop중에 고민되었다.
🎈 보니까 일단 실행순서가 interceptor ->aop->(기능)->aop->interceptor 와 같이 interceptor가 더 외곽의 것이 었다.
🎈 interceptor는 controller 이전에 실행되므로 조건에 맞지 않으면 controller에 접근 조차 하지 않게 제어 할 수 있었다.
controller에 접근하지 않는다는 것은 아무래도 mapping을 찾는 과정이나 계산이 조금은 줄어들 수 있다고 생각했다. (확실하지 않다)
그리고 controller에 접근하기 전에 로그인 체크를 하는게 매우 적절한 순서라고 생각했다.
🎈 interceptor는 uri pattern을 통해 대상을 지정할 수 있었다.
사실 난 전에는 이 기능을 알고는 있었지만, 그냥 책에서 본 수준이라 미리 생각하지 못했다.
그래서 접근 가능한 기능을 폴더를 통해 구분하고 있었는데,
이 방법을 보니 훨씬 탁월하다고 생각했다.
예를 들어 많은 기능중에 "/board/**" 로 시작되는 게시판 관련 기능은 로그인 후에만 접속 가능하게 하려고 하면,
controller에서 mapping할 때 단순히 /board/ 뒤에 기능을 붙여 mapping을 하면 되는 것이다.
이것은 interceptor 사용을 위해서도 좋지만, controller내부의 가독성에도 좋다고 생각을 했다.
aop는 조금더 세밀한 타겟 지정이 가능했고, 실행 시점이 좀더 안쪽인 특징도 있었다.
뭐 aop도 좋지만, 일단 최초와 최후를 담당하는 로그인 확인 기능을 가장 밖에 두면 기능간에 순서를 나눌때 더 유용하다고 생각했다.
예를 들어 로그인 기능이 aop를 통해 너무 안쪽에 위치하면 그보다 더 안쪽을 정의할때 약간 애매해지며,
로그인 기능보다 더 상위의 기능은 filter을 통해 구현하기 적합한 기능 (보안, 암호화 등)이라고 생각하였다.
이제 Interceptor를 적용시켜보자
우선 기본적으로 Interceptor를 만들기 위해 implement하는 interface부터 살펴보자.
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 {
}
}
주석은 제거하고 가져왔다.
여기서 보니 얼마전에 읽었던 default 메서드가 구현되어 있다.
preHandle, postHandle, afterCompletion만 적절히 구현해주면 된다는 것을 알 수 있다.
그렇다면 그게 뭐냐하면
간단하게 말하면
preHandle -> 컨트롤러 호출 전
postHandle -> 컨트롤러 실행 후
afterComplete -> 뷰에서 최종결과가 생성하는 일을 포함한 모든 일이 완료되었을 때 실행
흐름은 이러하다.
controller를 호출하기 전 인터셉터를 호출한다.
매우 간단하게 설명하면
- Request가 Dispatcher Servlet에 들어오면
- 우선 Handler Mapping을 통해 적합한 Handler를 찾아 가져온다.
- (handler를 실행시킬수 잇는 HandlerAdapter 찾기)
- Handler객체에 적용할 interceptor가 존재하면 객체의 preHandle메서드를 호출한다.
다른 곳은 몰라도 저기만 보면 된다. controller로 가기 전 preHandle이 작동한다는 것이다.
그러면 어떻게 작동하냐? preHandler 부터
파라미터의 갯수나 순서가 바뀌면 다른 메서드로 인식되므로 Override가 되지 않는다는 것을 조심하자
위 인터페이스의 preHandler메서드를 보자
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
자 return타입은 boolean 즉 true false로 나온다는 것이다.
true -> controller로 진행됨
false -> controller로 진행되지 않음
자 controller로 가기 전에 흐름을 가로채서 제어한다.
주어지는 재료들을 보면 (HttpServletRequest request, HttpServletResponse response, Object handler)
HttpServletRequest request, HttpServletResponse response는 너무 많이 써서 잘 아신다고 가정하고,
구글링 해도 잘 안나오는 Object handler에 대해서 쓰겠다.
Object handler
Object handler는 현재 실행하려는 메서드 자체를 의미한다. 위의 구조에서 보드시 지금 이동하려는 controller 메서드를 의미한다.
그래서 아래와 같이 컨트롤러와 메서드 정보를 파악 할 수 있다.
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
System.out.println("Bean: " + handlerMethod.getBean());
System.out.println("Method: " + method);
return true;
}
즉 첫번째 preHandle에서 여러가지 처리를 한 뒤 컨트롤러로 보낼지 말지 제어가 가능하다.
나의 경우는 로그인 세션 체크 기능을 여기에 넣었고, 로그인이 안되었을시 다른곳으로 보냈다.
자세한 내용은 나중에 쓰겠다.
두번째 postHandle
말 그대로 메서드가 실행된 다음 실행되는 메서드 이다.
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
매게 변수는 보시다 싶이 ModelAndView가 들어있다. 그리고 @Nullable이다.
ModelAndView 는 우리가 컨트롤러에서 넘겨주는 부분에 힌트가 있다.
흔히 우리가 컨트롤러에서 넘겨줄 때 세가지 방법이 있다.
- return 값을 String으로 하고, Model model을 받아서 attribute를 넣어주는 방법(model.addAttribute)
- return 값을 ModelAndView로 하고, 직접 setViewName이나 addAttribute를 해주는 방법
- "redirect:/"와 같이 리다이렉트 시키는 방법
일단 여기서 1,2번째 방법만 보겠다.
둘중 어떤것을 쓰던지 간에 ModelAndView로 바뀌어 전달된다.
즉 여기서 ModelAndView를 받아서 추가적인 작업을 추가해줄 수있다.
나의 경우는 Session에서 loginMember를 받아다가 넣어주는 작업이 반복되었는데, 여기서 넣어주었다.
자세한 방법은 아래에 쓰겠다.
세번째 afterCompletion
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
여기서는 Exception을 받을 수 있다.
DispatcherServlet의 화면 처리가 완료된 상태에서 처리한다.
exception을 추가로 받기 때문에 관련 처리가 가능하다.
나는 어떻게 처리했는가?
public class LoginInterceptor implements HandlerInterceptor {
public final static String TARGET_PATTERN = "/board/**";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MemberDto memberDto = (MemberDto) request.getSession(true).getAttribute(SessionConstants.LOGIN_MEMBER);
if (memberDto!=null){return true;} //로그인 되어있음
//로그인이 안된경우
response.sendRedirect("/");
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//model에 세션 멤버 명 넣어주기
MemberDto memberDto = (MemberDto) request.getSession().getAttribute(SessionConstants.LOGIN_MEMBER);
modelAndView.addObject("member",memberDto);
}
//
// @Override
// public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// }
}
preHandle 에서는 로그인이 되었을 경우 바로 컨트롤러로 전달하고,
로그인이 안되었을 경우 response.sendRedirect를 통해 리다이렉트 시켰다.
그리고 return false를 통해 컨트롤러로의 연결을 막고 종료시켰다.
postHandle 에서는 세션에서 로그인 멤버에 대한 객체를 받아서, model에 추가로 넣어주었다.
이를 통해 각 페이지에서는 모두 로그인 멤버의 정보를 이용해 페이지를 생성할 수있다.
코드의 중복 없이
afterCompletion은 나는 필요가 없어서 구현하지 않았다.
그리고 맨꼭대기에
public final static String TARGET_PATTERN = "/board/**";
이거는 내가 나중에 관리하기 편하게 그냥 인터셉터 자체에 static 상수를 넣어준 것인데 이제 나올 등록할때 사용된다.
이제 이 인터셉터를 등록해야한다
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns(LoginInterceptor.TARGET_PATTERN);
}
}
우선 적절한 위치에다가 WebMvcConfiguration을 만들고 WebMvcConfigurer을 implements 시켜준다.
그리고 addInterceptor를 구현해주면 된다.
파라미터로 받은 registry에 넣어주는 방식인데, 중요한것은 뒤에 패턴 부분이다.
public final static String TARGET_PATTERN = "/board/**";
.addPathPatterns(LoginInterceptor.TARGET_PATTERN);
위와 같이 입력하면 /board/...하위의 모든 요청이 대상이 된다.
예를 들어 /board/openBoardDetail , /board/login/board/boardExample 과 같은것도 모두 대상이 된다.
/board/*와 같이 입력하면 해당문구의 바로 붙은 경로에만 적용된다.
.excludePathPatterns()
로 바꿔쓰면 해당 경로를 제외하고 실행됩니다.
이 패턴을 통해 알게된 사실
mapping을 할 주소를 정할 때 interceptor적용을 염두해두고 주소를 정하는게 좋다.
비슷한 내용끼리는 같은뿌리를 두게 하는 식으로 처음부터 설계해야겠다.
위와 같은 방법으로 나는 로그인 확인과 멤버를 모델에 등록하는 반복코드를 더 아름답게 해결하였고,
작동은 전과 완전히 같았다.
https://gngsn.tistory.com/153 다음 블로그에 맨 아래 이런 내용이 있었다.
아마 비동기 처리와 관련된 Interceptor도 있나보다. 하지만 난 아직 비동기 처리를 잘 알지 못해서 다음에 비동기를 공부할때 같이 공부해서 추가해야겠다.
'SpringBoot > [SpringBoot]' 카테고리의 다른 글
[SpringBoot] 파일 다운로드의 두 가지 방법 (0) | 2022.11.24 |
---|---|
[SpringBoot] Thymeleaf 반복문 첫번째 요소만 다르게 하기 (0) | 2022.11.17 |
[SpringBoot] 웹에 이미지를 표시하는 두가지 방법 (0) | 2022.11.17 |
[SpringBoot] 로컬 파일 저장과 DB를 같이 트랜잭션 시키기 (0) | 2022.11.16 |
[SpringBoot] @Transactional 의도적 에러 발생시키기 (0) | 2022.11.16 |