이 포스팅은 신선영 저, 『스프링 부트 3 백엔드 개발자 되기』(골든래빗, 2023)를 공부하면서 마주한 에러 및 버그를 해결하는 과정을 기록하기 위해서 작성되었습니다.
'10장. OAuth2로 로그인/로그아웃 구현하기'의 코드를 실행했는데 다음과 같은 컴파일 에러가 뜨며 앱이 실행되지 않았다.
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| webOAuthSecurityConfig defined in file [C:\Users\...]
↑ ↓
| userService defined in file [C:\Users\...]
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
circular references에 대해 검색해보니, 두 개 이상의 Bean이 서로를 참조하는 '순환 참조' 를 의미한다고 한다. 나의 경우에는 webOAuthSecurityConfig가 userService를 참조하고, 동시에 userService가 webOAuthSecurityConfig를 참조하는 상황이 벌어진 것이다.
순환 참조가 발생했을 때 앱이 실행되지 않는 이유는 의존성을 주입하는 과정에서 무한 루프에 걸려 무한정으로 대기하기 때문이다. A가 B에 의존하는 경우에는 B의 빈이 생성되고 난 다음에 A의 빈이 생성된다. 그런데 A와 B가 동시에 서로 의존하는 경우에는 A와 B가 서로 먼저 생성되기를 무한히 기다리게 된다.
webOAuthSecurityConfig와 userService가 서로 어떻게 참조하고 있는지 확인해보기로 했다.
다음은 클래스 WebOAuthSecurityConfig의 코드 중 일부이다. 클래스 UserService에 의존하고 있음을 알 수 있다.
@RequiredArgsConstructor
@Configuration
public class WebOAuthSecurityConfig {
private final OAuth2UserCustomService oAuth2UserCustomService;
private final TokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;
private final UserService userService;
// *** 생략 *** //
@Bean
public OAuth2SuccessHandler oAuth2SuccessHandler() {
return new OAuth2SuccessHandler(
tokenProvider, refreshTokenRepository,
oAuth2AuthorizationRequestBasedOnCookieRepository(),
userService
);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
다음은 클래스 UserService의 코드이다.
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public Long save(AddUserRequest request){
return userRepository.save(User.builder()
.email(request.getEmail())
.password(bCryptPasswordEncoder.encode(request.getPassword()))
.build()).getId();
}
// *** 생략 *** //
}
클래스 UserService는 클래스 WebOAuthSecurityConfig에 직접적으로 의존하고 있지 않는 듯 보인다. 그렇다면 왜 두 클래스 사이에 순환참조가 발생했던 것일까?
두 클래스가 공통적으로 지니고 있는 클래스 BCryptPasswordEncoder에 눈길이 갔다. 클래스 WebOAuthSecurityConfig에서는 BCryptPasswordEncoder의 생성자를 선언했고, 클래스 UserService는 BCryptPasswordEncoder를 참조하고 있다. 혹시 이 클래스를 매개로 하여 두 클래스 간에 순환참조가 발생했던 것이 아닐까?
그래서 클래스 WebOAuthSecurityConfig에 있는 BCryptPasswordEncoder의 생성자를 없애보고 앱을 실행해보았다. 그랬더니 다음과 같은 컴파일 에러가 발생했다.
Parameter 1 of constructor in MyCompany.MyBlog.service.UserService required a bean of type 'org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder' that could not be found.
클래스 UserService에서 참조하고 있는 빈인 BCryptPasswordEncoder을 찾을 수 없다고 한다. 클래스 UserService는 클래스 WebOAuthSecurityConfig에서 생성자를 통해 생성된 빈인 BCryptPasswordEncoder를 참조하고 있었던 것이다. UserService는 BCryptPasswordEncoder를 사용하기 위해 WebOAuthSecurityConfig를 참조해야만 했고, WebOAuthSecurityConfig는 UserService를 명시적으로 참조하고 있는 관계로 두 클래스 사이에 순환참조 관계가 형성되었던 것이다.
교재의 코드가 올라와있는 깃허브에서는 클래스 UserService를 다음과 같이 수정함으로써 순환참조를 방지했다.
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
public Long save(AddUserRequest request){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return userRepository.save(User.builder()
.email(request.getEmail())
.password(encoder.encode(request.getPassword()))
.build()).getId();
}
// *** 생략 *** //
}
BCryptPasswordEncoder의 생성자를 save라는 메서드의 내부에서 선언함으로써 WebOAuthSecurityConfig에서 선언한 BCryptPasswordEncoder의 생성자를 참조하지 않는 전략을 사용했다.
순환 참조를 방지하기 위한 또다른 아이디어가 떠올라서 한번 실행해보기로 하였다.
바로 BCryptPasswordEncoder를 WebOAuthSecurityConfig에서 생성하지 않고 별도의 클래스로 빼놓는 것이다. BCryptPasswordEncoder는 패스워드를 암호화하는 기능을 가지고 있다. 이 기능은 회원가입을 할 때 뿐만 아니라 추후에 비밀번호를 변경할때에도 사용될 기능이다. 그래서 이 기능을 담당하는 클래스를 별도로 만들면 순환 참조도 발생하지 않을 것이고 확장성도 용이하다.
@RequiredArgsConstructor
@Component
public class PasswordEncoder {
public String encode (String password){
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder.encode(password);
}
}
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public Long save(AddUserRequest request){
return userRepository.save(User.builder()
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.build()).getId();
}
// *** 생략 *** //
}
이와 같은 방식으로 코드를 수정하고 앱을 실행시켰더니 잘 되었다!
참고 자료
https://curiousjinan.tistory.com/entry/spring-circular-references