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);
}
@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("일치하는 토큰이 없음.") {
};
}
}
}
@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());
}
}
@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);
}
}
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를 주입받고 있게 되는 형태로 바뀌어 스프링 빈 순환참조 문제를 해결하였습니다.