스프링 시큐리티를 사용한다면 우리는 WebSecurityConfigurerAdapter 를 확장하는 클래스를 구현해야하고 아래와 같은 설정을 하게 된다.
처음 마주하면 이게 무슨 복잡한 빌더인가? 이 설정을 다 알아야하나? 싶을 정도겠지만 이처럼 여러 설정을 한번에 끝낼 수 있다는 데에서 경이로움을 느끼게 되는 그런 빌더 패턴의 사용 예시이다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers().cacheControl().disable().and()
.cors().and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic().disable()
.formLogin().disable()
.authorizeRequests()
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.antMatchers(permitAll).permitAll()
.anyRequest().authenticated()
.and()
....
}
특정 업무에서 반복되는 코드를 빌더 패턴을 사용해서 좀 편리하게 멀티 타입 조립을하고 싶었던 나는 빌더를 이렇게 저렇게 조립하는 코드를 PR한적이 있다.
내 요청을 본 사수는 빌더 패턴을 정말 우아하게 구현한걸 본적이 있는지 물었고 스프링 시큐리티를 참고해보라고 조언해주었다.
덕분에 처음으로 열어본 시큐리티 설정부분은 정말이지 높은 수준의 추상화의 산물이었다.
시큐리티 전부를 뒤집어 까고 설명하긴 아직 다 이해하지도 못했기 때문에 이해한 부분만 간단히 정리하고자 글을 쓴다.
본론으로 돌아와 시큐리티 설정을 보면 서로 다른 타입들을 이어주는 중요한 연결점은 and()라는걸 알 수 있다.
그리고 이 and() 는 SecurityConfiguererAdapter 추상 클래스에 있고 이를 확장한 구체 클래스, 또 그 클래스를 확장한 다른 구체 클래스들이 사용함으로써 서로 다른 타입을 연결하고 있다.
public abstract class SecurityConfigurerAdapter<O, B extends SecurityBuilder<O>> implements SecurityConfigurer<O, B> {
private B securityBuilder;
/* --- */
public B and() {
return this.getBuilder();
}
protected final B getBuilder() {
Assert.state(this.securityBuilder != null, "securityBuilder cannot be null");
return this.securityBuilder;
}
/* --- */
public void setBuilder(B builder) {
this.securityBuilder = builder;
}
/* --- */
}
하지만 시큐리티 설정은 단순히 어댑터 패턴을 사용하는데 그치지 않는다.
SecurityCongifurerAdapter는 <O, B extends SecurityBuilder<O>> 제네릭으로 타입을 한정하고 있고 SecurityConfigurer<O, B>를 구현하고 있다.
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
void init(B var1) throws Exception;
void configure(B var1) throws Exception;
}
SecurityConfigurer는 또 <O, B extends SecurityBuilder<O>> 타입으로 제한하고 있다.
그리고 여기 나온 SecurityBuilder는 이렇게 생겼다.
public interface SecurityBuilder<O> {
O build() throws Exception;
}
즉 SecurityCongifurerAdapter는 재귀적 타입 한정 문법을 활용하여 타입 안정성과 문법 강제의 효과를 가져간다.
물론 스프링 시큐리티는 여기서 그치지 않고 getOrApply()와 apply() 메서드를 사용하여 빌더를 바꿔 끼우고 주어진 설정을 반영하는 등 무척 복잡한 고도의 추상화를 통해 이를 가능케 한다.
맨 처음 봤던 설정을 보면 어떤 대상에 대해 설정을 할건지 선언하는 것으로 메서드 체이닝이 시작되는데 아래는 그 중 가장 기본적인 headers()와 cors()이다.
각 메서드는 getOrApply()를 호출한다.
public HeadersConfigurer<HttpSecurity> headers() throws Exception {
return (HeadersConfigurer)this.getOrApply(new HeadersConfigurer());
}
public HttpSecurity headers(Customizer<HeadersConfigurer<HttpSecurity>> headersCustomizer) throws Exception {
headersCustomizer.customize(this.getOrApply(new HeadersConfigurer()));
return this;
}
public CorsConfigurer<HttpSecurity> cors() throws Exception {
return (CorsConfigurer)this.getOrApply(new CorsConfigurer());
}
public HttpSecurity cors(Customizer<CorsConfigurer<HttpSecurity>> corsCustomizer) throws Exception {
corsCustomizer.customize(this.getOrApply(new CorsConfigurer()));
return this;
}
public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception {
ApplicationContext context = this.getContext();
return ((ExpressionUrlAuthorizationConfigurer)this.getOrApply(new ExpressionUrlAuthorizationConfigurer(context))).getRegistry();
}
getOrApply()는 호출한 타입으로 이미 설정 되어 있다면 해당 설정을, 다른 설정이라면 적용한 후 새로 적용 된 설정 객체를 apply() 메서드를 사용해 적용한 뒤 반환한다.
private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(C configurer) throws Exception {
C existingConfig = (SecurityConfigurerAdapter)this.getConfigurer(configurer.getClass());
return existingConfig != null ? existingConfig : this.apply(configurer);
}
그리고 apply()메서드는 아래와 같이 생겼는데 여기서 다시 SecurityConfigurereAdapter가 등장한다.
여기서 새로운 설정 객체를 적용하고 빌더를 설정하는 것이다.
AbstractConfiguredSecurityBuilder
public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
configurer.addObjectPostProcessor(this.objectPostProcessor);
configurer.setBuilder(this);
this.add(configurer);
return configurer;
}
public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Exception {
this.add(configurer);
return configurer;
}
다루지는 않았지만 permitAll()이나 disabled()와 같은 메서드도 열어보면 이와 유사한 형태를 지니고 있다.
즉 스프링 시큐리티 설정의 멀티 타입 빌더는 간단해 보이지만
자그만치 어댑터 패턴(Adapter Pattern)과 재귀적 타입 한정(recirsove type bound), 그리고 고도의 추상화가 합쳐진 컴비네이션!!
여기까지는 공부했지만 getOrApply()와 같은 설정 객체를 교환하는 쪽은 이해가 부족하여 원래 목표했던 멀티 타입 빌더를 활용한 우아한 빌더 사용은 아직 완성하지 못했다.
하지만 시큐리티의 우아한 멀티 타입 빌더가 너무 놀라운 나머지 이해한 정도까지라도 정리하여 남긴다.
어댑터 패턴, 재귀적 타입 한정, 추상화를 활용하면 정말 우아하게 멀티 타입 빌드가 가능하다.
'공부 기록' 카테고리의 다른 글
[SPRING RETRY] 바퀴를 다시 발명하지 마라 - @Retrayble을 활용한 재시도 전략 (0) | 2023.01.30 |
---|---|
[SPRING SECURITY] WebSecurityConfigurerAdapter Deprecated - 최신 설정 방법 (0) | 2022.11.12 |
[Network] 액세스 회선 (0) | 2022.07.18 |
[Rest Assured] Rest Assured로 Mock과 함께 Controller Unit Test 하기 (0) | 2022.07.09 |
[Network] 라우터 (0) | 2022.07.07 |
댓글