[리팩터링] JWT 구조 개선

Jayson·2025년 3월 9일
post-thumbnail

이번에는 큰 틀에서 auth 관련을 개선하면서 JWT 부분을 개선해보고자 한다.
각 클래스별로 의존성들을 확인해보고, 리뷰어가 이해하기 쉽고, 유지보수하기 좋은 형태로 개선하고자 노력해보자. (이번 기회에 레거시 코드도 읽어보고 ,, 좋다)
굵직한 부분들을 위주로 분석해보고 자세한 사항은 PR을 참고하여 변경사항을 보면 좋을 것 같다.

먼저 이전 코드(레거시)의 형태를 봐보자.. (부끄럽다)

1. CustomJwtAuthenticationEntryPoint

이 클래스는 Spring Security에서 인증되지 않은 사용자가 보호된 리소스에 접근할 때 실행되는 인증 진입점 역할을 한다.

주요 역할

  1. Spring Security의 AuthenticationEntryPoint 구현

    • AuthenticationException이 발생했을 때, 클라이언트에게 401 Unauthorized 응답을 반환한다.
    • JWT가 유효하지 않거나 인증이 필요한 경우 실행된다.
  2. JSON 형식의 에러 응답 처리

    • CustomException(INVALID_TOKEN) 객체를 JSON 형식으로 변환해 응답.
    • 클라이언트가 쉽게 처리할 수 있도록 UTF-8 및 application/json 응답을 설정.
  3. 예외 발생 시 자동으로 실행

    • commence() 메서드는 AuthenticationException이 발생할 경우 실행되며, setResponse()를 호출하여 응답을 설정한다.

문제점 및 개선할 점

  1. CustomException을 직접 응답 객체로 사용

    • 예외 객체(CustomException)를 JSON 변환하여 클라이언트에 응답하지만, 예외 객체는 일반적으로 프로그램 내에서만 사용되는 것이 바람직하다.

    • API 응답을 담당하는 DTO (예: ErrorResponse) 없이, 예외 객체를 직접 JSON으로 변환하는 것은 설계적으로 부적절하다.

  2. ObjectMapper를 통한 수동 JSON 변환

    • ObjectMapper.writeValueAsString()을 사용해 직접 JSON 변환을 수행하는데, 이는 Spring의
    • ResponseEntity나 @ExceptionHandler를 활용하면 자동으로 JSON 변환이 가능하다. 이를 개선하면 중복 코드 감소 및 유지보수성이 증가한다.
  3. 예외 처리를 CustomJwtAuthenticationEntryPoint 내부에서 처리

    • commence() 메서드에서 예외 발생 시 직접 HttpServletResponse를 조작하여 응답을 설정하는 방식은 유지보수성이 떨어진다.
    • 예외 처리를 별도의 예외 처리 핸들러 (GlobalExceptionHandler) 로 이동하는 것이 좋다.
  • 기존 코드 흐름
  1. AuthenticationException 발생 시 commence() 실행.
  2. setResponse()에서 JSON 응답을 직접 생성.
  3. 예외 응답이 JSON 형태로 반환됨.

개선된 코드 분석

개선 사항

1. 예외 처리를 전역 예외 처리 핸들러 (GlobalExceptionHandler)로 위임

  • CustomJwtAuthenticationEntryPoint에서 직접 응답을 처리하지 않고, 예외를 던지는 방식으로 변경 (throw new JwtException(JwtErrorCode.INVALID_JWT_TOKEN)).
  • Spring의 @RestControllerAdvice@ExceptionHandler를 사용하여 전역 예외 처리를 담당하도록 개선.

2. JwtException 클래스 도입 및 JwtErrorCode Enum 활용

  • JWT 관련 예외를 JwtException으로 한 곳에서 통합 관리.
  • JwtErrorCode Enum을 활용하여 에러 코드 및 메시지를 일관되게 정의.
  • API 응답 메시지가 일관된 형식을 유지할 수 있도록 개선.

3. ObjectMapper 제거 및 ResponseEntity 활용

  • HttpServletResponse를 직접 조작하는 대신, Spring의 ResponseEntity를 사용하여 JSON 응답을 자동 변환.
  • JwtException을 던지면 전역 예외 핸들러에서 이를 감지하고 자동으로 처리.

개선 후 코드 흐름

  1. AuthenticationException 발생 시 commence() 실행.
  2. JwtException(JwtErrorCode.INVALID_JWT_TOKEN) 예외 발생.
  3. GlobalExceptionHandler에서 JwtException을 감지하고 적절한 HTTP 응답을 반환.

2. JwtAuthenticationFilter

이 클래스는 Spring Security에서 JWT 기반 인증을 수행하는 필터로,
클라이언트 요청에서 JWT 토큰을 추출하고 검증하여 인증을 처리하는 역할을 한다.

