AOP
AOP(Aspect Oriented Programming): 관점 지향 프로그래밍
관심사의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임이다.
Spring AOP
Spring에서도 AOP를 지원하며, 공통적으로 처리해야할 부분은 따로 모듈화해서 개발자가 비즈니스 로직에만 집중할 수 있도록 해준다.
예를 들어, 어떤 API의 어느 지점(Pointcut)에서 어떤 작업을 수행할 것인지(Advice)를 공통적으로 처리할 수 있다.
다음 예시를 보자. (본 코드는 Kotlin을 기준으로 작성되었다)
Pointcut
"어느 지점에서"
@Aspect
class PointcutList {
@Pointcut("execution(* com.example.ltaop..controller..*.*(..))")
fun allController() {
}
}
위 코드는 controller 디렉토리 하위에 호출되는 메서드에 대해 Pointcut을 한다는 뜻이다.
Advice
"어떤 작업을"
@Aspect
@Component
class SystemAdvice {
companion object {
val log: Logger = LoggerFactory.getLogger(SystemAdvice::class.java) as Logger
}
@Before("PointcutList.allController()")
fun adviceController1() {
log.info(">>>>> controller start")
}
}
@Before()는 앞서 생성한 특정 Pointcut(컨트롤러) 이전에 해당 메서드를 수행하는 애노테이션이다.
실제로 컨트롤러의 아무 API나 호출해보면 매번 ">>>>> controller start" 로그가 찍히는 것을 확인할 수 있다.
이 밖에 @After @AfterThrowing 등 다양한 Advice 애노테이션을 제공한다.
@Around
이번에는 @Around 애노테이션에 대해 알아보자.
@Aspect
@Component
class SystemAdvice {
companion object {
val log: Logger = LoggerFactory.getLogger(SystemAdvice::class.java) as Logger
}
@Around("PointcutList.allController()")
fun adviceController(pjp: ProceedingJoinPoint): Any? {
val result: Any?
log.info(">>>>> controller start")
try {
result = pjp.proceed()
} catch (e: Exception) {
log.error(">>>>> controller end")
throw e
}
log.info(">>>>> controller end")
return result
}
}
API의 실행 이전에 무조건 수행됐던 @Before와 달리,
@Around는 API의 실행 자체도 제어할 수 있는 애노테이션이다.
코드는 ProceedingJoinPoint 변수를 인자로 받아서 proceed() 메서드를 통해 API를 실행시킨다.
proceed() 메서드 이전과 이후에 코드를 추가하면 API 실행 이전과 이후에 해당 작업을 수행한다고 볼 수 있다.
전체적인 API의 흐름을 제어할 수 있기 때문에 많이 사용하는 애노테이션이다.
@Around를 조금 더 응용해보자.
@Aspect
@Component
class SystemAdvice {
companion object {
val log: Logger = LoggerFactory.getLogger(SystemAdvice::class.java) as Logger
}
@Around("PointcutList.allController()")
fun adviceController(pjp: ProceedingJoinPoint): Any? {
val result: Any?
val req = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request
val signatureName = "${pjp.signature.declaringType.simpleName}.${pjp.signature.name}"
log.info(">>>>> controller start [$signatureName() from ${req.remoteAddr}] by ${req.method} ${req.requestURI}")
val elapsedApi = measureTimeMillis {
try {
result = pjp.proceed()
} catch (e: Exception) {
log.error(">>>>> controller start [$signatureName() from ${req.remoteAddr}] with Error[${e.message}]")
throw e
}
}
if (elapsedApi > 1000) {
TODO("문자 알림")
}
log.info(">>>>> controller end [$signatureName() from ${req.remoteAddr}]")
return result
}
}
AOP에서는 request 정보를 활용하면 좋다. request에서 호출한 호스트 주소, HTTP 메서드, API URI 등을 조회할 수 있다.
ProceedingJoinPoint에서는 signatureName이라는 것을 조회할 수 있는데, 이는 컨트롤러에서 어떤 메서드가 실행됐는지 확인할 수 있다.
이처럼 request와 ProceedingJoinPoint의 정보를 적절히 조합하여 각 API가 어떤 정보를 담고 있는지 공통적으로 처리할 수 있다.
또한, elapsedApi와 같이 각 API를 실행하는데 어느 정도의 시간이 소요됐는지 측정하는 등 여러 방면에서 AOP를 활용할 수 있다.
Spring AOP 실무
앞서 소개한 Spring AOP를 실무에서 응용하는 방법에 대해 알아보자.
여러 가지 방법이 있겠지만 여기서는 공통적으로 자주 사용하는 정보를 미리 스프링 빈에 담아두는 방법을 소개한다.
하나의 API는 여러 서비스 로직을 사용하고, 각 서비스 로직은 공통적으로 필요한 정보가 있기 마련이다. (고객 정보 등)
그런데, 매번 서비스 로직을 수행할 때마다 필요한 정보를 DB에서 조회하면 API 규모가 커질수록 비용이 증가하게 된다.
그렇다면 이러한 공통 정보들을 미리 어디에 담아두고 서비스 로직에서 필요할 때마다 조회할 수는 없을까?
이를 Spring AOP에서 처리할 수 있다.
스프링은 스프링 컨테이너에 객체들을 스프링 빈 형태로 담아 사용한다.
매 API 요청 시마다 공통 정보를 담아둘 객체를 스프링 빈으로 등록하고 각 서비스 로직에서 그 때 그 때 사용하면 매번 DB에 접근할 필요가 없는 것이다.
공통 데이터 저장
예를 들어 고객 정보를 공통적으로 사용한다고 해보자.
먼저 고객 정보를 담아둘 객체를 생성한다.
<CommonUser>
data class CommonUser(
var userId: Int = 0,
var email: String = ""
)
<Commons>
이제 스프링 빈으로 등록할 객체를 생성한다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
data class Commons(
var user: CommonUser? = null
)
@Component: 컴포넌트 스캔 시, 해당 객체를 스프링 빈으로 등록한다.
@Scope(value = "request", ...): 스프링 빈은 API 요청 시 생성되고, API 종료 시 삭제된다. (빈의 생명주기가 API 시작부터 끝이다)
여기에 앞서 생성한 CommonUser를 추가했다. 만약 공통적으로 처리할 또 다른 정보가 있다면 Commons 객체에 추가하면 되겠다.
이제 이전 SystemAdvice 코드를 응용해보자.
@Aspect
@Component
class SystemAdvice {
companion object {
val log: Logger = LoggerFactory.getLogger(SystemAdvice::class.java) as Logger
}
@Autowired lateinit var common:Commons // 의존관계 주입
@Around("PointcutList.allController()")
fun adviceController(pjp: ProceedingJoinPoint): Any? {
val result: Any?
val req = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request
val res = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).response as HttpServletResponse
val signatureName = "${pjp.signature.declaringType.simpleName}.${pjp.signature.name}"
log.info(">>>>> controller start [$signatureName() from ${req.remoteAddr}] by ${req.method} ${req.requestURI}")
setAuth() // 메서드 추가
val elapsedApi = measureTimeMillis {
try {
result = pjp.proceed()
} catch (e: Exception) {
log.error(">>>>> controller start [$signatureName() from ${req.remoteAddr}] with Error[${e.message}]")
throw e
}
}
if (elapsedApi > 1000) {
TODO("문자 알림")
}
log.info(">>>>> controller end [$signatureName() from ${req.remoteAddr}]")
return result
}
private fun setAuth() {
val user = CommonUser(
userId = 1, email = "email"
)
common.user = user
}
}
CommonUser를 생성하는 setAuth() 메서드를 추가했다.
이제 각 서비스에서 Commons를 의존하면 언제든지 CommonUser 정보를 조회할 수 있다.
예를 들어, 다음과 같이 활용할 수 있다.
@Service
class UserService {
@Autowired lateinit var repoUser: UserRepo
@Autowired lateinit var commons: Commons // 의존관계 주입
fun (): Optional<ComUserMst> = repoUser.findById(commons.user.userId) // 스프링 빈 정보로 조회
}
이렇게 되면 매번 고객정보를 위해 매번 DB에 접근할 필요가 없다.
공통 데이터 상속
각 서비스마다 Commons의 의존관계를 주입하지 않고 상속을 사용하여 더 깔끔하게 코드를 짤 수도 있다.
<BaseService>
open class BaseService {
@Autowired lateinit var commons: Commons
}
<UserService>
@Service
class UserService: BaseService() {
@Autowired lateinit var repoUser: UserRepo
fun (): Optional<ComUserMst> = repoUser.findById(commons.user.userId) // 스프링 빈 정보로 조회
}
모든 서비스에서는 BaseService를 상속받고, 모든 컨트롤러에서는 BaseController를 상속받는 등 각 그룹마다 규칙을 정해서 공통 데이터를 조회하면 편리하다.
만약, BaseController를 사용하면 상속을 활용해서 PointCut을 다음과 같이 수정할 수 있다.
@Aspect
class PointcutList {
@Pointcut("within(com.example.fwk.core.base.BaseController*)")
fun allController() {
}
}
애노테이션 Pointcut
앞서 상속을 통해 공통 데이터를 조회했는데, 최근에는 애노테이션 방식을 많이 사용한다고 한다.
예를 들어, 아래와 같은 애노테이션을 선언해보자.
@RestController
annotation class MyController {
}
@MyController는 @RestController 정보를 포함하기 때문에 그대로 다른 컨트롤러에서 사용할 수 있다.
@MyController
@RequestMapping("/users")
class UserController {
@Autowired lateinit var service: UserService
@GetMapping
fun getListUser() {
service.getListUser()
}
}
이렇게 하면 BaseController(클래스) 대신 MyController(애노테이션)에 원하는 공통 정보를 넣어서 사용할 수 있겠다 → 이 부분은 이후에 더 공부해 봐야겠다.
참고로 애노테이션에 대한 Pointcut은 다음과 같다.
@Aspect
class PointcutList {
@Pointcut("within(@com.example.fwk.core.base.MyController *)")
fun allController() {
}
}
'java > spring' 카테고리의 다른 글
[Spring] Spring Data JPA의 페이징 (2) | 2023.06.21 |
---|---|
[Spring] @ControllerAdvice, 특정 예외 발생 시 404에러가 발생하는 이슈 (0) | 2023.06.21 |
[Spring] PostgreSQL - PostGIS, JPA를 통해 공간 데이터 다루기 (0) | 2023.01.12 |
[Spring] Mockito when으로 repository save 리턴받기 (0) | 2022.11.21 |
[Spring] AWS S3 압축 파일 풀어서 업로드하기 - TransferManager (0) | 2022.08.05 |