프로젝트 : HandlerInterceptor를 이용한 읽지 않은 알람 처리

김건우·2023년 2월 7일
0

spring

목록 보기
8/9
post-thumbnail

현재 프로젝트에서 알림 생성 기능은 구현하였다. 이제 구현 해야할 기능은 읽지 않은 알림(Notification)유무에 따라서 알림 아이콘을 다르게 보여줘야 한다. 서버사이드는 타임리프를 사용하여 맨 끝에서 타임리프 문법을 설명 할 것이고 , 지금부터는 🎈왜 알림 유무 확인을 HandlerInterceptor를 사용하였는가이다.

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에 값을 넣어줘야함)

model에 읽지 않은 알람의 갯수를 전달하려고했다. 하지만 문제가 있다. 알람의 기능은 어느 view단에서도 다 들어가는 항목인데 모든 핸들러에 해당 model 정보를 모든 곳에서 넘겨줘야하는 이슈가 나타난 것이다(동일 코드 반복).

🌞HandlerIntercpetor의 사용이유

그래서 이 과정에서 든 생각은 view를 랜더링 하기 직전에 공통으로 처리하게 하는 부분이 있으면 좋겠다라고 생각하고 그 과정에서 HandlerIntercpetor가 딱 맞는 역할이라고 생각했다.

HandlerInterceptor 란?

Intercept라는 단어는 '낚아채다'라는 의미이다. 해당 단어의 의미와 같이 사용자 요청에 의해 서버에 들어온 Request 객체를 컨트롤러의 핸들러(사용자가 요청한 url에 따라 실행되어야 할 메서드, 이하 핸들러)로 **도달하기 전에 낚아채서 개발자가 원하는 추가적인 작업을 한 후 핸들러로 보낼 수 있도록 해주는 것이 인터셉터(Interceptor)다.

🎈 여기서 중요한 점은 읽지 않은 알람을 확인하는 기능은 VIEW를 렌더링 하기 전에 작업을 해줘야 한다. ➡ postHandle() 메서드를 재정의하자!

앞에서도 말하였지만 확인하지않은 알람을 VIEW단에 다르게 표시해야하는데 그렇기 위해서는 Interceptor가 제공하는 메서드 중에서 postHandle() 메서드를 재정의 해야한다.

postHandle() : 핸들러가 실행은 완료되었지만 아직 View가 생성되기 이전에 호출된다.

NotificationInterceptor

다음 아래의 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);

이제 이렇게 만든 NotifiacationInterceptor를 spring에 등록을 해주자!

Interceptor 적용하지 않아야 할 부분들?

  1. 리다이렉트 요청에는 적용하지 않아야한다.
  2. static 리소스 요청에도 적용하지 않아야한다.

위의 두 개의 부분에서는 해당 읽지 않은 알람값을 넘겨 줄 필요가 없다.
따라서 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에 전송

profile
Live the moment for the moment.

0개의 댓글