728x90
발단
Spring Security 와 MockMvc 를 Kotlin 환경에서 검증을 하던 도중 설정(config)이 옳음에도 테스트의 결과가 일관된 반응을 하는 현상을 발견하여 그 이유를 알아보고 왜 그러하였는지 과정을 작성합니다.
Kotlin 고차함수 & 함수형 인터페이스
- Kotlin 의 고차함수에 대하여 위 글을 통해 개념을 간단히 보시면 더 이해가 쉽습니다.
- SAM(Single Abstract Method) 변환에 대해서도 다루고 있습니다.
MockMvc 호출을 Kotlin 으로 표현하는 2가지 방식
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
..
@Autowired
lateinit var mockMvc: MockMvc
// 정석적인 호출
mockMvc.perform(
get("/api/test")
)..
// SAM 변환을 사용한 호출
mockMvc.perform {
get("/api/test")
.buildRequest(it)
}..
- 정석적인 호출: perform 함수의 매개변수인 `RequestBuilder` 를 반환 타입으로 가지는 함수(get, put..)를 호출하여 처리할 수 있습니다.
- SAM 변환 호출: perform 의 반환타입인 `MockHttpServletRequest` 를 반환하게끔 처리할 수 있습니다.
2가지 방식을 호출할 가능성
- 제가 사용하고 있는 Intellij 의 경우 SAM 변환이 가능한 호출의 경우 SAM 변환 방식을 선 추천해주고 있습니다.
- 따라서, 2가지 방식 어느것이든 같은 결과를 가질 것이라 예측하고 의심없이 사용하게 될 수 있다고 생각됩니다.
Spring Security 와 사용시 문제 발생
..
@Test
@WithMockUser(roles = ["MANAGER"]) // spring security test 를 위한 annotation 으로 인증된 가상 사용자를 제공합니다.
fun test1() {
..
// `/api/test` 엔드포인트는 `MANAGER` 권한이 있어야 접근이 가능합니다.
mockMvc
// 정석적인 호출
.perform(get("/api/test"))
// SAM 변환 호출
.perform {
get("/api/test").buildRequest(it)
}
.andExpect {
// 인증된 가상 사용자 권한을 추가하였기에 검증을 통과하여야 합니다.
Assertions.assertEquals(it.response.status, 200)
}
}
- 정석적인 호출 사용시 @WithMockUser 로 제공한 가상 사용자 권한이 허용되어 검증을 통과합니다.
- 그러나, SAM 변환 사용시 사용자 권한 검증에 통과하지 못하게 됩니다.
문제 발생 이유
- 위에서 언급하였던 mockMvc.perform(..) 함수의 파라미터 타입은 `RequestBuilder` 입니다.
- 그런데 SAM 변환을 적용하면서 저는 RequestBuilder 타입을 반환하는 람다를 인자로 전달하였습니다.
- 이는 ..perform(..) 내부 동작에 문제가 발생하게 만듭니다.
defaultRequestBuilder 와 Merge 가 되지 않아 TestSecurityContext 미적용
// MockMvc.perform 메서드 일부
public ResultActions perform(RequestBuilder requestBuilder) throws Exception {
// requestBuilder 는 Mergeable 인스턴스의 일부인지 묻고 있습니다.
if (this.defaultRequestBuilder != null && requestBuilder instanceof Mergeable) {
requestBuilder = (RequestBuilder)((Mergeable)requestBuilder).merge(this.defaultRequestBuilder);
}
...
}
// (Mergeable)requestBuilder.merge 메서드 일부
public Object merge(@Nullable Object parent) {
...
this.postProcessors.addAll(0, parentBuilder.postProcessors);
return this;
...
}
// TestSecurityContextHolderPostProcessor.postProcessRequeset 메서드 일부
private static final class TestSecurityContextHolderPostProcessor extends SecurityContextRequestPostProcessorSupport implements RequestPostProcessor {
...
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
...
// TestSecurityContext 를 SecurityContext 로 대체
SecurityContext context = TestSecurityContextHolder.getContext();
if (!this.EMPTY.equals(context)) {
this.save(context, request);
}
return request;
...
}
}
- MockMvc.perform 코드블럭의 첫 if 문에 따라 requestBuilder 가 Mergeable 인스턴스에 포함된다면 merge 함수를 통해 defaultRequestBuilder 와 merge 해주고 있습니다.
- 저는 requestBuilder 를 SAM 변환을 통해 람다로 제공하였기에 람다는 Mergeable 인스턴스가 아니라 merge 를 호출할 수 없었습니다.
- merge 메서드 내부 동작을 살펴보면 parentBuilder(defaultRequestBuilder)로 부터 postProcessors 를 merge 하고 있는 것을 알 수 있습니다.
- parentBuilder.postProcessors 내부에는 SecurityMockMvcRequestPostProcessors 클래스가 포함되어 있습니다.
- 이 클래스의 postProcessRequest 메서드는 TestSecurityContext 를 현재 호출의 context 로 대체하여 줍니다.
- 따라서, @WithMockUser 로 제공한 정보를 context 에 반영하게 되어 인증/권한 정보를 가진 채로 호출할 수 있게 됩니다.
결론
- Builder 패턴혹은 호출전 다양한 설정이 적용되어야 하는 클래스의 경우 설정 적용을 위해 내부적으로 다양한 조건문을 사용할 수 있습니다.
- 이는 인터페이스 혹은 고차함수 등으로 복잡한 인수가 포함될 수 있습니다.
- 따라서, 정석적이 방식의 호출을 사용하는 것이 오류 확률을 줄여줄 것이라 생각합니다.
- 위 경우처럼 본인이 구현한 동작이 아니고서야 SAM 변환은 줄이는 것이 좋겠다는 생각이 듭니다.
728x90