본 포스팅은 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"
    }
}