문제점

  1. 예외 처리가 적절하지 않음

    • try-catch 블록에서 예외 발생 시 로깅만 수행하고 예외를 적절히 처리하지 않음.
    • 예외 발생 시 보안 컨텍스트를 설정하지 않고 그대로 요청을 진행함.
  2. JWT 검증 로직의 분리 부족

    • jwtTokenProvider.validateToken(token)을 직접 호출하여 검증 수행.
    • getUserId(token)에서 jwtTokenProvider.getUserFromJwt(token)을 호출해 사용자 ID를 추출하는데, 검증과 사용자 ID 추출이 하나의 클래스에서 이루어짐.
  3. 토큰 추출 로직이 복잡함

    • getAccessTokenFromRequest()isContainsAccessToken()getAuthorizationAccessToken()getTokenFromBearerString() 형태로 복잡하게 구성되어 있음.
    • val을 사용하여 가독성이 떨어질 수 있음.
  4. 하드코딩된 상수 사용

    • "Bearer " 문자열을 ValueConfig.BEARER_HEADER에서 관리하지만, getTokenFromBearerString()에서 직접 문자열을 다룸.

개선된 코드 분석

개선 사항

1. 예외 처리 방식 개선

  • try-catch 블록을 제거하고 Optional을 활용하여 예외 발생 시 코드가 자연스럽게 처리되도록 변경.
  • JWT 검증 및 사용자 ID 추출 과정에서 예외 발생 시 흐름을 중단하지 않고 적절히 관리.

2. JWT 검증 및 사용자 ID 추출 로직 분리

  • JwtTokenVerifier 클래스를 사용하여 JWT 검증 및 사용자 ID 추출을 한 곳에서 처리.
  • validateAndExtractUserId() 메서드를 통해 검증과 사용자 ID 추출을 동시에 수행.

3. 토큰 추출 로직 단순화

  • extractToken(request) 메서드를 추가하여 토큰을 단순하게 추출.
  • Optional<String>을 사용하여 null 처리를 자연스럽게 해결.
  • map(token -> token.replaceFirst("Bearer ", "").trim())을 활용하여 불필요한 메서드 체이닝 제거.

4. 사용자 인증 로직 개선

  • authenticateUser(Long userId) 메서드에서 UserDetailsFactory를 사용하여 사용자 객체 생성.
  • UserAuthentication.authenticated(userDetails)를 활용하여 명확한 인증 객체 생성.

3. JwtTokenProvider

개선 이전 코드 분석

문제점

  1. JWT 키 생성 로직이 JwtTokenProvider에 직접 포함됨

    • getSigningKey() 메서드에서 키를 생성하는데, 이는 JWT 검증 및 생성 로직과 분리되지 않아 단일 책임 원칙(SRP)에 위배됨.
  2. JWT 발급, 검증, 클레임 생성이 한 클래스에서 이루어짐

    • generateToken(), validateToken(), getUserFromJwt()가 같은 클래스(JwtTokenProvider)에 존재하여 관심사가 분리되지 않음.
    • JWT 관련 책임을 나누어 재사용성과 유지보수성을 높일 필요가 있음.
  3. 예외 처리가 일관되지 않음

    • validateToken()에서 예외를 log.error(exception.getMessage());로 기록 후 단순히 JwtValidationType을 반환.
    • 예외 처리를 따로 관리하는 클래스(ExceptionHandler)가 존재하지 않아 일관된 예외 처리 및 응답이 어렵다.
  4. 하드코딩된 값이 포함됨

    • claims.put("userId", authentication.getPrincipal()); 등의 값이 하드코딩되어 있음.
    • JWT 클레임 키를 별도의 상수나 Enum으로 관리하는 것이 더 바람직함.

개선된 코드 분석

개선 사항

1. JWT 키 생성 로직 분리 (JwtKeyProvider 도입)

  • JwtKeyProvider 클래스를 추가하여 JWT 키 생성 역할을 별도로 분리.
  • JwtTokenProvider에서 JWT 서명 키를 직접 생성하는 것이 아니라, JwtKeyProvider.getSigningKey()를 통해 키를 가져오도록 변경.
  • 이는 JWT 검증과 키 관리 책임을 분리하여 유지보수성을 향상.

2. JWT 발급, 검증, 클레임 생성 역할 분리

  • JwtSigner: JWT 서명 및 발급 담당.

  • JwtClaimsGenerator: 클레임 생성 담당.

  • JwtTokenIssuer: 토큰 발급을 담당하는 상위 모듈로 변경.

  • JwtTokenManager: 실제 서비스에서 JWT 토큰을 발급 및 관리하는 역할 수행.

  • 이점:

    • 각 클래스가 하나의 역할만 수행하도록 분리하여 단일 책임 원칙(SRP) 준수.
    • 모듈 간 재사용성과 테스트 용이성 증가.

3. 예외 처리 로직 개선

  • JwtValidationType을 반환하는 방식 대신 예외를 던지는 방식으로 변경.
  • JwtExceptionGlobalExceptionHandler를 활용하여 일관된 예외 처리를 보장.
  • JWT 관련 오류(INVALID_JWT_TOKEN, EXPIRED_JWT_TOKEN 등)를 JwtErrorCode Enum으로 정의하여 예외 메시지를 일관되게 유지.
profile
Small Big Cycle

0개의 댓글