🎯 목표 : Spring Security 순환 참조 에러 발생시 Bean 재설계 해결
📒 Spring Security 적용시 Cirular Reference 발생
📌 양방향 의존관계
- 스프링에서 양방향 의존관계 오류가 발생하는 경우는 잘못된 의존 관계 설계로 인하여 발생하게 된다.
- 간단하게 설명하면, Bean A 가 Bean B를 의존하고 Bean B 가 Bean A를 서로 의존하고 있을때 발생한다.
- 순환 참조에 대해서 여러 해결 방법이 있지만, 가장 좋은 해결 방법은 의존 관계를 재설계하는 것이다.
- Spring Security를 적용하며 The dependencies of some of the beans in the application context form a cycle 문제가 발생 하였는데, 해결하는 과정을 예제를 만들어 기록하려 한다.
📌 예제 코드
@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurityConfig {
private final UserService userService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.antMatchers("/", "/css/**", "/home", "/example", "/signup").permitAll()
.antMatchers("/post").hasRole("USER")
.antMatchers("/admin").hasRole("ADMIN")
.antMatchers(HttpMethod.POST, "/notice").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/notice").hasRole("ADMIN")
.anyRequest().authenticated();
http.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.permitAll(); // 모두 허용
// logout
http.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/");
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
return username -> {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return user;
};
}
@Bean
@PostConstruct
public void adminAccount() {
userService.signup("user", "user");
userService.signupAdmin("admin", "admin");
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
- Spring Security 연습을 위해 간단한 관리자와 유저가 존재하고 게시판 기능이 있는 어플리케이션을 개발하던 중 위와 같이 Spring Security Config 파일을 적용했을때 아래와 같은 에러를 만났다.
The dependencies of some of the beans in the application context form a cycle:
initializeConfig defined in file ......
┌─────┐
| userService defined in file .....
↑ ↓
| springSecurityConfig defined in file ......
└─────┘
- 현재
UserService
에서 SpringSecurityConfig
로 의존하고 있으며 그 반대로도 의존하고 있는 상황에서 발생한 오류다.
UserService.java
코드를 살펴 보자.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public User signup(
String username,
String password
) {
if (userRepository.findByUsername(username) != null) {
throw new RuntimeException("이미 등록된 유저입니다.");
}
return userRepository.save(new User(username, passwordEncoder.encode(password), "ROLE_USER"));
}
}
UserService
에서 유저의 Password를 암호화 하여 DB에 저장하기 위해 PasswordEncoder
를 의존하고 있다.
- 다시
SpringSecurityConfig
를 확인해 보면,
- 첫번째로
UserDetailsService
를 Bean으로 등록하기 위해 UserService
를 참조하고 있다.
- 두번째로
adminAccount()
메소드에서 Stub 데이터를 어플리캐이션 생성시에 만들기 위해 UserService
를 참조하고 있다.
- 세번째로
PasswordEncoder
를 SpringSecurityConfig
내부에서 Bean으로 등록 해 주었다.
- 위 예제에서 순환 참조가 발생할수 있는 경우를 예상해 보았다.
UserService
에서 PasswordEncoder
를 참조하고 있는데 PasswordEncoder
의 Bean을 SpringSecurityConfig
내부에서 등록하고 있으며 SpringSecurityConfig
에서도 UserService
를 참조하고 있다.
- 결국,
UserService
가 SpringSecurityConfig
를, SpringSecurityConfig
가 UserService
를 참조하고 있는 구조가 된다.
- 다음으로 예상가능한 문제는,
adminAccount()
메소드에서 Stub 데이터를 어플리캐이션 생성시에 만들기 위해 UserService
를 참조하고 있을때 발생한다.
- 스프링 컨테이너에서 현재 프로잭트 내에서만 Bean 생성 순서를 봤을때 만약 커스텀
AuthenticationProvider
가 있다면 해당 AuthenticationProvider
의 객체를 먼저 Bean 으로 등록하고 없다면 Spring Security에서 기본으로 제공하는 AuthenticationProvider
객체가 Bean으로 등록 된 후, SpringSecurityConfig
의 객체를 Bean으로 등록하게된다.
- 그 다음,
UserService
의 객체가 Bean으로 등록되는데, 순서를 봤을때 아직 생성되지도 않은 UserService
의 객체를 SpringSecurityConfig
에서 참조하고 있는 구조가 된다.
📌 문제 해결
- 위에서 언급한 두가지 문제를 어떻게 해결해야 될까?
- 순환 참조 문제를 해결하는 방법은 다양하다. 여러가지 해결 방법에 대해서는 레퍼런스 블로그를 확인 해보면 되겠다.
- 가장 좋은 방법은 의존 관계를 재설계하는 것이다.
SpringSecurityConfig
에서 UserService
의 의존 관계를 완벽히 분리하고, Stub 데이터도 의도한대로 생성할수 있는 방법으로 해결 해 보았다.
📌 PasswordEncoder Bean 등록 분리
@Configuration
public class PasswordEncoderConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurityConfig {
private final UserService userService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
}
@Bean
public UserDetailsService userDetailsService() {
return username -> {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return user;
};
}
@Bean
@PostConstruct
public void adminAccount() {
userService.signup("user", "user");
userService.signupAdmin("admin", "admin");
}
}
- 첫번째로
PasswordEncoderConfig
를 새로 만들어 UserService
가 SpringSecurityConfig
를, SpringSecurityConfig
가 UserService
를 참조하고 있는 문제를 해결하였다.
- 하지만 위 코드로 실행해보면 아래와 같은 에러가 다시 발생하게된다.
The dependencies of some of the beans in the application context form a cycle:
┌──->──┐
| springSecurityConfig
└──<-──┘
- 예상했던대로,
adminAccount()
메소드에서 Stub 데이터를 어플리캐이션 생성시에 만들기 위해 UserService
를 참조하고 있을때 발생하는 문제다.
📌 UserDetailsService 구현과 Stub 데이터를 생성하는 Config 작성
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Not Found "+username);
}
return user;
}
}
@Configuration
public class InitializeConfig {
private final UserService userService;
public InitializeConfig(
@Autowired UserService userService
) {
this.userService = userService;
}
@PostConstruct
public void adminAccount() {
userService.signup("user", "user");
userService.signupAdmin("admin", "admin");
}
}
UserDetailsService
을 구현하는UserDetailsServiceImpl
를 만들어 @Service
로 Bean 등록 해 주었다.
- 두번째로 발생한
SpringSecurityConfig
단독으로 순환참조 하는 에러의 근본적인 원인은 아니지만, 의존 관계를 분리하기 위해 UserService
를 참조하지 않고 UserRepository
를 직접 참조하도록 새로 작성하였다.
- 다음으로,
InitializeConfig
를 작성하여 Stub 데이터를 어플리캐이션 실행시 생성하도록 하였다.
- 생성되지도 않은
UserService
의 객체를 InitializeConfig
에서 참조하고 있는 구조는 변하지 않았지만 해당 클래스 내부에서 생성자에 @Autowired
를 사용하여 UserService
의 Bean 등록후 의존관계가 주입되도록 순서를 변경할 수 있다.
- 위와 같이 모든 의존 관계들을 재설정하여 발생한 문제를 해결하였고 정상적으로 동작하는것을 확인할 수 있다.