티스토리 뷰

RedirectAttributes 를 찾아보게 된 계기

인증 실패 문구

운영중인 서비스에서 로그인 인증에 실패하면 아래와 같이 인증 실패 문구를 보여주고있다.

하지만 실패 문구가 보일때가 안보일때가 있다는 문의가 인입되었다.

 

실패 문구 노출 조건은 아래와 같이 Handlebars 로 작성되어있다.

{{#if fail}}
	<label>
 		비밀번호가 맞지 않습니다.
    </label>
{{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<String, ?> 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<FlashMap> 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<FlashMap> retrieveFlashMaps(HttpServletRequest request) {
	HttpSession session = request.getSession(false);
	return (session != null ? (List<FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE) : null);
}

 

정리

  • RedirectAttribute 는 http session 에 attribute 를 저장하여 새로운 HTTP 요청이 온다고 하더라도 동일한 JSESSIONID 를 가지고 있다면 Attribute 를 불러와서 사용할 수 있게 되었다
  • 서두에 설명한 이슈의 원인은 서버가 여러대인 상황에서 Http Session 이 Clustering 이 되어있지 않기 떄문에 같은 sessionId 로 조회한다고 하더라도 새로운 서버로 요청이 전달되면 Session 에 Attribute 가 저장되어있지 않아 발생한 문제였다.

'Spring Framework' 카테고리의 다른 글

Spring JPA 와 Dynamic Proxy  (0) 2022.04.24
Spring Bean 과 CGLIB proxy  (0) 2022.04.23
Spring RequestMapping 우선순위  (0) 2021.02.04
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함