Spring

Circular Dependency

728x90

토이 프로젝트 하면서 다음과 같은 문제가 생겼었다

 

spring security 를 도입하면서 UserDetailsService 인터페이스를 상속받아서 

loadUserByUsername 을 오버라이드 해야 했는데 그 과정에서 다음과 같은 에러가 났었다.

 

 

 

- Circular Dependency

: 순환 종속, Spring 의 Bean 이 어떤 다른 Bean 에 대해서 의존성 주입을 받을 때 서로가 서로를 의존성 주입하는 과정에서 발생하는 에러이다.

만약 Bean A, B 가 있다고 했을때 

Bean A 가 B 를 의존성 주입 받고, B가 A 를 의존성 주입받으면 이런 문제가 발생한다

 

 

 

- 스프링 내부 작업

Spring Context 가 실행되고 Bean 들을 생성할때 다음과 같은 순서로 생성한다

예를들어 (순환 종속성이 없다는 가정하에)

Bean A -> Bean B -> Bean C

(Bean A 가 B 를 의존성 주입 받은 상태고, B 가 C 를 의존성 주입 받은 상태라 할때)

 

먼저 아무런 의존성 주입을 받지 않고 존재하는 C 를 먼저 생성하고

그 다음 B 를 생성한 뒤에 C 를 B 에 의존성 주입 시키고

그 다음 A 를 생성한 뒤에 B 를 A 에 의존성 주입 시키는 순서를 가지며 Bean 을 생성한다

 

그래서 순환 종속성 상황이 되었다면 Spring 이 어떤 Bean 을 먼저 생성해야 하는지 결정하지 못하고 

BeanCurrentlyInCreationException 에러가 뜨는 것이다.

 

 

 

- 해결책

1. 재설계

가장 좋은 해결책은 Bean 들의 의존성 관계를 분석해보고

어떻게 하면 상호간의 의존성 관계를 해소할 수 있는지 찾아내서

다시 설계 하는게 가장 좋은 방법이다

 

예를들어 내가 마주한 문제에선 코드를 다음과 같이 개선해보려했다

 

 

* 문제가 생겼던 코드

- WebSecurity.java

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {
 
    private final UserService userService;
 
    // 나머지 생략...
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder());
    }
 
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
cs

 

- UserService.java

public interface UserService extends UserDetailsService {
    // 기타 메소드 생략...
}
cs

 

- UserServiceImpl.java

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
 
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
 
    // 기타 메소드 생략 ......
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(username);
 
        if (user == null)
            throw new UserServiceException("user not exist !");
 
        return new org.springframework.security.core.userdetails.User(
                user.getEmail(),
                user.getEncryptedPwd(),
                true,
                true,
                true,
                true,
                new ArrayList<>()
        );
    }
}
cs

 

이 코드들의 문제점은 

먼저 AuthenticationManagerBuilder 에 userDetailsService 를 제시해줘야하고  

이에 대한 처리를 위해 UserService 가 UserDetailsService 를 상속 받아서 

UserServiceImpl 클래스에서 loadUserByUsername 을 재정의 하였는데

 

회원가입시에 비밀번호 암호화 처리를 위해 BCryptPasswordEncoder 를 Bean 으로 등록해서 사용해야 했었다

그렇게 되다 보니 UserService 는 BCryptPasswordEncoder 를 위해서 WebSecurity 를 참조하게 되었고

WebSecurity 는 userDetailsService 를 위해서 UserService 를 참조해야 했다

 

즉, 앞서 말한 Circular Dependency 문제가 생긴것이다.

 

그래서 이 문제 해결을 위해서 UserDetailsService 를 UserService 에서 상속 받지 말고 

별도의 클래스를 만들어서 거기서 구현하도록 분할하려고 하였다.

 

 

 

* 문제를 해결한 코드

- UserDetailsServiceImpl.java

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
 
    private final UserRepository userRepository;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(username);
 
        if (user == null)
            throw new UserServiceException("user not exist !");
 
        return new org.springframework.security.core.userdetails.User(
                user.getEmail(),
                user.getEncryptedPwd(),
                true,
                true,
                true,
                true,
                new ArrayList<>()
        );
    }
}
 
cs

 

 

- WebSecurity.java

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity extends WebSecurityConfigurerAdapter {
 
    private final UserDetailsServiceImpl userDetailsServiceImpl;
 
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(bCryptPasswordEncoder());
    }
 
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
 
cs

 

 

- UserService.java

public interface UserService {
}
cs

 

 

- UserServiceImpl.java

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
 
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
 
    // 기타 메소드 생략 ....
}
cs

이런식으로 분할해서 처리를 해줌으로써 순환종속 문제를 해결할 수 있었다.

 

 

 

2. @Lazy

또 다른 해결책으로는 @Lazy 를 쓰는 방법이 있다.

가장 좋은 해결책은 사실 재설계를 하는 것이나 부득이한 이유로 그럴 수 없는 경우 

임시 방편 정도의 해결책이라 생각하면 될 것 같다.

 

- 예시 코드 (출처 : https://www.baeldung.com/circular-dependencies-in-spring)

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}
cs

최초에 Bean 을 초기화할때 앞서서 말한 순차적인 순서대로 생성하는게 아니라 

프록시를 따로 만들고 나중에 필요할때 주입하는 형식이다.

 

 

3. @PostConstruct

@Autowired 로 의존성 주입해놓은 경우 @PostConstruct 를 쓰면 문제를 해결 할 수 있다

 

- 코드 예시(출처 : https://www.baeldung.com/circular-dependencies-in-spring)

@Component
public class CircularDependencyA {
 
    @Autowired
    private CircularDependencyB circB;
 
    @PostConstruct
    public void init() {
        circB.setCircA(this);
    }
 
    public CircularDependencyB getCircB() {
        return circB;
    }
}
cs

 

 

@Component
public class CircularDependencyB {
 
    private CircularDependencyA circA;
    
    private String message = "Hi!";
 
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
    
    public String getMessage() {
        return message;
    }
}
cs

 

 

 

4. Setter

생성자를 통한 의존성 주입 말고 Setter 를 통한 의존성 주입을 하게 되면

필요한 시점이 아닌 순간이 되기 전엔 의존성 주입을 받지 않게 된다

 

- 코드 예시 (출처 : https://www.baeldung.com/circular-dependencies-in-spring)

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public void setCircB(CircularDependencyB circB) {
        this.circB = circB;
    }
 
    public CircularDependencyB getCircB() {
        return circB;
    }
}
cs

 

@Component
public class CircularDependencyB {
 
    private CircularDependencyA circA;
 
    private String message = "Hi!";
 
    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
 
    public String getMessage() {
        return message;
    }
}
cs

 

 

 

- Reference

https://www.baeldung.com/circular-dependencies-in-spring

 

 

 

728x90