본문 바로가기
개발 이야기/Springboot

[Kotlin] Springboot, AOP를 활용한 요청 데이터 조작(feat. Reflection)

by 농개 2021. 2. 9.
반응형

Kotlin + Springboot 환경에서 간단한 AOP를 만드는 방법을 공유합니다.

AOPAspect Oriented Programming의 약자로 직역하면 관점 지향 개발이라 합니다.

공통적인 기능들을 모듈화 해서 개발자가 적용하고 싶은 위치(메서드, 클래스 )에 심을 수 있습니다.

 

이번 예제에서는 사용자로부터 요청된 전송데이터를 AOP를 통해 조작해보는 것을 해보려고합니다.

요청 데이터를 조작하는데에는 Reflection을 사용합니다.

Reflection 이란 클래스 정보를 코드상에서 사용하는 기술로 개발자가 직접 class의 다양한 정보들(필드, 메서드, 어노테이션 등등)을 불러올수 있는 기술입니다. 주로 자바에서 코드를 조작하는 방법으로 많이 언급 되는데 당연히 코틀린에서도 가능합니다.

 

 

01. AOP 클래스 작성

@Aspect
@Component
class MyAop {
    val logger: Log = LogFactory.getLog(MyAop::class.java)

    @Before("@annotation(me.examplewebmvc.annotation.AuthorDefaultSet)")
    fun setAuthorDefault(jointPoint: JoinPoint) {
        val request =
            (RequestContextHolder.currentRequestAttributes() as ServletRequestAttributes).request // request 정보

        logger.info("Request URI: " + request.requestURI)
        logger.info("Request IP: " + request.remoteAddr)
        logger.info("Request Method: " + request.method)

    }
}

위와 같이 Springboot에서 제공되는 @Aspect를 사용해서 AOP를 위한 클래스를 작성할 수 있습니다.

해당 클래스도 빈으로 등록되야하기 때문에 @Component 또한 추가해 줬습니다.

본 예제에서는 어노테이션으로 타겟을 지정할 것이기 때문에 아래와 같이 포인트 컷을 지정했습니다.

  • "@annotation(me.examplewebmvc.annotation.AuthorDefaultSet)"  // 패키지경로가 다를때 전체 경로를 지정해줘야합니다!!
  • @Before 은 공통 기능이 실행되는 지점을 메소드 실행 전으로 지정해줍니다.

 

02. Annotation 작성

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class AuthorDefaultSet

위와 같이 작성해줍니다.

@Target은 해당 어노테이션을 어디에다 붙일 수 있을지를 지정합니다. 위의 경우 함수에다가 지정했습니다.

@Retetion을 Runtime으로 지정해줌으로서 애플리케이션 실행 중에도 어노테이션을 유지하게됩니다.

 

 

03. 함수에 Annotation 추가

@AuthorDefaultSet    
@PostMapping
fun setBook(
	@RequestBody book: BookRequest
): ResponseEntity<Any>{
	return try {
		val book = bookService.setBook(book)  // 책 추가

		ResponseEntity(BasicResponse("SUCCESS", book), HttpStatus.OK)
	} catch (e: Exception){
		logger.error(e.localizedMessage, e)
		throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "internal server error")
	}
}

위와 같이 함수 위에다 추가해주면 됩니다.

 

 

04. Reflect을 사용해서 RequestBody 데이터를 조작

data class BookRequest(
    var name: String? = null,
    var author: String? = null
): Serializable

 

위와 같은 Request 데이터 형식에서 author 필드가 null 일 때,

해당 필드 값을 조작하는 예제를 작성해보겠습니다.

 

 

val jsonObjectMapper = jacksonObjectMapper() 

@Before("@annotation(me.examplewebmvc.annotation.AuthorDefaultSet)")
fun setAuthorDefault(jointPoint: JoinPoint) {
  ...(생략)

  // jointPoint 에서 파라미터 정보를 가져올 수 있다.
  jointPoint.args.forEach {
    val requestBodyObject = jsonObjectMapper.convertValue(it, it.javaClass) // get request body object

    // kotlin reflection
    requestBodyObject.javaClass.kotlin.declaredMemberProperties.forEach { prop ->
      with(prop) {
        if (name == "author" && get(requestBodyObject) == null) {
          // Property field name 이 "author"이며, null일 경우 
          logger.info("Set default value in RequestBody!!")
          (prop as KMutableProperty<*>).setter.call(it, "unknown")
        }
      }
    }
  }
}

먼저 jsonObjectMapper를 이용해 앞서 정의된 함수의 파라미터인 BookRequest 객체를 불러옵니다.

그리고 코틀린에서 제공하는 reflection API를 사용해봅시다.

  • 해당 객체에서 .javaClass를 통해 Class<T> 객체를 가져옵니다.
  • kotlin.declaredMemberProperties를 이용하면 필드들에 대한 정보를 리스트로 가져올 수 있습니다.
  • prop.name에는 필드명이 저장되어있습니다.
  • prop.get()을 통해 필드의 값을 확인 할 수 있습니다.
  • prop.setter를 통해 해당 값에 새로운 값("unknown")을 셋팅해주었습니다.

 

 

 

이후 서버를 띄우고 해당 API 를 호출해보면 author 가 null 값이 오면 "unknown"으로 변경되어 메서드에 전달되는 것을 확인 할 수 있을겁니다.

 

사실 예제가 좀 억지긴합니다..ㅎㅎ 필드에 그냥 default 값을 줘도 되는데 말이죠..

하지만 실무에서 공통 기능을 설계하고 복잡한 기능이 요구된다면 AOP와 Reflection을 사용할 일이 있지 않을까 생각 됩니다.

 

 

반응형