토이 프로젝트 하면서 다음과 같은 문제가 생겼었다
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
'Spring' 카테고리의 다른 글
Spring Boot 2.5 이후 버전과 Hibernate 와 data.sql (0) | 2021.09.15 |
---|---|
Spring Cloud Eureka 와 기본 REST API (0) | 2021.09.14 |
Spring Boot 와 JWT 로 로그인하기 (0) | 2021.07.06 |
Spring Boot 로 이메일 회원가입 하기 (1) | 2021.07.06 |
Spring Cloud Netflix Eureka (0) | 2021.07.05 |