[Spring] Spring Security 적용시 순환 참조 Error

Gogh·2023년 1월 2일
0

Spring

목록 보기
18/23

🎯 목표 : 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 {
        // permit
        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();
        // login
        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를 참조하고 있다.
    • 세번째로 PasswordEncoderSpringSecurityConfig내부에서 Bean으로 등록 해 주었다.
  • 위 예제에서 순환 참조가 발생할수 있는 경우를 예상해 보았다.
    • UserService에서 PasswordEncoder를 참조하고 있는데 PasswordEncoder의 Bean을 SpringSecurityConfig내부에서 등록하고 있으며 SpringSecurityConfig에서도 UserService를 참조하고 있다.
      • 결국, UserServiceSpringSecurityConfig를, SpringSecurityConfigUserService를 참조하고 있는 구조가 된다.
    • 다음으로 예상가능한 문제는, adminAccount()메소드에서 Stub 데이터를 어플리캐이션 생성시에 만들기 위해 UserService를 참조하고 있을때 발생한다.
      • 스프링 컨테이너에서 현재 프로잭트 내에서만 Bean 생성 순서를 봤을때 만약 커스텀 AuthenticationProvider가 있다면 해당 AuthenticationProvider의 객체를 먼저 Bean 으로 등록하고 없다면 Spring Security에서 기본으로 제공하는 AuthenticationProvider객체가 Bean으로 등록 된 후, SpringSecurityConfig의 객체를 Bean으로 등록하게된다.
      • 그 다음, UserService의 객체가 Bean으로 등록되는데, 순서를 봤을때 아직 생성되지도 않은 UserService의 객체를 SpringSecurityConfig에서 참조하고 있는 구조가 된다.

📌 문제 해결

  • 위에서 언급한 두가지 문제를 어떻게 해결해야 될까?
  • 순환 참조 문제를 해결하는 방법은 다양하다. 여러가지 해결 방법에 대해서는 레퍼런스 블로그를 확인 해보면 되겠다.
  • 가장 좋은 방법은 의존 관계를 재설계하는 것이다.
  • SpringSecurityConfig에서 UserService의 의존 관계를 완벽히 분리하고, Stub 데이터도 의도한대로 생성할수 있는 방법으로 해결 해 보았다.

📌 PasswordEncoder Bean 등록 분리

// PasswordEncoderConfig.java
@Configuration
public class PasswordEncoderConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

// SpringSecurityConfig.java
@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를 새로 만들어 UserServiceSpringSecurityConfig를, SpringSecurityConfigUserService를 참조하고 있는 문제를 해결하였다.
  • 하지만 위 코드로 실행해보면 아래와 같은 에러가 다시 발생하게된다.
The dependencies of some of the beans in the application context form a cycle:

┌──->──┐
|  springSecurityConfig
└──<-──┘
  • 예상했던대로, adminAccount()메소드에서 Stub 데이터를 어플리캐이션 생성시에 만들기 위해 UserService를 참조하고 있을때 발생하는 문제다.

📌 UserDetailsService 구현과 Stub 데이터를 생성하는 Config 작성

//UserDetailsServiceImpl.java
@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;
    }
}

//InitializeConfig.java
@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 등록후 의존관계가 주입되도록 순서를 변경할 수 있다.
  • 위와 같이 모든 의존 관계들을 재설정하여 발생한 문제를 해결하였고 정상적으로 동작하는것을 확인할 수 있다.
profile
컴퓨터가 할일은 컴퓨터가

0개의 댓글