SpringBoot ResponseBodyAdvice 특정 응답 값 암호화하기
본 포스팅은 ResponseBodyAdvice를 이용하여 암호화된 응답값을 생성하는 예제입니다.
사실 HandlerInterceptor의 postHandler 같은 곳에서 응답값을 가공할 수 있을 듯하지만, 사실 인터셉터 단에서 응답 가공은 불가능합니다. 하지만 특정 API만 암호화된 값으로 응답을 가공하고 싶을 때가 있는데, 그럴때 사용하는 것이 ResponseBodyAdvice입니다.
ResponseBodyAdvice
@RestControllerAdvice
class EncryptedResponseWrapper : ResponseBodyAdvice<Any?> {
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
TODO("Not yet implemented")
}
override fun beforeBodyWrite(
body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse
): Any? {
TODO("Not yet implemented")
}
}
1. @ControllerAdvice 및 ResponseBodyAdvice를 구현
먼저 Advice클래스로 만들 클래스에 어노테이션 @ControllerAdvice를 붙여준 뒤 ResponseBodyAdvice의 구현체로 만들어줍니다.
2. supports와 beforeBodyWrite을 오버라이딩
ResponseBodyAdvice를 구현하기 위해 두 메소드(supports, beforeBodyWrite)를 오버라이딩 해야합니다. 이 두 메소드의 역할은 supports에서 현재 Controller의 결과 response를 beforeBodyWrite로 보낼 것인지 판단합니다. 그 뒤 beforeBodyWrite에서 사용자가 원하는 가공 작업 수행합니다.
3. supports
/**
* Whether this component supports the given controller method return type
* and the selected {@code HttpMessageConverter} type.
* @param returnType the return type
* @param converterType the selected converter type
* @return {@code true} if {@link #beforeBodyWrite} should be invoked;
* {@code false} otherwise
*/
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
위 설명에 따라 beforeBodyWrite 메소드를 실행 유무를 판단하면 되는 메소드입니다. 저는 특정 컨트롤러 메소드에 어노테이션이 달려있는 경우에 암호화 작업을 수행하고자 합니다.
4. beforeBodyWrite
/**
* Invoked after an {@code HttpMessageConverter} is selected and just before
* its write method is invoked.
* @param body the body to be written
* @param returnType the return type of the controller method
* @param selectedContentType the content type selected through content negotiation
* @param selectedConverterType the converter type selected to write to the response
* @param request the current request
* @param response the current response
* @return the body that was passed in or a modified (possibly new) instance
*/
@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
Controller 응답이 끝난 뒤 반환된 Body데이터를 JSON, XML 등 기타 형식으로 변환할 HttpMessageConverter가 선택되었지만 데이터를 변환하기 직전에 호출 되는 메소드입니다. 즉, 클라이언트와 JSON형식으로 주고받는 API서버인 경우 HttpMessageConverter로 JSON 변환 클래스가 선택이 된 상황에서 converter를 이용하여 JSON으로 변환하기 직전에 호출되는 것입니다.
구현
먼저 암호화를 구분할 수 있는 어노테이션을 작성합니다.
@Target(AnnotationTarget.FUNCTION)
annotation class Encrypt
이후 ResponseBodyAdvice의 구현체를 만듭니다.
@RestControllerAdvice
class EncryptedResponseWrapper(
private val objectMapper: ObjectMapper
) : ResponseBodyAdvice<Any?> {
override fun supports(methodParameter: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
return methodParameter.getMethodAnnotation(Encrypt::class.java) != null
}
override fun beforeBodyWrite(
body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse
): Any? {
return body?.let {
encrypt(objectMapper.writeValueAsString(it))
}
}
// 암호화 로직
fun encrypt(body: Any?): Any? {
return body
}
}
이후 간단하게 supports 메소드에 대해 kotest로 테스트코드를 작성해보겠습니다.
class EncryptedResponseWrapperTest : FreeSpec({
"supports" - {
"Encrypt 어노테이션이 달려 있으면 true를 반환한다." {
// given
val encryptedResponseWrapper = EncryptedResponseWrapper(Super2ObjectMapper.objectMapper())
val method = TestClass::class.java.getDeclaredMethod("annotatedMethod")
val methodParameter = MethodParameter(method, -1)
// when
val result = encryptedResponseWrapper.supports(
methodParameter,
mockkClass(type = HttpMessageConverter::class).javaClass
)
// then
result shouldBe true
}
"Encrypt 어노테이션이 달려 있지 않으면 false를 반환한다." {
// given
val encryptedResponseWrapper = EncryptedResponseWrapper(Super2ObjectMapper.objectMapper())
val method = TestClass::class.java.getDeclaredMethod("notAnnotatedMethod")
val methodParameter = MethodParameter(method, -1)
// when
val result = encryptedResponseWrapper.supports(
methodParameter,
mockkClass(type = HttpMessageConverter::class).javaClass
)
// then
result shouldBe false
}
}
})
class TestClass {
@Encrypt
fun annotatedMethod(): String {
return "hello"
}
fun notAnnotatedMethod(): String {
return "hello"
}
}
'Spring boot' 카테고리의 다른 글
SpringBoot 모니터링 하기 (feat. Grafana, Prometheus) (1) | 2023.03.06 |
---|---|
Kotest를 활용해 Spring Boot에서 테스트코드 작성하기 (0) | 2023.01.29 |
Hikari Connection Pool 확인 (0) | 2022.12.09 |
Spring Boot + Kotlin 깔끔한 validation 처리 (feat. Jackson) (1) | 2022.10.18 |
댓글
이 글 공유하기
다른 글
-
SpringBoot 모니터링 하기 (feat. Grafana, Prometheus)
SpringBoot 모니터링 하기 (feat. Grafana, Prometheus)
2023.03.06 -
Kotest를 활용해 Spring Boot에서 테스트코드 작성하기
Kotest를 활용해 Spring Boot에서 테스트코드 작성하기
2023.01.29 -
Hikari Connection Pool 확인
Hikari Connection Pool 확인
2022.12.09 -
Spring Boot + Kotlin 깔끔한 validation 처리 (feat. Jackson)
Spring Boot + Kotlin 깔끔한 validation 처리 (feat. Jackson)
2022.10.18