AOP는 관점지향프로그래밍으로 공통된 부분은 좀 더 쉽게 프로그래밍하는 기법입니다.
ex. 클래스에 메서드를 여러 개 구현하고 있는데, 각각 메서드마다 걸리는 실행시간을 알고 싶다.
→ 모든 메서드마다 시작 지점의 시간과 끝나는 지점의 시간을 체크해서 확인한다.
ex. 각각 메서드마다 어떤 일을 하는지 로그에 출력하고 싶다.
→ 모든 메서드에 로그를 출력하는 코드를 작성한다.
이렇게 되면 각각의 메서드가 다른 역할을 하지만, 메서드마다 걸리는 실행시간을 측정하는 기능과 로그를 출력하는 기능은 모두 동일하고 코드의 중복이 발생합니다. 또한 코드가 길어지며 가독성이 안좋아지고 실수도 할 수 있습니다.
이를 AOP로 개선이 가능합니다.
public class Hello {
public String sayHello(String name) {
//로그 남기는 공통 코드
System.out.println("log: " + new java.util.Date());
String msg = "hello~ " + name;
return msg;
}
}
Hello
클래스로부터 분리해 보겠습니다.public class HelloLog {
public static void log() {
System.out.println("log: " + new java.util.Date());
}
}
public class Hello {
public String sayHello(String name) {
HelloLog.log(); //공통코드를 호출하는 코드가 포함됨
String msg = "hello~" + name;
return msg;
}
}
AOP에서는 핵심 로직들을 구현한 코드에서 공통기능을 직접 호출하지 않습니다.
AOP에서는 분리한 공통 기능의 호출까지도 관점으로 다루며 이런 모듈로 산재한 관점을 "횡단 관점"이라고 부릅니다.
횡단관점이란, 아래 사진처럼 파란색 박스는 각각 다른 로직을 지닌 메서드들이고, 빨간색 박스가 로그 등 공통 코드입니다. 빨간색 박스가 파란색 박스를 횡단한다는 개념으로 횡단 관점이라고 부릅니다.
AOP는 이런 횡단 관점까지 분리하면서 각 모듈로부터 관점에 대한 코드를 완전히 제거하는 것을 목표로 합니다.
이전 예시에서는 클래스를 단순히 분리해서 공통 코드인 로그를 남기는 것을 구현했습니다.
public class HelloProxy extends Hello {
@override
publid String sayHello(String name) {
HelloLog.log(); //공통 코드 실행
return super.sayHello(name); //핵심 코드 실행
}
}
HelloProxy
클래스가 Hello
클래스를 상속하고 오버 로딩을 구현했습니다.Hello
클래스를 호출하면 HelloProxy
가 먼저 호출되고 HelloProxy
는 로그를 찍고 Hello
클래스의 핵심 코드를 실행합니다.하지만 이 방법에는 단점이 있습니다.
외부에서 Hello
의 A메서드를 호출해야 프록시를 거쳐서 해당 메서드가 호출됩니다.
하지만 Hello
클래스 내의 다른 메서드가 A메서드를 호출하면 프록시를 거치지 않고 바로 호출되기 때문에 공통 코드의 실행이 생략됩니다.
void aaa() {
bbb();
}
@Transactional
void bbb() {
ccc();
}
@Transactional
void ccc() {
...
}
@Transactional
어노테이션은 해당 메서드를 트랜잭션처럼 동작하도록 합니다.@Transactional
어노테이션을 거치지 않게 되는 것입니다.자바 언어에서 메서드 앞에 별도의 참조가 없으면 this를 자동으로 붙여 자기 자신의 인스턴스를 가리킨다. 결과적으로 자기 자신의 내부 메소드를 호출하는 this.ccc()가 되는데 여기서 this는 실제 대상 객체(target)의 인스턴스를 뜻합니다. 결과적으로 이러한 내부 호출은 프록시를 거치지 않게 됩니다.
위빙(Weaving)은 AOP 프레임워크가 공통 코드를 핵심코드에 삽입하는 것을 말합니다. 위빙에는 3가지 방법이 있습니다.
AspectJ
가 있습니다.AspectJ
를 사용하면 프록시를 이용한 AOP 구현에서의 단점 같은 상황은 일어나지 않지만 사용하기가 귀찮습니다.AspectWerkz
스프링에서는 자체적으로 런타임 때 위빙하는 프록시 기반의 AOP를 지원하고 있습니다. 프록시 기반이기 때문에 외부에서 메서드를 호출할 때만 적용이 가능합니다. 스프링은 완전한 AOP 기능을 제공하는 것이 목적이 아니라, 어플리케이션을 구현할 때 필요한 기능을 제공하는 것을 목적으로 하고 있습니다. spring-boot-starter-aop
라이브러리를 사용하고, @Aspect
같은 어노테이션을 통해 쉽게 AOP를 구현할 수 있습니다.
먼저 스프링 프로젝트에 build.gradle
파일에 의존성을 추가합니다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
AOP를 사용하기 위해서는 다음과 같은 Annotation들을 알아야 합니다.
@Aspect | AOP로 정의하는 클래스를 지정함 |
---|---|
@Pointcut | AOP기능을 메소드, Annotation 등 어디에 적용시킬지 지점을 설정 지점을 설정하기 위한 수식들이 매우 많음 |
@Before | 메소드 실행하기 이전 |
@After | 메소드가 성공적으로 실행 후 (예외 발생 되더라도 실행 됨) |
@AfterReturning | 메소드가 정상적으로 종료될때 |
@AfterThrowing | 메소드에서 예외가 발생할때 |
@Around | Before + After 모두 제어 (에외 발생 되더라도 실행 됨) |
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
System.out.println("Get Method가 실행됨!");
System.out.println("Get Method {id}: " + id);
System.out.println("Get Method {name}: " + name);
//서비스 로직
return id + " " + name;
}
@PostMapping("/post")
public User post(@RequestBody User user) {
System.out.println("Post Method가 실행됨!");
System.out.println("Post Method {user}: " + user);
//서비스 로직
return user;
}
이를 AOP로 분리시켜 보겠습니다.
@Aspect
@Component
public class ParameterAop {
//com/example/aop/controller 패키지 하위 클래스들 전부 적용하겠다고 지점 설정
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
private void cut() {}
//cut() 메서드가 실행 되는 지점 이전에 before() 메서드 실행
@Before("cut()")
public void before(JoinPoint joinPoint) {
//실행되는 함수 이름을 가져오고 출력
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
System.out.println(method.getName() + "메서드 실행");
//메서드에 들어가는 매개변수 배열을 읽어옴
Object[] args = joinPoint.getArgs();
//매개변수 배열의 종류와 값을 출력
for(Object obj : args) {
System.out.println("type : "+obj.getClass().getSimpleName());
System.out.println("value : "+obj);
}
}
//cut() 메서드가 종료되는 시점에 afterReturn() 메서드 실행
//@AfterReturning 어노테이션의 returning 값과 afterReturn 매개변수 obj의 이름이 같아야 함
@AfterReturning(value = "cut()", returning = "obj")
public void afterReturn(JoinPoint joinPoint, Object obj) {
System.out.println("return obj");
System.out.println(obj);
}
}
ParameterAop
클래스를 다음과 같이 작성했습니다.@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
//서비스 로직
return id + " " + name;
}
@PostMapping("/post")
public User post(@RequestBody User user) {
//서비스 로직
return user;
}
컨트롤러에 로그를 찍어주는 모든 코드를 삭제하고 위와 같이 수정해주었습니다.
이전보다 코드가 훨씬 간결해지고 가독성도 높아졌습니다.
결과
이렇게 AOP클래스를 작성하여 컨트롤러에 로그를 찍는 공통적인 부분을 제거하여 코드의 가독성을 높일 수 있고 AOP 클래스를 한번만 작성하여 여러 개의 메서드의 적용할 수 있음을 확인하였습니다.
위에서 메서드의 매개변수와 리턴 값에 대한 로깅을 남기는 AOP를 작성하였는데, 이번에는 메서드 실행 시간에 대해 로그를 찍어주는 AOP 클래스를 작성해보겠습니다.
이번에는 단순히 클래스를 작성하는 것이 아닌 사용자 지정 어노테이션을 만들어서 해당 어노테이션이 붙어있는 메서드에서 AOP 기능을 수행하도록 코드를 작성해보겠습니다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {}
com.example.aop.annotation
패키지를 생성하고 Timer
라는 어노테이션 파일을 만들어주었습니다.@Aspect
@Component
public class TimerAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
private void cut() {}
//사용자 지정 어노테이션이 붙은 메서드에도 적용!
@Pointcut("@annotation(com.example.aop.annotation.Timer)")
private void enableTimer() {}
//메서드 전 후로 시간 측정 시작하고 멈추려면 Before, AfterReturning으로는 시간을 공유 할 수가 없음 Around 사용!
@Around("cut() && enableTimer()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
//메서드 시작 전
StopWatch stopWatch = new StopWatch();
stopWatch.start();
//메서드가 실행되는 지점
Object result = joinPoint.proceed();
//메서드 종료 후
stopWatch.stop();
System.out.println("총 걸린 시간: " + stopWatch.getTotalTimeSeconds());
}
}
TimerAop
클래스를 작성해줍니다.@Timer
@DeleteMapping("/delete")
public void delete() {
//삭제 서비스 로직: 소요시간 3초
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
delete()
메서드를 작성해주고 위에서 만든 Timer
어노테이션을 붙여줍니다.결과
들어오는 값에 대해 동일한 전처리나 가공이 필요하면 AOP로 구현할 수 있습니다.
Filter나 Interceptor로 변환할 수는 있지만 스프링 부트의 내장 서버인 톰캣은 Body를 한번 읽으면 다시 못 읽게 막아놨기 때문에 변환하기가 어렵습니다. 하지만 AOP를 적용하면, 들어오는 객체나 값은 Filter와 Interceptor를 지나서 객체화되어 왔기 때문에 값을 변환해주기 비교적 쉽습니다.
혹은 암호화된 데이터가 들어오면 코드에서 복호화하지 않고 AOP에서 복호화 완료된 상태로 받을 수 있으며, 그 역의 작업도 가능합니다.
혹은 가공된 데이터를 보낼 때 특정 회원들이나 특정 서버에 보내고 싶을 때도 AOP를 사용할 수 있습니다.
데이터를 주고받을 때 암호화된 상태로 받는데, 코드에서 복호화하지 않고 AOP에서 복호화되어 컨트롤러 메서드로 떨어질 수 있게 AOP를 작성해 보겠습니다.
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decode {}
com.example.aop.annotation
패키지에 Decode
라는 어노테이션 파일을 만들어줍니다.@Aspect
@Component
public class DecodeAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
private void cut() {}
@Pointcut("@annotation(com.example.aop.annotation.Decode)")
private void enableDecode() {}
//암호화된 데이터 들어오면 복호화!
@Before("cut() && enableDecode()")
public void before(JoinPoint joinPoint) throws UnsupportedEncodingException {
//메서드로 들어오는 매개변수들
Object[] args = joinPoint.getArgs();
for(Object arg : args) {
if(arg instanceof User) {
//클래스 캐스팅
User user = User.class.cast(arg);
String base64Email = user.getEmail();
//복호화
String email = new String(Base64.getDecoder().decode(base64Email), "UTF-8");
user.setEmail(email);
}
}
}
//리턴할때는 암호화 하여 리턴
@AfterReturning(value = "cut() && enableDecode()", returning = "returnObj")
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
if(returnObj instanceof User) {
//클래스 캐스팅
User user = User.class.cast(returnObj);
String email = user.getEmail();
//암호화
String base64Email = Base64.getEncoder().encodeToString(email.getBytes());
user.setEmail(base64Email);
}
}
}
DecodeAop
코드를 작성해줍니다.@Decode
@PutMapping("/put")
public User put(@RequestBody User user) {
//서비스 로직
return user;
}
ApiController
에 put()
메서드를 작성해주었습니다.결과
DecodeAop
에 의해 암호화되는 것을 확인할 수 있습니다.