티스토리 뷰
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 공식 문서 설명
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
- GlobalFilter
- reactor
- dynamodb
- mariada-connector
- RoutePredication
- wait()
- notify()
- aurora
- ConcurrentHashMap
- referencedColumnName
- getBoolean
- mariadb-connector-j
- HashMap
- DyanomoDB
- AbstractMethodError
- Lazy
- rate limit
- Flux
- router
- custom config data convertion
- MariaDB
- notifyAll()
- spring cloud gateway
- ResultSet
- reative
- circurit breaker
- RouteDefinition
- msyql-connector-java
- Seperate Chaining
- N+1
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |