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 는 요구사항을 따라 지정하였다.
'내일배움캠프 > Schedule Management' 카테고리의 다른 글
[일정 관리 앱] 리팩토링(3) (0) | 2024.10.15 |
---|---|
[일정 관리 앱] 리팩토링(2) (0) | 2024.10.15 |
[일정 관리 앱] 도전 기능 요구사항 반영 (0) | 2024.10.14 |
[일정 관리 앱] N : M(다대다) 관계 풀어내기 (0) | 2024.10.12 |
[일정 관리 앱] 기묘한 모험 - 1 : N 관계에서의 전체 조회 (0) | 2024.10.11 |