본문 바로가기

연재작/WEB - BE

Spring Bean Deep Dive

Spring Bean과 관련해서 알게된 몇 가지 사실을 적어보고자 한다.

 

1. 생성자 주입을 권장하는 이유

 

MVC 패턴에 맞게, 그리고 3-layered-Architecture 패턴에 맞게 만들어오던 그동안은 순환 참조 자체가 발생할 일이 없었다.

계층이 다른게 패키지 단위로부터 구분이 됐기 때문이다.

 

문제가 되는 시점은 @Configuration을 통해서 @Bean을 정의하고,

해당하는 @Bean을 다른 @Configuration 혹은 @Component에 정의할 때 만들어졌다. 

최근에 Spring Security를 배우다 생긴 상황이다.

 

CustomAuthenticationProvider는 CustomUserDetails와 passwordEncoder에 의존

@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    ...
}

 

CustomUserDetailService는 UserRepository에 의존

@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService {
    private final UserRepository userRepository;

    ...
}

 

UserRepository는 PasswordEncoder에 의존

@Repository
@RequiredArgsConstructor
public class UserRepository {
    private final PasswordEncoder passwordEncoder;

    ...
}

 

SecurityConfig(PasswordEncoder)는 CustomAuthenticationProvider에 의존

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {

    private final CustomAuthenticationProvider authProvider;
    
    ...
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    ...
}

 

 

이 때 가능한 cycle의 후보는 4개짜리 cycle과 2개짜리 cycle(이건 확정적)이 겹쳐져 있는 안좋은 순환 참조가 형성된다.

이렇게 될 경우, application을 시작하면 Spring Boot에서 다음과 같은 에러 내용을 보여준다.

이렇게 application의 시작에서 보여줄 수 있기 때문에 final 선언과 생성자 주입 방식을 추천하는 것이다.

 

그러면 이를 해결하기 위해서는 어떻게 해야하는가?

1) 아래의 내가 그림을 그린 것 처럼 모든 참조 과정을 파악해서 도식을 그려보고
직접적인 순환 참조를 확인해 cycle을 끊어야 한다.

userRepository와 CustomUserDetailService 사이의 화살표 방향이 반대로 그려졌다 ㅠㅠ...

 

확실한 cycle이 형성되는 경우는 SecurityConfig의 PasswordEncoder와 CustomAuthenticationProvider이므로

PasswordEncoder를 SecurityConfig에서 생성된 것이 아닌, 독립된 Bean으로 생성한 class를 형성시켜야 한다.

 

2) Spring Boot에서 보여주는 도식에서 맨 마지막에 나와있는 SecurityConfig에서

맨위에 참조되는 Bean이 어떤 것인지를 확인해줘도 동일한 결과를 얻을 수 있다.

마찬가지로 PasswordEncoder를 독립 시켜줘야 한다는 것을 알게 될 것이다.

 

 

그러면 아래와 같이 하면 된다.

@Configuration
public class PasswordEncoderConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        return encoder;
    }
}

 

의존 관계는 아래의 그림처럼 변형된다.

 

P.S.) 생성자 주입은 사실상 파라미터 주입이니까 파라미터 주입을 봤다고 해서

나처럼 처음 본 것 마냥 놀라지 말도록 하자.

 

 

2. 변수명에 맞게 @Autowired 된다.

 

다형성을 이용한 SOLID 법칙중 D, 의존 역전 법칙을 지키기 위해

해당 component를 Interface화 하고 이에 대해 여러 구현체를 만드는 경우가 있을 것이다.

만약 이때, 각각의 구현체마다 @Service로 Bean 설정을 한 뒤

상위 컴포넌트에서

IComponent iComponent; 라고 필드에 선언하면 어떻게 될까?

이러면 Spring에서는 너무 많은 mapping이 일어나기 때문에

어느 하나의 component를 선택하라는 얘기를 한다.

물론 이런 경고를 한다면 해결책은 아래와 같다.

 

1) 나머지 구현체의 @Component를 지워버려서 명시적으로 하나만 사용

 

2) 내가 사용하고자 하는 구현체를 변수명으로 사용

가령 IComponent에서 구현한 구현체가 AComponent, BComponent라고 한다면

IComponent aComponent; 혹은 IComponent bComponent; 로 정의하면

변수명에 맞춰서 Autowired가 된다.

 

3) Order 설정

 

Order 가 작은 순서대로 우선 순위를 갖는다.

@Component 
@Order(1) 
public class FirstService implements MyService {
} 

@Component 
@Order(2) 
public class SecondService implements MyService {
} 

@Autowired private List<MyService> services; // FirstService -> SecondService 순으로 주입됩니다.

 

 

 

3. Bean의 생성은 싱글톤만 가능한 것이 아니다.

 

일반적으로 Spring IoC에 의해서 주입되는 모든 Bean은 싱글톤 life cycle을 가진다고 생각할 수 있는데,

아니다. singleton을 기본으로 하는 것일 뿐이지,

요청당 혹은 세션당 생애주기를 가질수도 있다. 이를 스코프라고 얘기한다.

 

이 부분은 앞으로 사용할 것 같아서 여지를 남겨두겠다.