[일정 관리 앱] 리팩토링(1)

2024. 10. 15. 14:18내일배움캠프/Schedule Management

 이전 게시물에서 말한 것 처럼 현재 모든 요구사항을 반영한 것이 아니다 '도전 기능' 에 해당하는 '세부 요구사항' 을 반영하지 않았기에 해당 요구사항들을 반영하면서 리팩토링을 진행해보려 한다. 과제 제출 전까지 진행할 리팩토링 순서는 아래와 같다.

  • 필터(인증/인가) → 유저(멤버) → 일정 → 나머지 엔티티
  • 기본적으로 적절한 클래스명, 메서드명, 변수명으로 수정 및 비즈니스 로직 수정

이번 게시글에서는 '필터' 에 초점을 맞춰 리팩토링을 진행하려 한다. 이번 리팩토링으로 인해 수정된 코드는 여기서 확인이 가능하다.

 

1. 필터

 필터를 사용하니 비즈니스 로직에서 '인증/인가' 를 분리할 수 있었다. 이번 프로젝트에서의 인증, 인가에 대한 예시는 아래와 같다.

  • 인증 : 일정을 생성하는 것은 회원(멤버)만이 가능하다.
  • 인가 : 일정을 수정하는 것은 관리자 권한을 가진 회원(멤버)만이 가능하다.

필터에서 요청에 따라 필요한 인증 및 인가를 수행, 이를 통과했다면 요청을 수행해 결과를 반환한다. 만약 통과하지 못했다면 요청을 수행하는 대신 요청에 필요한 인증정보 또는 권한이 없다는 내용을 반환하게 될 것이다.

 

구현한 필터 중 하나인 'AuthFilter' 는 '요청 URL' 에 따라 요청에 대한 '인증/인가' 를 수행한다. 이 부분은 요구사항에 따라 아래와 같이 적용하였다.

  • 인증 : 미수행 - 회원 가입, 로그인 || 수행 - 그 외
  • 인가 : 미수행 - 그 외 || 수행 - 일정 수정/삭제
@Slf4j
@Component
@Order(3)
@RequiredArgsConstructor
public class AuthFilter implements Filter {
    private final JwtUtil jwtUtil;
    private final MemberRepository memberRepository;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String httpMethod = httpServletRequest.getMethod();
        String url = httpServletRequest.getRequestURI();

        if (StringUtils.hasText(url)) {
            if (url.matches("/member/join") || url.matches("/member/logIn")) {
                chain.doFilter(request, response);

            } else if (url.startsWith("/schedule") &&
                    (httpMethod.matches("PUT") || httpMethod.matches("DELETE"))) {
                Claims claims = getClaimsFromRequest(httpServletRequest);

                Long memberId = Long.parseLong(claims.getSubject());
                String auth = claims.get(AUTH.getKey(), String.class);

                if (!auth.matches(ADMIN.getRole())) {
                    throw new HasNotPermissionException(HAS_NOT_PERMISSION);
                }

                Member member = memberRepository.findById(memberId)
                        .orElseThrow(() -> new NotFoundEntityException(NOT_FOUND_MEMBER));

                request.setAttribute("member", member);
                chain.doFilter(request, response);

            } else {
                Claims claims = getClaimsFromRequest(httpServletRequest);

                Long memberId = Long.parseLong(claims.getSubject());

                Member member = memberRepository.findById(memberId)
                        .orElseThrow(() -> new NotFoundEntityException(NOT_FOUND_MEMBER));

                request.setAttribute("member", member);
                chain.doFilter(request, response);
            }
        }
    }

    private Claims getClaimsFromRequest(HttpServletRequest httpServletRequest) {
        String token = jwtUtil.getTokenFromRequest(httpServletRequest);

        jwtUtil.checkTokenValidity(token);

        return jwtUtil.getPayload(token);
    }
}

 

 

2. 필터에서의 예외 핸들링

 기존에 작성해둔 'GolbalExceptionHandler' 는 '@RestControllerAdvice' 어노테이션을 지정해 'Controller' 단에서 잡히는 예외를 핸들링 해준다. 그렇기에 클라이언트의 요청을 최초/최후로 다루는 '필터' 의 경우 예외 핸들링을 따로 해줄 필요가 있었다.

 

기존의 필터 체인이 '로깅 필터 → 인증/인가 필터' 이었는데 필터의 예외를 핸들링하기 위해 필터 체인을 '로깅 필터 → 예외 핸들링 필터 → 인증/인가 필터' 로 수정하였고 아래와 같이 'ExceptionHandleFilter' 를 추가해 주었다.

@Component
@Order(2)
public class ExceptionHandleFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        try {
            chain.doFilter(request, response);

        } catch (NotValidTokenException e) {
            setExceptionToResponse(httpResponse, e.getExceptionCode());

        } catch (HasNotPermissionException e) {
            setExceptionToResponse(httpResponse, e.getExceptionCode());

        } catch (NotFoundEntityException e) {
            setExceptionToResponse(httpResponse, e.getExceptionCode());
        }
    }

    private void setExceptionToResponse(HttpServletResponse httpServletResponse, ExceptionCode exceptionCode) throws IOException {
        ObjectMapper objectMapper = new ObjectMapper();
        httpServletResponse.setStatus(exceptionCode.getHttpStatus().value());
        httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

        ResponseExceptionCode responseExceptionCode = ResponseExceptionCode.builder()
                .code(exceptionCode.name())
                .message(exceptionCode.getMessage())
                .build();

        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(responseExceptionCode));
    }
}

 

현재 'AuthFilter' 에서 터질 수 있는 예외는 아래와 같이 파악하고 해당 예외들을 try-catch 문으로 핸들링해 주었다.

  • '토큰' 의 유효성에 대한 예외 - 토큰 만료, 토큰이 없는 경우, 발급 토큰이 아닌 경우, 지원하지 않는 토큰인 경우
  • '권한' 에 대한 예외 - 요청에 대한 유저의 권한이 미달인 경우
  • 토큰 정보로 조회한 '유저(멤버)' - 토큰은 유효한데 토큰에 담긴 유저 정보로 유저를 DB 에서 조회시 유저 정보가 없는 경우

API 테스트를 보면 지정한 예외가 잘 핸들링 되는 것을 확인할 수 있었다.

요청 헤더에 토큰이 존재하지 않는 경우
요청 헤더의 토큰이 유효하지 않은 경우
요청 헤더의 토큰이 만료된 경우
토큰에 담긴 유저의 권한이 요청 수행에 필요한 권한 미달인 경우

 

각 예외에 대한 HttpStatus 는 요구사항을 따라 지정하였다.