@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;
}
GET 방식으로 request
get 메서드 실행
type : Long
value : 100
type : String
value : paul
return obj
100 paul
POST 방식으로 request
post 메서드 실행
type : User
return obj
value : User{id='paul', pw= '1234', email='paulkim1997@naver.com '}
User{id= paul', pw= '1234', email= paulkim1997@naver.com '}
2. 메서드 실행 시간을 알고싶은 경우
TimeTraceAop 클래스 작성
@Component
@Aspect
public class TimeTraceAop {
@Around("execution(* study.studySpring..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
// 시작 전
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try {
// 메서드 시작 시점
return joinPoint.proceed();
} finally {
// 메서드 종료 시점
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: " + joinPoint.toString()+ " " + timeMs + "ms");
}
}
}
로그
첫 START-END는 spring bean에 등록되면서 뜬 것이다.
나머지는 MemberController→MemberService→repository 순으로 동작하고 각 동작 시간을 측정한 것이다.
Spring AOP 동작 방식
기본적으로 프록시 방식으로 동작한다.
프록시 패턴: 어떤 객체를 사용하고자 할 때, 객체를 직접 참조하는 것이 아니라, 해당 객체를 대행(대리, proxy)하는 객체를 통해 대상 객체에 접근하는 방식을 말한다.
AOP 적용 전 의존 관계
memberController가 memberService를 의존하고 있다.
때문에 memberController를 호출하면 memberService도 메서드를 호출한다.
프록시 객체가 모두 실행되면 이후에 joinPoint.proceed() 가 실행되고 나서야 실제 객체가 실행된다.
때문에 controller가 호출하는 service는 프록시 service인 것이다.(이후의 의존 관계에서도 마찬가지)
왜 Spring AOP는 프록시 방식을 사용하는가?
프록시 객체가 없이 memberService를 사용하면, Aspect에 정의된 부가 기능을 사용하기 위해 원하는 위치에 직접 Aspect 클래스를 호출해야한다.
이런 경우 Target 클래스 안에 부가 기능을 호출하는 로직이 포함되기 때문에, AOP를 적용하는 의미가 없다.
즉, 여러 곳에서 반복적으로 Aspect를 호출해야 하고, 그로 인해 유지보수성이 크게 떨어진다.
이러한 이유로 Spring에서는 Target 클래스 혹은 그의 상위 인터페이스를 상속하는 프록시 클래스를 생성하고, 프록시 클래스에서 부가 기능에 관련된 처리를 한다.
CGLib Proxy
클래스 기반으로 바이트코드를 조작하여 프록시를 생성하는 방식
클래스 기반이기 때문에 인터페이스 기반의 JDK Dynamic Proxy보다 성능이 우수하다고 한다.
memberService를 MemberController에 의존성 주입되는 시점에 실제로 memberService.getClass()로 로그를 찍어보면 그냥 memberService가 아닌 CGLIB에 의해 생성된 프록시 객체임을 알 수 있다.
JDK Dynamic Proxy vs CGLIB Proxy
JDK Dynamic Proxy
proxy 생성을 위해 interface가 필요하다.
Refelction을 이용해 proxy를 생성한다.
JDK에서 지원하는 프록시 생성 방법
외부 라이브러리에 의존하지 않는다
프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트
프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스 오브젝트를 자동으로 생성
인터페이스가 반드시 존재해야한다
Invocation Hanlder를 재정의한 invoke 코드를 직접 구현해줘야 부가기능이 추가된다
CGLIB Proxy
바이트 코드를 조작해 프록시 생성
Hibernate의 lazy loading, Mockito의 모킹 메서드 등에서 사용
프록시 팩토리에 의해 런타임 시 다이나믹하게 만들어지는 오브젝트
클래스 상속을 이용하여 프록시 구현. 인터페이스가 존재하지 않아도 가능
바이트 코드를 조작해서 프록시 생성함
인터페이스에도 강제로 적용 가능. 이 경우 클래스에도 프록시를 적용해야 한다
Dynamic Proxy 보다 약 3배 가까이 빠르다
메서드가 처음 호출되었을 때 동적으로 타깃 클래스의 바이트 코드를 조작
이후 호출 시엔 조작된 바이트 코드를 재사용
MethodInterceptor를 재정의한 intercept를 구현해야 부가 기능이 추가된다
메서드에 final을 붙이면 오버라이딩이 불가능
Spring AOP와 AspectJ
Spring AOP
런타임 시점에 동적으로 변할 수 있는 프록시 객체를 이용하기에 앱 성능에 영향을 끼칠 수 있다.
AspectJ
AspectJ 는 자바에서 완벽한 AOP 솔루션 제공을 목표로하는 기술이다.
[ .aj 파일]을 이용한 assertj 컴파일러를 추가로 사용하여 컴파일 시점이나 JVM 클래스 로드시점에 조작한다.
런타임 시점에는 영향끼치지 않는다. 즉 컴파일이 완료된 이후에는 앱 성능에 영향이 없다.
차이 비교
1. 기능과 목표가 다르다.
Spring AOP는 프로그래머가 직면하는 일반적인 문제 해결을 위해 Spring IoC에서 제공하는 간편한 AOP 기능이다. 어디에나 쓸 수 있는 완벽한 AOP 솔루션이 아니라, [Spring 컨테이너가 관리하는 Bean]에만 사용하려고 만들었고, 실제로 여기에만 AOP를 적용 할 수 있다.
AspectJ는 [자바코드에서 동작하는 모든 객체]에 대해 완벽한 AOP 솔루션 제공을 목표로 하는 기술이다. 성능이 뛰어나고 기능이 매우 강력하지만 그만큼 Spring AOP에 비해 사용방법이나 내부 구조가 훨씬 더 복잡하다.
2. Weaving 방법이 다르다.
Weaving은 공통관심사항(Aspect)의 동작코드(Advice)를 대상 객체(Target)에 연결시켜 관점지향을 구현한 객체로 만드는 과정이다. 좀 더 쉽게말하면 AOP를 구현하기 위한 바이트코드 조작 방법을 의미한다고 생각하면 된다.
Spring AOP는 위에서 설명한 런타임 위빙(다이나믹 프록시)를 사용한다.
자바의 리플렉션 API와 CGlib등의 도구로 런타임시 동적인 프록시 객체를 만든다.
동적인 프록시 객체란, 클래스의 정보에 따라 런타임시에 다르게 구현되도록 만드는 객체를 의미한다.
복잡한 설정없이 Spring 빈 등록을 하게되면 자동으로 등록되어 사용하기 매우 편하다.
근데 런타임에 바이트코드를 조작하는 만큼, 오버헤드가 심하고 성능에 영향을 크게 미칠 수 있다.
벤치마킹상 AspectJ가 Spring AOP보다 최소 8배, 최대 35배정도 빠르다
AspectJ는 3가지 유형의 Weaving을 제공한다.
Compile-Time 위빙 : AspectJ 전용 컴파일러를 이용하여 Aspect 부분과 Target 코드 부분을 입력으로 받고 하나로 엮인 바이트코드(.class)를 생성한다. 컴파일이 완료된 이후에는 앱의 성능에 전혀 영향을 끼치지 않는다.
Post-Complie 위빙 : 외부 라이브러리를 Weaving 할 때 사용한다. [Compile-Time 위빙]과 거의 동일한 동작을 한다. 보통 클래스파일을 JAR와 엮기 위해서 사용해서 [Binary 위빙]이라고 부르는게 일반적이다.
Load-time 위빙 : 전용 컴파일러를 사용하지않고, 조작되지 않은 바이트코드(.class)를 가 JVM에 로드 될 때 ClassLoader를 이용하여 바이트코드를 조작하는 위빙 방식이다. 객체를 로드할 때 위빙이 일어나는 거라 앱 성능의 하락을 발생시킬 수 있다.
결론
Spring AOP가 성능과 기능은 매우 부족하지만, Spring Bean에 자동으로 적용되고 설정하기 매우 편리하다. 또한 AspectJ와 다르게 컴파일 시점에 건드리는게 없어서 각종 라이브러리(Lombok)과 호환성이 뛰어나다.
하지만 Spring AOP는 컨테이너 안의 Bean만 조작할 수 있고, JoinPoint를 메서드호출시점밖에 적용 못한다는 단점이 있다. 반면에 AspectJ는 런타임이 아닌 컴파일 시점에 동작하는 도구라서 다음과 같이 다양한 시점을 지정할 수 있다.