스프링 빈 순환참조 문제

김동훈·2023년 3월 31일
0

reDuck

목록 보기
1/5
post-thumbnail


refresh token을 통해 access token을 재발급 하는 로직을 짜던중 위와 같은 에러를 마주했습니다. 둘 이상의 빈이 생성자를 통해 서로를 주입할 때 발생합니다. 실제로 제 프로젝트에서 JwtProvider 클래스에서 JwtService를, JwtService에서 JwtProvider를 @RequiredArgsConstructor 어노테이션을 사용하여 서로 주입하고 있었습니다.
@Lazy 어노테이션 등을 사용하거나, 순환참조를 허용하는 설정을 통해 위 에러를 해결 할 수는 있긴 하지만 근본적으로는 컴포넌트 설계가 잘못되었다고 생각하여 다시 설계하는 방향으로 택했습니다.

초기 상태라 코드가 더러운 점 양해부탁드립니다...ㅎ

기존 설계 형태

Controller(access token 재발급 요청) -> JwtService의 refreshAccessToken함수 호출 -> JwtProvider의 refreshAccessToken함수 호출 -> JwtService의 getRefreshToken함수 호출 -> 새로운 access token 발급.

아래 코드들은 함수 호출 순서대로 작성하였습니다.

@GetMapping("/user/{userId}/token")
    public ResponseEntity<AccessTokenDto> refreshAccessToken(HttpServletRequest request, @PathVariable("userId") String userId) throws Exception {
        return new ResponseEntity<>(jwtService.refreshAccessToken(request, userId), HttpStatus.OK);
    }
  1. access token이 만료되어 재발급 요청이 들어오면, JwtService의 refreshAccessToken을 호출합니다.
@Service
@RequiredArgsConstructor
public class JwtService {
    private final JwtRepository jwtRepository;
    private final UserRepository userRepository;
    private final JwtProvider jwtProvider;
    
    @Transactional
    public AccessTokenDto refreshAccessToken(HttpServletRequest request, String userId) throws Exception {

        User user = userRepository.findByUserId(userId).orElseThrow(() -> new Exception("계정을 찾을 수 없습니다."));
        try {
            return AccessTokenDto.builder()
                    .accessToken(jwtProvider.refreshAccessToken(request, user))
                    .build();
        } catch (NoSuchElementException e) {
            throw new AuthenticationException("일치하는 토큰이 없음.") {
            };
        }
    }
}
  1. JwtProvider의 refreshAccessToken을 호출하여 새로운 access token을 return 합니다.
@RequiredArgsConstructor
@Component
public class JwtProvider {
    private final JwtService jwtService;
	public String refreshAccessToken(HttpServletRequest request, User user ) throws Exception {
        String refreshToken = resolveToken(request);
        validateToken(refreshToken);

        RefreshToken findRefreshToken = jwtService.getRefreshToken(user.getId());
        if(!findRefreshToken.equals(refreshToken)){
            throw new NoSuchElementException("일치하지 않는 refresh token입니다.");
        }

        String userId = getAccount(refreshToken.split(" ")[1].trim());
        return createToken(userId, user.getRoles());

    }
}
  1. 요청의 헤더에 담긴 request token의 유효성을 검사하고 JwtService의 getRefreshToken을 호출하여 Database에서 user의 refresh token을 가져오고, 일치하다면 새로운 access token을 발급합니다.
@Service
@RequiredArgsConstructor
public class JwtService {
	@Transactional
    public RefreshToken getRefreshToken(Long userPKId) throws Exception {
        List<RefreshToken> allByUserPKId = jwtRepository.findAllByUser_Id(userPKId);
        return allByUserPKId.get(allByUserPKId.size()-1);
    }
}
  1. Database에서 user의 refresh token을 찾아서 리턴합니다.

JwtService클래스와 JwtProvider클래스를 보면 @RequiredArgsConstructor로 서로 주입하고 있습니다.

수정후 설계 형태

약간의 함수 네이밍 수정도 있었습니다.

Controller(access token 재발급 요청) -> JwtService의 reissuanceAccessToken함수 호출 -> JwtService의 getRefreshToken함수 호출 -> JwtProvider의 createToken함수 호출 -> 새로운 access token 발급.




결론

JwtProvider는 jwt관련 키나 토큰등을 발급하는 기능을 담아놓은 클래스라고 생각했고, JwtService에서 jwt관련 로직을 처리해야한다고 결정했습니다. 그래서 JwtProvider에서 JwtService를 참조하는 관계를 풀었고, access token을 재발급 한다는 것 역시 access token을 생성하는 것과 똑같은 의미이므로 JwtProvider클래스의 refreshAccessToken함수를 없앴습니다. JwtService에서만 JwtProvider를 주입받고 있게 되는 형태로 바뀌어 스프링 빈 순환참조 문제를 해결하였습니다.

profile
董訓은 영어로 mentor

0개의 댓글