chae._.chae

[Error] 순환참조 오류 - The dependencies of some of the beans in the application context form a cycle 본문

Error

[Error] 순환참조 오류 - The dependencies of some of the beans in the application context form a cycle

walbe0528 2022. 11. 11. 23:15
728x90
반응형

 

application context에서 일부 Bean의 순환 참조 문제가 발생했다.

 

순환 참조 문제는 둘 이상의 Bean이 생성자를 통해 서로 주입하려고 할 때 발생한다.

즉, 클래스 A에서 생성자 주입을 통해 클래스 B의 인스턴스가 필요하고,

클래스 B는 생성자 주입을 통해 클래스 A의 인스턴스가 필요한 상황인 것이다. 

 

@Component
public class AComponent {
    @Autowired
    private BComponent bComponent;

}
@Component
public class BComponent {
    @Autowired
    private AComponent aComponent;
}

이런 상황이다. 

 

 

내가 작성한 코드인데, SecurityConfig와 PrincipalOauth2UserService에서 서로 주입하려 해서 문제가 발생했다. 

  • SecurityConfig.class
@AllArgsConstructor
@Configuration
@EnableWebSecurity 
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private PrincipalOauth2UserService principalOauth2UserService;

    @Bean  // 해당 메소드의 리턴되는 오브젝트를 IoC로 등록해준다.
    public BCryptPasswordEncoder encodePwd(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/user/**").authenticated()
                .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
                .anyRequest().permitAll()
                .and()
                .formLogin()
                .loginPage("/loginForm")
                .loginProcessingUrl("/login") 
                .defaultSuccessUrl("/")  
                .and()
                .oauth2Login()
                .loginPage("/loginForm")
                .userInfoEndpoint()
                .userService(principalOauth2UserService);  // 소셜 로그인 후처리작업 진행

    }
}

 

 

  • PrincipalOauth2UserService.class
@Service  // 로그인 완료후 진행되는 후처리 작업
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    private UserRepository userRepository;

    // loadUser : 구글로부터 받은 userRequest 데이터에 대한 후처리되는 함수
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        System.out.println("userRequest: " + userRequest);  // userRequest: org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest@703cff7b
        System.out.println("userRequest.getClientRegistration(): " + userRequest.getClientRegistration());  // registrationId로 어떤 OAuth로 로그인했는지 확인 가능
        System.out.println("userRequest: " + userRequest.getAccessToken().getTokenValue());

        OAuth2User oauth2User = super.loadUser(userRequest);
        // 구글로그인 완료시 code를 리턴받는다. -> (OAuth-Client라이브러리가 받아준다) -> 코드를 통해 Access Token 요청
        // userRequest 정보로 회원프로필을 받는다.(이때 사용하는 함수가 loadUser)

        // 강제로 회원가입을 진행
        String provider = userRequest.getClientRegistration().getRegistrationId();  // "google"
        String providerId = oauth2User.getAttribute("sub");  // 구글의 sub
        String username = provider + "_" + providerId;
        String password = bCryptPasswordEncoder.encode("시큐리티");
        String email = oauth2User.getAttribute("email");
        String role = "ROLE_USER";

        User userEntity = userRepository.findByUsername(username);

        if (userEntity == null){  // 유저 찾지 X, 강제로 회원가입 진행
            userEntity = User.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();
            userRepository.save(userEntity);
        }

        return new PrincipalDetails(userEntity, oauth2User.getAttributes());
        // PrincipalDetails가 만들어져서 Authentication 객체 안에 들어간다.
    }
}

 

Spring은 Application Context의 시작 과정에서 모든 싱글톤 Bean을 즉시 생성하는데, 런타임이 아닌 컴파일 과정에서 모든 가능한 오류를 감지하기 위해서이다.

 

PrincipalOauth2UserService에서 생성자에 @Lazy어노테이션을 적어 해결해주었다. 

@Autowired
private @Lazy BCryptPasswordEncoder bCryptPasswordEncoder;

이처럼 문제가 되는 빈들을 주입 받을 때, @Lazy 어노테이션을 사용해서 지연로딩으로 문제를 해결해준다.

(근데 이제 임시방편의 해결책이라는,,)

설계의 오류로 발생한 문제이기에, 가장 이상적인건 스프링 빈들의 설계를 다시 해서 문제를 해결해주는 것이다.

 

A -> B, B -> A의 의존관계가 문제가 된다면,

새로운 C를 만들어서 A -> B, B -> C로 문제를 해결해준다. 

728x90