현재 프로젝트에서 알림 생성 기능은 구현하였다. 이제 구현 해야할 기능은 읽지 않은 알림(Notification)유무에 따라서 알림 아이콘을 다르게 보여줘야 한다. 서버사이드는 타임리프를 사용하여 맨 끝에서 타임리프 문법을 설명 할 것이고 , 지금부터는 🎈왜 알림 유무 확인을 HandlerInterceptor를 사용하였는가이다.
맨 처음에 알림 아이콘을 다르게 보여주기 위한 기능을 생각했을 때는 다음의 코드와 같이 model에 값을 넣어 view 에 넘겨 주려고 했다.
/**메인화면**/
@GetMapping("/")
public String home(@CurrentUser Account account, Model model) {
if (account != null) {
model.addAttribute(account);
}
/**view를 렌더링 하기직전에 읽지 않은 알람 check**/
long count = notificationRepository.countByAccountAndChecked(account, false);
model.addAttribute("hasNotification", count > 0);
return "index";
}
Notification(알람) 엔티티에는 checked라는 필드가 존재한다.
1.checked = true (읽은 알람)
2.checkeed = false(읽지 않은 알람)
따라서 아래의 메서드와 같이 회원가 checked=false를 매개변수로 읽지 않은 알람의 갯수를 조회한다.
/**해당 회원의 checked=false 알람 갯수 조회**/
notificationRepository.countByAccountAndChecked(account, false);
model에 읽지 않은 알람의 갯수를 전달하려고했다. 하지만 문제가 있다. 알람의 기능은 어느 view단에서도 다 들어가는 항목인데 모든 핸들러에 해당 model 정보를 모든 곳에서 넘겨줘야하는 이슈가 나타난 것이다(동일 코드 반복).
그래서 이 과정에서 든 생각은 view를 랜더링 하기 직전에 공통으로 처리하게 하는 부분이 있으면 좋겠다라고 생각하고 그 과정에서 HandlerIntercpetor가 딱 맞는 역할이라고 생각했다.
Intercept라는 단어는 '낚아채다'라는 의미이다. 해당 단어의 의미와 같이 사용자 요청에 의해 서버에 들어온 Request 객체를 컨트롤러의 핸들러(사용자가 요청한 url에 따라 실행되어야 할 메서드, 이하 핸들러)로 **도달하기 전에 낚아채서 개발자가 원하는 추가적인 작업을 한 후 핸들러로 보낼 수 있도록 해주는 것이 인터셉터(Interceptor)다.
앞에서도 말하였지만 확인하지않은 알람을 VIEW단에 다르게 표시해야하는데 그렇기 위해서는 Interceptor가 제공하는 메서드 중에서 postHandle() 메서드를 재정의 해야한다.
postHandle() : 핸들러가 실행은 완료되었지만 아직 View가 생성되기 이전에 호출된다.
다음 아래의 HandlerInterceptor를 구현한 NotificationInterceptor의 postHandle() 재정의 메서드를 보면 조건이 있다.
1. 인증된 사용자만 읽지 않은 알림 기능을 사용할 수 있다.(로그인 회원)
2. 리다이렉트 상황에서는 해당 interceptor의 postHandle()이 동작하지 않아야한다.
3. modelAndView가 null이 아니어야 한다.(렌더링 될 view가 존재해야함)
@Component
@RequiredArgsConstructor
public class NotificationInterceptor implements HandlerInterceptor {
private final NotificationRepository notificationRepository;
/**postHandle()은 뷰 랜더링 전 핸들러 처리이후**/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//인증이 된 회원만
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (modelAndView != null && !isRedirectView(modelAndView) && authentication != null
&& authentication.getPrincipal() instanceof UserAccount) {
Account account = ((UserAccount)authentication.getPrincipal()).getAccount();
//읽지 않은 알림 갯수
long count = notificationRepository.countByAccountAndChecked(account, false);
//true이면 읽지 않은 알람 존재
modelAndView.addObject("hasNotification", count > 0);
}
}
/**리다이렉트 view가 아닌 경우**/
private boolean isRedirectView(ModelAndView modelAndView) {
return modelAndView.getViewName().startsWith("redirect:") || modelAndView.getView() instanceof RedirectView;
}
위의 메서드의 반환 값이 true이면 리다이렉트 방식의 핸들러이다. 따라서 !isRedirectView(modelAndView) 조건을 넣어 준 것이다.🔽
if (modelAndView != null && !isRedirectView(modelAndView) && authentication != null && authentication.getPrincipal() instanceof UserAccount)
또 Principal 안에 들어있는 UserAccount 객체를 다운캐스팅 해주어서 실제 회원(account) 객체를 꺼내와서 해당 회원의 읽지 않은 알람의 갯수를 check해주었다.🔽
//로그인 한 회원
Account account = ((UserAccount)authentication.getPrincipal()).getAccount();
//읽지 않은 알림 갯수
long count = notificationRepository.countByAccountAndChecked(account, false);
읽지 않은 알람 갯수가 0보다 크면 읽지 않은 알람이 존재한다는 의미이므로 model에 해당 flag를 전달해준다. true이면 읽지 않은 알람이 존재한다는 것이다.🔽
//true : 읽지않은 알람 존재 , false : 알람 모두 읽음
modelAndView.addObject("hasNotification", count > 0);
위의 두 개의 부분에서는 해당 읽지 않은 알람값을 넘겨 줄 필요가 없다.
따라서 excludePathPatterns()를 사용하여 위의 2개의 요청에는 제외시켜주자!
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final NotificationInterceptor notificationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//static 리소스를 문자열 배열로
List<String> staticResourcesPath = Arrays.stream(StaticResourceLocation.values())
.flatMap(StaticResourceLocation::getPatterns)
.collect(Collectors.toList());
//node_modules 추가
staticResourcesPath.add("/node_modules/**");
registry.addInterceptor(notificationInterceptor)
.excludePathPatterns(staticResourcesPath);
}
}
이제 그러면 읽지 않은 알람이 존재한다면 "hasNotification"이 true로 전달 될 것이다. 따라서 타임리프는 다음과 같이 작성해주면된다.
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" th:href="@{/notifications}">
<i th:if="${!hasNotification}" class="fa fa-bell-o" aria-hidden="true"></i>
<span class="text-info" th:if="${hasNotification}"><i class="fa fa-bell" aria-hidden="true"></i></span>
</a>
</li>
hasNotification이 false일 때는 빈 벨이 보이게 설정해주었고 hasNotification이 true일 때는 벨에 색깔을 넣어 주었다.
읽지 않은 알람 존재 시
색이 있는 알람벨 view에 전송
모든 알람 읽었을 시
빈 알람벨 view에 전송