on
Spring Framework - RedirectAttributes 설명
Spring Framework - RedirectAttributes 설명
RedirectAttributes 를 찾아보게 된 계기
인증 실패 문구
운영중인 서비스에서 로그인 인증에 실패하면 아래와 같이 인증 실패 문구를 보여주고있다.
하지만 실패 문구가 보일때가 안보일때가 있다는 문의가 인입되었다.
실패 문구 노출 조건은 아래와 같이 Handlebars 로 작성되어있다.
{{#if fail}} 비밀번호가 맞지 않습니다. {{else}}
로그인 시도시 동작 흐름은 아래와 같다.
Login 에 실패할 경우 로그인 Page 로 다시 redirect 시키고 있다.
하지만 로그인에 실패하였을때 fail 여부를 전달하기 위해 Parameter 로 전달하지 않고 있다.
또한 Login Controller 에서 fail 에 대한 Attribute 를 넣어주고 있지도 않았다.
하지만 운영에서는 간헐적으로 동작하지 않고 있지만 로컬 환경에서는 매번 실패메시지가 정상적으로 노출되고 있었다.
View 를 리턴하는 Controller 가 아닌 인증 Controller 에서 fail 에 대한 Attribute 로 아래와 같이 세팅해주고 있는것을 보고 RedirectAttributes 에 대해서 찾아보기 시작했다.
어떻게 서로 다른 HTTP 요청이 Attribute 를 공유하고 있는지를 찾는 것과 운영 환경에서는 간헐적으로 실패가 되는지를 찾아야 했다.
redirectAttributes.addFlashAttribute("fail", Boolean.True);
RedirectAttributes 공식 문서 설명
https://docs.spring.io/spring-framework/docs/3.2.15.RELEASE/spring-framework-reference/htmlsingle/#new-in-3.1-flash-redirect-attributes
Spring Framework 3.2.15.RELEASE 문서에는 아래와 같이 설명되어있다.
FlastAttribute 는 FlashMap 을 HTTP session 에 저장하여 Redirect 후 에도 남아있을 수 있도록 한다. Annotation 이 달린 Controller 에 @RequestMapping 함수는 RedirectAttributes 타입의 메소드를 선언하여 Flash Attribute 를 추가할 수 있다. < addFlashAttribute() > . 이 메서드를 사용하여 Redirect 시에 Attribute 를 필요에 따라 제어할 수 있다.
Code Level 분석
공식 문서에 보면 Http session 에 저장한 Attribute 를 Redirect 시에 불러와서 사용할 수 있다고 설명되어있다.
이 내용을 코드레벨로 확인해보면 아래와 같다.
저장
redirectAttributes.addFlashAttribute("fail", Boolean.True);
위와 같이 addFlashAtrribute 함수를 호출하면 RedirectAttributesModelMap 의 fashAttributes 에 put 하게 된다.
RedirectAttributesModelMap.addFlashAttribute()
@Override public RedirectAttributes addFlashAttribute(String attributeName, @Nullable Object attributeValue) { this.flashAttributes.addAttribute(attributeName, attributeValue); return this; }
이렇게 RedirectAttributesModelMap 에 저장된 Attribute 는 RequestMappingHandlerAdapter 에서 불러와져서 OutputFlashMap 에 저장되게 된다.
RequestMappingHandlerAdapter.getModelAndView()
if (model instanceof RedirectAttributes) { Map flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); if (request != null) { RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } }
OutputFlashMap 에 저장된 FlashAttribute 는 DispatcherServlet 의 doService() 함수에서 Session 에 저장되게 된다.
DispatcherServlet.doService()
if (this.flashMapManager != null) { FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); if (inputFlashMap != null) { request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); } request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); }
FlashMap 에 필요한 로직들이 실제로 처리되는 클래스는 AbstractFlashMapManager 이다.
AbstractFlashMapManager.retrieveAndUpdate()
if (!mapsToRemove.isEmpty()) { Object mutex = getFlashMapsMutex(request); if (mutex != null) { synchronized (mutex) { allFlashMaps = retrieveFlashMaps(request); if (allFlashMaps != null) { allFlashMaps.removeAll(mapsToRemove); updateFlashMaps(allFlashMaps, request, response); } } } else { allFlashMaps.removeAll(mapsToRemove); updateFlashMaps(allFlashMaps, request, response); } }
위에서 호출한 updateFlashMaps 함수에서 해당 HTTPRequest 에 해당하는 Session 에 저장을 해준다.
SessionFlashMapManager.updateFlashMaps()
/** * Saves the given FlashMap instances in the HTTP session. */ @Override protected void updateFlashMaps(List flashMaps, HttpServletRequest request, HttpServletResponse response) { WebUtils.setSessionAttribute(request, FLASH_MAPS_SESSION_ATTRIBUTE, (!flashMaps.isEmpty() ? flashMaps : null)); }
이렇게 저장한 Attribute 는 서버의 Session 저장소에 남아있게 된다.
불러오기
Controller 에서 Attribute 를 불러오다가 ModelAndView 에 addAttribute 해주지 않아도 저절로 Response 에 포함되어 전달되게 된다. 이 역할을 해주는 코드는 아래와 같다.
마찬가지로 DispatcherServlet 에서 Session 에서 데이터를 불러와서 request.setAttribute() 를 호출하여 INPUT_FLASH_MAP_ATTRIBUTE 라는 이름으로 세팅해준다.
DispatcherServlet.doService()
if (this.flashMapManager != null) { FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); if (inputFlashMap != null) { request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); } request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); }
Session 에서 FlashAttribute 를 불러오는 부분은 SessionFlashMapManager 에서 처리된다. 위에서는 flashMapmanager.retrieveAndUpdate() 호출한 부분이다.
SessionFlashManager.retrieveAndUpdate()
/** * Retrieves saved FlashMap instances from the HTTP session, if any. */ @Override @SuppressWarnings("unchecked") @Nullable protected List retrieveFlashMaps(HttpServletRequest request) { HttpSession session = request.getSession(false); return (session != null ? (List) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE) : null); }
정리
RedirectAttribute 는 http session 에 attribute 를 저장하여 새로운 HTTP 요청이 온다고 하더라도 동일한 JSESSIONID 를 가지고 있다면 Attribute 를 불러와서 사용할 수 있게 되었다
서두에 설명한 이슈의 원인은 서버가 여러대인 상황에서 Http Session 이 Clustering 이 되어있지 않기 떄문에 같은 sessionId 로 조회한다고 하더라도 새로운 서버로 요청이 전달되면 Session 에 Attribute 가 저장되어있지 않아 발생한 문제였다.
from http://woooongs.tistory.com/83 by ccl(A) rewrite - 2021-12-17 14:01:55