오늘은 SpringBoot에서 AOP를 구현하는 방법에 대해 간단히 알아보도록 하겠다.
AOP ?
Aspect-Oriented Programming의 약자로 분산되어 있는 Aspect를 모듈화 시키는 기법
쉽게 다시 설명한다면 다음과 같다.
우리는 개발을 하다보면 횡단 개념으로 공통적으로 처리하고 싶은 부분들이 있다.
이외에도 각 서비스의 사용 용도에 따라 다양한 이유가 존재할 수 있다.
만약, 이러한 경우에 대해 상속(extend)과 구현(implement)을 통해 처리하겠다고 생각한다면 어떤 현상이 발생할까???
아마 꼬리의 꼬리를 무는 연결 관계가 형성될 것이고 이러한 구조는 결과적으로 가독성과 편의성을 저하시키는 단점이 부각되게 될 것이다.
Spring은 이러한 문제를 해결하기 위해 AOP를 제시한다.
서로 다른 클래스 사이에 횡단 개념으로 공통 사용되는 부분들이 존재할 경우, 이를 각각 구현하는 것이 아니라 Aspect 단위로 묶어 공통 관리하고 호출할 수 있도록 하는 것이다.
이러한 기법을 통해 자바의 단점 중 하나인 다중 상속 불가의 문제를 해결함과 동시에 코드 가독성 및 편의성 향상이라는 궁극적인 해결책을 제시한다.
이제 AOP의 기본적인 이론에 대해 알아봤으니 간단하게 코드를 구현해보도록 하자.
기본 스펙
- PC: Mac (M1)
- IDE: Eclipse
- 빌드 관리 도구: Maven
간단하게 아래와 같이 프로젝트를 생성한다. (의존성은 Spring Web만 우선 주입)
sping initializr 링크
https://start.spring.io
2-1을 통해 한번에 의존성을 주입할 수도 있겠지만 pom.xml에서 직접 주입하는 방법도 알아놓으면 좋으니 나머지 의존성에 대해선 코드 상, 주입을 진행한다.
우리가 추가로 주입해야 하는 의존성은 두가지이다.
pom.xml로 접근하여 아래 코드를 추가하자.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
해당 starter 패키지들을 주입하면 Springboot 특성 상, 버전 명시 없이 가장 최신 버전의 패키지 라이브러리들을 주입시켜준다.
이제 aop에 대한 테스트를 위해 간단하게 Controller를 하나 생성하겠다.
Tree 구조는 위와 같으며 MainController의 전체 소스는 아래와 같다.
package com.example.demo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MainController {
Logger log = LoggerFactory.getLogger(this.getClass());
@RequestMapping("/index")
public String index (Model model) {
log.info("index");
return "main/index";
}
}
다음으로 Controller에서 호출한 url에 대한 화면이 필요하다.
thymeleaf는 의존성을 주입하면 resources/templates까진 자동으로 생성되며 하위부턴 직접 구성해야 한다.
Tree 구조는 위와 같으며 index.html의 전체 소스는 아래와 같다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
Hello World
</body>
</html>
위 html 파일은 간단히 Hello World라는 문구만 출력될 것이다.
프로젝트 생성과 동시에 자동으로 만들어지는 최상위 클래스(~Application.java) 파일에 접근한 후, 아래처럼 @EnableAspectJAutoProxy 애노테이션을 추가해주도록 하자.
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@EnableAspectJAutoProxy
@SpringBootApplication
public class AopStudyApplication {
public static void main(String[] args) {
SpringApplication.run(AopStudyApplication.class, args);
}
}
@EnableAspectJAutoProxy
해당 애노테이션을 기재해야 Spring의 AOP를 사용할 수 있다.
최상위 패키지 클래스(~Application.java)에 해당 애노테이션을 적용함으로써 AOP를 찾을 수 있게 도와준다.
이제 aop를 사용하기 위한 클래스를 생성하도록 하자.
Tree 구조는 위와 같으며 DemoCommonAspect 클래스의 전체 소스는 아래와 같다.
package com.example.demo.common;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DemoCommonAspect {
Logger log = LoggerFactory.getLogger(this.getClass());
@Pointcut("execution(* com.example.demo..*(..))")
private void doExecute() {}
@Around("doExecute()")
public Object doLogging(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("In dologging");
String methodName = joinPoint.getSignature().toShortString();
try {
log.info(methodName+" is start");
Object obj = joinPoint.proceed();
return obj;
}finally {
log.info(methodName + " is Finish");
}
}
}
오늘 주제에 해당하는 가장 중요한 부분이라고 볼 수 있다.
- @Aspect: 해당 클래스가 공통 기능(AOP)을 제공하는 클래스임을 명시
- @Component: 해당 클래스를 Bean으로 등록
- @PointCut: 어떤 패키지/클래스/메소드에 대해 aop를 적용할 것인지 명시 (com.example.demo하위 모든 패키지/클래스/메소드에 대해 적용)
- @Around: 해당 메소드 호출 전/후로 호출
주요 정보는 위와 같으며 현재 코드 상, 테스트 흐름을 간단히 정리하면 다음과 같다.
-> com.example.demo 하위 패키지 중 어떤 메소드가 호출되면 해당 메소드에 대해 로깅한다.
이제 직접 실행해보도록 하자.
해당 도메인을 호출하면 위와 같이 화면이 노출될 것이다.
그리고 로그를 확인한다면 위와 같이 우리가 DemoCommonAspect 클래스에서 명시했던 것처럼 호출한 메소드 명에 대한 로그가 노출될 것이다.