Plain Old Java Object약자, 관점 지향 프로그래밍
관점 지향이란 핵심 로직과 부가 기능을 분리하여 애플리케이션 전체에 걸쳐 사용되는 부가 기능을 모듈화 하여 재사용할 수 있드록 지원하는 것이다.
→ 즉 AOP는 흩어진 관심사(Crosscutting Concerns)를 모듈화 할 수 있는 프로그래밍 기법이다.
이처럼 각각의 Service의 핵심 기능에서 바라보면 User와 Order는 공통된 요소가 없다.
이 처럼 부가 기능 관점에서 보면 공통된 요소가 보인다.
그 공통된 요소는 각각의 Service의 get메소드를 호출하는 전후에 before()와 after() 라는 메소드가 공통되는 것을 확인할 수 있다.
즉, 기존에 OOP에서 바라보던 관점을 다르게 하여 부가 기능적인 측면에서 공통된 요소를 추출하는 것이다.
→ 이걸 가로 영역의 공통된 부분을 잘라낸다고 하여 AOP를 Cross-Cutting 이라고도 부른다.
여기서 OOP와 AOP를 보자면
OOP : 비즈니스 로직의 모듈화
AOP : 인프라 혹은 부가기능의 모듈화
정리를 하자면, AOP는 공통된 기능을 재사용하는 기법. OOP에선 공통된 기능을 재사용하는 방법으로 상속 이나 위임을 사용한다.
그런데, 전체 애플리케이션에서 여러 곳에서 사용되는 부가 기능들은 상속이나 위임으로 처리하기엔 깔끔한 모듈화가 어렵다. →대처 : AOP
간단하게 설명하면 JDK 동적 프록시는 인터페이스를 구현해서 프록시 생성, CGLIB는 클래스를 상속 받아 프록시 생성한다.
인터페이스가 없다면 당연히 CGLIB로 동작하지만, 인터페이스가 있는 경우라면 CGLIB과 JDK 동적 프록시 중에서 선택할 수 있다.
JDK 동적 프록시는 구체 클래스로 타입 캐스팅이 불가능하다.
MemberServiceImpl을 대상으로 프록시를 생성한다면 인터페이스인 MemberService를 기반으로 구현체(프록시)로 만들어 빈으로 등록한다.
그래서 프록시는 MemberService로는 타입 캐스팅이 가능하나, MemberServiceImpl로는 타입 캐스팅이 불가능하다.
CGLIB는 구체 클래스로 타입 캐스팅이 가능하다.
CGLIB는 구체 클래스를 상속받아 프록시를 생성한다. 따라서 MemberServiceImpl을 프록시 대상으로 선택하면, MemberServiceImpl을 상속 받아서 프록시를 만들어 빈으로 등록 하기 때문에 프록시는 당연히 MemberServiceImpl로 타입 캐스팅이 가능하다.
그래서, JDK 동적 프록시 설정으로 돌리면 빈에 등록된 프록시는 MemberServiceImpl로 타입 캐스팅이 불가능해 의존관계 주입에 실패한다.
반면에 CGLIB를 사용하면 타입 캐스팅이 가능하기 때문에 의존관계 주입이 가능하다.
정리하면, CGLIB는 구체 클래스가 AOP의 대상이 되고, JDK 동적 프록신느 구체 클래스가 AOP의 대상이 되지 못한다.
그런데 CGLIB의 단점이 있습니다.
이 단점들은 spring에서 해결해서 Spring 3.2 버전부터 CGLIB를 Spring Core 패키지에 포함시켜 더이상 의존성을 추가하지 않아도 개발할 수 있게 되었다.
4버전에선 Objensis 라이브러리의 도움을 받아 default 생성자 없이도 Proxy를 생성할 수 있게 되었고, 생성자 2번 호출 되던 상황도 개성이 되었다.
그래서 Spring에선 CGLIB가 기본값으로 Proxy를 생성한다.
프록시 패턴에서는 interface가 존재하고 Client는 이 interface 타입으로 프록시 객체를 사용한다. 프록시 객체는 기존의 타겟 객체(Real Subject)를 참조한다. 프록시 객체와 기존의 타겟 객체의 타입은 같고,프록시는 원래 할 일을 가지고 있는 Real Subject를 감싸서 Client의 요청을 처리하는 것이다.
Spring AOP를 사용하려면 의존성을 추가해줘야 한다.
maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
gradle
implementation 'org.springframework.boot:spring-boot-starter-aop'
해당 의존성을 추가하면 자동 프록시 생성기(AnnotationAwareAspectJAutoProxyCreator)를 사용할 수 있게 된다. 이 생성기는 Advisor 기반으로 프록시를 생성하는 역할 한다. 그리고 자동 프록시 생성기는 @Aspect를 보고 Advisor로 변환해서 저장하는 작업을 한다.
이 자동 프록시 생성기에 의해 @Asepct에서 Advisor로 변환된 Advisor는 @Aspect Advisor 빌더 내부에 저장된다.
Advisor 빈을 조회하고 이후에 @Aspect Advisor 빌더 내부에 저장된 모든 Advisor를 조회하는 로직이 추가된 것을 확인할 수 있다.
@Aspect는 Advisor를 쉽게 만들 수 있도록 도와주는 역할을 할 뿐, 컴포넌트 스캔이 되는게 아니기때문에 반드시 스프링 빈으로 등록해줘야 한다.
3가지 방법으로 등록한다.
@Bean
으로 수동 등록@Component
로 컴포넌트 스캔을 사용해서 자동 등록@Import
를 사용해서 파일 추가Advice는 실질적으로 프록시에서 수행하게 되는 로직을 정의하게 되는 곳이다. 스프링에서는 Advice에 관련된 5가지 어노테이션을 제공하는데, 이 어노테이션은 메소드에 붙여서 사용한다.
해당 메소드는 advice의 로직을 정의하게 되고, 어노테이션의 종류에 따라 포인트컷에 지정된 대상 메소드에서 Advice가 실행되는 시점을 정할 수 있다. 또한 속성값으로 포인트컷을 지정 할 수 있다.
@Around
@Before
@AfterReturning
@AfterThrowing
@Afeter
사용하는 예제 코드를 보면
@Component
@Aspect
public class PerfAspect {
@Around("execution(* com.example..*.EventService.*(..))")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object reVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return reVal;
}
}
스프링 AOP는 빈에서만 동작한다. 따라서 아까 말한 세가지 방법 중 선택해서 스프링 빈으로 등록해준 뒤 사용하면 된다. @Aspect 어노테이션을 붙이면 해당 클래스가 Aspect라는 것을 명시해준다.
logPerf()메소드는 @Around 어노테이션의 execution을 통해 Advice를 적용할 범위를 지정할 수 있다.
예제 코드로 보면 com.example 밑의 모든 클래스에 적용하고, EventService 밑의 모든 메서드에 적용한다
다른 코드를 보면
@Component
@Aspect
public class PerfAspect {
@Around("@annotation(PerfLogging)")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
위와 같이 @Around 어노테이션에 @annotation(PerfLogging)처럼 적용될 어노테이션을 명시할 수 있다.
그럼 해당 메소드를 적용시킬 특정 메소드에 @PerfLogging 어노테이션을 붙여주기만 하면 logPerf() 기능이 동작한다.
이 코드는 Bean 전체에 기능을 적용 시킨다.
@Component
@Aspect
public class PerfAspect {
@Around("bean(simpleServiceEvent)")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
@Around 어노테이션에 bean(simpleServiceEvent)처럼 적용될 빈을 명시할 수 있다.
이렇게 하면 해당 빈이 가지고 있는 모든 public 메서드에 해당 기능이 적용된다.
이거 말고도 사용법 예시는 무수히 많다
코드 정의와 공정한 소프트웨어를 추구함에 있어서 관점 지향 프로그래밍은 중요한 역할을 한다.
즉, AOP는 공통 관심사의 체계적인 적용을 통해 코드의 균형 잡힌 측면을 달성하려는 목표를 가지고 있다.
AOP 유형을 이해하고 응용 프로그램의 라이프사이클 다양한 지점에서 어떻게 개입하는지 파악함으로 개발자는 코드 베이스 내에서 기능 및 비 기능적 측면 사이의 조화로운 균형을 유지할수 있다.
https://github.com/cs-wiki/cs-wiki/issues/9
https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html
https://jaimemin.tistory.com/2025
https://docs.spring.io/spring-framework/reference/core/aop.html
https://code-lab1.tistory.com/193
https://velog.io/@backtony/Spring-AOP-총정리#cglib과-jdk-동적-프록시-중-spring의-선택