Circular Dependency 문제가 두달전에도 한번 발생했던 것 같은데, 어제도 발생해서 두시간정도 헤맸던 것 같다. 이번이 문제의 원인을 분석하기 좀 더 어려웠달까..? (사실 쉬웠는데 왜 못찾았는지 나 자신도 이해 못함)
그래서 이번기회에 확실히 알고 넘어가려고 한다.
먼저 Spring Container의 생성 과정에 대해 간략히 알아보자.
우리는 컨테이너에 등록하고자하는 객체들을 @Bean
이라는 어노테이션으로 등록하거나,
Config 설정하는 경우가 수동으로 등록하는 경우이다.
@Bean
만 등록하게 되면 싱글톤 보장이 안되므로,@Configuration
어노테이션이 함께 사용함을 잊지 말자)`
@Component
가 포함된 여러 어노테이션으로 스프링 빈을 등록한다.
컨테이너는 등록되어진 Bean들의 Dependency를 Injection까지 하는 과정을 거치게 된다.
하지만 이때, 등록된 Bean 자체가 생성자 주입(Constructor Injection)으로 다른 객체를 주입하는 것이라면, 1번 2번의 과정은 한번에 일어나게 됨을 유의하자.
다음 과정에서 문제가 생기는 부분은 당연히 2번의 과정들일 것이다.
스프링 빈으로 컨테이너에 등록이 되고, Dependency Injection을 거치는 과정에서 Circular Dependency가 생겨버리는 것이다.
내가 맞이한 상황은 이렇다.
이렇게 세개가 맞물려 있었다.
정상적인 상황이러면 이럴것이다.
등록된 Bean들의 이름을 A,B,C라고 하면, 세개의 객체들을 스프링 빈에 등록하고, Dependency가 맞물려있을때,
A - B - C
순의 단방향의 의존관계가 존재한다면 OK이지만,
A - B - C - A
와 같이 의존관계가 엮어져있으면, 컨테이너는 의존관계 주입을 할 때, 무엇부터 시작해야하지? 라는 난관에 봉착하게 된다. 나의 상황도 그러하였다.
그럼 파일 하나 하나를 살펴보자.
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final TokenProvider tokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
//OAuth2LoginConfig에서 @Configuration으로 등록된 bean 주입
private final ClientRegistrationRepository clientRegistrationRepository;
private final OAuthService oAuthService;
private final OAuthAuthenticationSuccessHandler oAuthAuthenticationSuccessHandler;
@Value("${jwt.domain}") private String domain;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// 생략 ..
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
// 생략 ..
}
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
// 생략 ..
}
}
네가지 Bean정보가 있고, 생성자 주입으로 많은 객체를 받고 있는 상태이다.
@Slf4j
@Service
@RequiredArgsConstructor
public class OAuthService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final MemberRepositoryImpl memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 생략 ..
}
}
}
여기에서는 MemberRepositoryImpl 객체만 생성자 주입을 받았다.
@Slf4j
@Repository // 인터페이스 구현체를 바꿀것 같지 않으므로 스프링 빈을 직접 등록하는 것이 아닌, 컴포넌트 스캔방식으로 자동의존관계설정
public class MemberRepositoryImpl implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
private final PasswordEncoder passwordEncoder;

public MemberRepositoryImpl(DataSource dataSource, PasswordEncoder passwordEncoder){
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.passwordEncoder = passwordEncoder;
}
// 생략 ..
}
여기에서는 JdbcTemplate과 PasswordEncoder를 주입받는다.
해결하기 위해 어디를 봐야했을까?
MemberRepositoryImpl
과 SecurityConfig
객체들이 어떤 객체를 서로 의존하고 있는지 봐야했다.
공통점은 단 한가지인 PasswordEncoder
객체이다.
스프링 컨테이너는 MemberRepositoryImpl
을 의존관계 주입(Constructor Injection)하면서 동시에 생성 및 컨테이너에 등록을 해야하는데, PasswordEncoder
를 @Bean
으로 등록한 SecurityConfig 파일이 먼저 등록이 되지 않으면, 누굴 먼저 생성할 것인지 알 수가 없다.
그래서 우리는
"SecurityConfig 너 먼저 생성해줘." 를 하거나,
@Bean
으로 등록한 PasswordEncoder 정보를 따로 빼서 주입받게끔 하면 될 것이다.
@Slf4j
@Repository // 인터페이스 구현체를 바꿀것 같지 않으므로 스프링 빈을 직접 등록하는 것이 아닌, 컴포넌트 스캔방식으로 자동의존관계설정
public class MemberRepositoryImpl implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
private final PasswordEncoder passwordEncoder;
public MemberRepositoryImpl(DataSource dataSource, @Lazy PasswordEncoder passwordEncoder){
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.passwordEncoder = passwordEncoder;
}
// 생략 ..
}
PasswordEncoder
를 @Lazy
어노테이션과 함께 작성하여 실질적으로 참조할 Bean이 존재할 때, 스프링 빈으로 등록하여, SecurityConfig 내에서 PasswordEncoder 객체가 등록된 후에 생성이 되게끔 한다.
사실 이 부분으로도 해보려고 했으나, 이유는 모르겠는데, 되지 않는다.
다음에 찾아보려고 한다.
Setter Injection으로 변경을 통해서 해결할 수 있는 이유는, 생성자 주입을 통해 Bean으로 등록하게 되면 컨테이너 객체에 등록함과 동시에 Dependency Injection의 과정을 거치므로, Circular Dependency가 생기는데, setter injection을 사용하면, 필요할 때만 사용이 가능하므로, 해결할 수 있을 것으로 생각된다.
Setter Injection은 Config 설정 정보에만 쓰는 것이 좋다. 이유는 다음과 같다.
- Immutable 하지 않음
final 키워드로 선언할 수 없으므로, 외부에서 마음대로 객체를 바꿀 수 있다.- DI 없으면 아무것도 못함
결국 Spring Container에 종속적이게 된다. (자바 코드가 아닌, 컨테이너에 종속적)
@Configuration
public class PasswordConfig {
// PasswordEncoder는 BCryptPasswordEncoder를 사용
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
이렇게 아예 다른 Config파일로 빼버리게 되면,
의존관계가 명확해진다.
Security Config는 PasswordConfig에 의존적이지 않게 되면서,
MemberRepositoryImpl만 PasswordConfig에 의존하게 된다.
이를 통해 해결할 수 있게 되었다.