ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Project(Springboot) - 조회수 Count AOP_1
    카테고리 없음 2022. 10. 24. 22:30

    Project(Springboot) - 조회수 Count AOP_2 바로가기

     

    Project(Springboot) - 조회수 Count AOP_2

    목적 지난번 1차 구현할때 발생한 문제인 DB의 조회수는 바로 올라가나 실제로 응답 하는 데이터에는 바로 반영이 안되는 부분의 해결을 위해 코드 수정을 진행하였다. 1차 구현 게시글 바로 가

    real-coding.tistory.com

    목적

    현재 진행중인 프로젝트에서 게시물마다 조회된 전체 횟수를 카운팅 하는 기능이 필요했으며 세부적인 옵션은 아래와 같다

    • 한 사람이 새로고침과 같은 부정한 방법으로 조회수를 올리는 기능을 방지 할 것
    • 여러 게시물에 공통적용 될 수 있는 기능이니 코드 재활용성을 고려 할 것

     

    구현

    먼저 조회수 중복 카운팅을 막기 위한 방법을 조사 하여보니 크게 세션, 쿠키, IP주소를 활용한 방법이 주로 사용되었다. 

    이번 글에서는 세션과 쿠키를 사용한 방법을 비교하고 실제로 적용한 쿠키를 바탕으로한 적용 사례를 포스팅한다.

     

    세션

    1. 조회 요청이 오면 세션에 고유키로 값이 존재하는지 확인
    2. 세션에 게시글 정보가 없으면 세션 스코리지에 게시글의 정보를 저장하고 글의 조회수 증가
    3. 세션에 게시글의 정보가 존재한다면 글의 조회수 증가시키지 않고 서비스 실행

    쿠키

    1. 조회 요청이 오면 HttpServletRequest의 쿠키에 해당 게시글의 정보가 있는지 확인
    2. 쿠키에 게시글 정보가 없으면 쿠키에 정보를 추가하고 조회수를 증가
    3. 쿠키에 게시글 정보가 있으면 조회수 증가없이 서비스 실행

    세션과 쿠키라는 매체만 달라졌을뿐 로직 자체는 동일하다.

     

    두번째, 코드 재활용성을 높이기 위한 방법으로는 AOP를 적용하여 전역으로 관리할 수 있도록 구현하였다.

    @Aspect
    @Component
    @Slf4j
    @RequiredArgsConstructor
    public class ViewCountAop {
    
        private final ViewCount viewCount;
    
        HttpServletRequest request;
        HttpServletResponse response;
        Cookie oldCookie;
        boolean isSetCookie;
        boolean isViewCountUp;
    
        @Before("@annotation(com.market.supercar.annotation.ViewCount) && args(brdSeq, ..)")
        private void before(final Long brdSeq) {
            oldCookie = null;
            isSetCookie = true;
            isViewCountUp = true;
    
            request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
            response = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
    
            Cookie[] cookies = Objects.requireNonNull(request).getCookies();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (cookie.getName().equals("boardView")) {
                        oldCookie = cookie;
                        break;
                    }
                }
            }
            // 중복 조회 case
            if (oldCookie != null && oldCookie.getValue().contains("[" + brdSeq + "]")) {
                isSetCookie = false;
                isViewCountUp = false;
            }
        }
    
        @AfterReturning("@annotation(com.market.supercar.annotation.ViewCount) && args(brdSeq, ..)")
        private void afterReturning(JoinPoint joinPoint, final Long brdSeq) {
    
            String domainName = joinPoint.toShortString().split("\\\\(")[1].split("Controller")[0];
            log.info("domainName : {}", domainName);
    
            if (isSetCookie) {
                // viewCountUp Process
                viewCount.setViewCount(domainName, brdSeq);
                if (oldCookie != null) {
                    // 저장된 쿠키는 있지만 새로운 페이지를 조회하는 case
                    oldCookie.setValue(oldCookie.getValue() + "[" + brdSeq + "]");
                    // 쿠키를 전송하기위한 웹서버의 URL 경로 지정
                    oldCookie.setPath("/");
                    // 기존 발행된 쿠키의 만료 시간을 리셋
                    oldCookie.setMaxAge(60*60*24);
                    Objects.requireNonNull(response).addCookie(oldCookie);
                } else {
                    // 저장된 쿠키가 없는 case
                    Cookie newCookie = new Cookie("boardView", "[" + brdSeq + "]");
                    // 쿠키를 전송하기위한 웹서버의 URL 경로 지정
                    newCookie.setPath("/");
                    // 쿠키의 만료 시간을 설정
                    newCookie.setMaxAge(60*60*24);
                    Objects.requireNonNull(response).addCookie(newCookie);
                }
            }
        }
    
    }
    
    @Component
    @RequiredArgsConstructor
    @Slf4j
    class ViewCount{
    
        private final MagazineRepository magazineRepository;
        private final ProductRepository productRepository;
        private final PaparazziRepository paparazziRepository;
    
        // viewCountUp Process
        @Transactional
        public void setViewCount(String domainName, Long brdSeq) {
            log.info("setViewCount");
            switch (domainName) {
                case "Magazine" :
                    log.info("magazine 실행");
                    Magazine magazine = magazineRepository.findById(brdSeq).orElseThrow(
                            () -> new CustomException(CustomErrorCode.PAGE_NOT_FOUND)
                    );
                    magazine.addViewCount(); break;
                case "Product" :
                    log.info("Product 실행");
                    Product product = productRepository.findById(brdSeq).orElseThrow(
                            () -> new CustomException(CustomErrorCode.PAGE_NOT_FOUND)
                    );
                    product.addViewCount(); break;
                case "Paparazzi" :
                    log.info("Paparazzi 실행");
                    Paparazzi paparazzi = paparazziRepository.findById(brdSeq).orElseThrow(
                            () -> new CustomException(CustomErrorCode.PAGE_NOT_FOUND)
                    );
                    paparazzi.addViewCount(); break;
                default: break;
            }
        }
    
    }
    

     

    횡단 관심사 패턴위치를 표시하기위해 @Before, @AfterReturning으로 나누어 사용하여 서비스 로직에서 예외가 발생시 조회수가 올라가는 것을 막아두었다. 하지만 정상 실행시 서비스 로직이 종료된 후 DB의 조회수가 올라가서 실제로 응답 하는 데이터에는 바로 반영이 안되는 문제점이 있다. 이 부분은 추후 리팩토링을 할 예정이다.

    /**
     *
     * 적용되는 method 의 가장 첫번째 parameter 는 해당 페이지의 번호가 되어야한다.
     * <p>
     * 해당 어노테이션 사용시 ViewCountAop 에서 호출하여 작업
     *
     */
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ViewCount {
    }
    

    viewCountAop 적용 위치를 간편하게 매하기 위해서 custom annotation을 구현 하였다.

     

    참고자료

    스스로 생각해보기!

    자료 조사를 하고 구현하는 과정에서 철저한 조회수 중복 방지를 위해서는 하나의 방식으로만은 안된다는 생각을 하게되었다. 현재 쿠키를 사용해 24시간내에 중복을 방지하게는 해두었으나 방지기능을 강화할 필요가 있다면 다른 방식도 함께 적용해야할텐데 이때 AOP를 통해 구현하였기에 한곳에만 추가 적용하면 되니 큰 장점으로 될 것 같다.

    댓글

Designed by Tistory.