AOP(Aspect-Oriented Programming)은 소프트웨어 개발에서 관심사(Concern)를 모듈화하고 분리하는 프로그래밍 패러다임이다. AOP는 주로 횡단 관심사(Cross-Cutting Concerns)를 처리하는 데 사용되며, 예를 들어 로깅, 트랜잭션 관리, 보안, 예외 처리와 같은 부가적인 작업들을 원래의 비즈니스 로직에서 분리하여 모듈화할 수 있게 한다.
Spring의 핵심 개념중 하나인 DI가 애플리케이션 모듈들 간의 결합도를 낮춘다면, AOP(Aspect-Oriented Programming)는 핵심 로직과 부가 기능을 분리하여 애플리케이션 전체에 걸쳐 사용되는 부가 기능을 모듈화하여 재사용할 수 있도록 지원하는 것이다.
Aspect-Oriented Programming이란 단어를 번역하면 관점(관심) 지향 프로그래밍이 된다. 즉, 프로젝트 구조를 바라보는 관점을 바꿔보자는 의미이다.
각각의 Service의 핵심 기능에서 바라보았을 때 User와 Order는 공통된 요소가 없다. 하지만 부가기능 관점에서 바라보면 이야기가 달라진다.
부가기능 관점에서 바라보면 각각의 Service의 getXX 메서드를 호출하는 전후에 before와 after라는 메서드가 공통되는 것을 확인할 수있다.
기존에 OOP에서 바라보던 관점을 다르게 하여 부가기능적인 측면에서 보았을 때 공통된 요소를 추출하자는 것이다. 이때 가로 영역의 공통된 부분을 잘라냈다고 하여, AOP를 크로스 컷팅(Cross-Cutting)이라고 부르기도 한다.
간단하게 정리해보자면, AOP는 공통된 기능을 재사용하는 기법이다.
OOP에서는 공통된 기능을 재사용하는 방법으로 상속이나 위임을 사용한다. 하지만 전체 애플리케이션에서 여기저기 사용되는 부가기능들은 상속이나 위임으로 처리하기에는 깔끔한 모듈화가 어렵다. 그렇기에 등장한 것이 AOP다.
애플리케이션 전체에 흩어진 공통 기능이 하나의 장소에서 관리되어 유지보수가 좋다.
핵심 로직과 부가 기능의명확한 분리로, 핵심 로직은 자신의 목적 외에 사항들에는 신경쓰지 않는다.
AOP 적용 방식은 크게 3가지 방법이 있다.
.java 파일을 컴파일러를 통해 .class를 만드는 시점에 부가 기능 로직을 추가하는 방식
모든 지점에 적용 가능하지만, AspectJ가 제공하는 특별한 컴파일러를 사용해야 하기 때문에 특별한 컴파일러가 필요한 점과 복잡하다는 단점이 있다.
.class 파일을 JVM 내부의 클래스 로더에 보관하기 전에 조작하여 부가 기능 로직을 추가하는 방식
모든 지점에 적용 가능하지만, 특별한 옵션과 클래스 로더 조작기를 지정해야하므로 운영하기 어렵다.
스프링이 사용하는 방식으로 컴파일이 끝나고 클래스 로더에 이미 다 올라가 자바가 실행된 다음에 동작하는 런타임 방식이다.
실제 대상 코드는 그대로 유지되고 프록시를 통해 부가 기능이 적용된다. 프록시는 메서드 오버라이딩 개념으로 동작하기 때문에 메서드에만 적용 가능하다.(스프링 빈에만 AOP를 적용 가능)
스프링 빈이란
스프링 컨테이너에 의해 관리되는 객체를 말한다. 스프링은 애플리케이션 구성 요소를 Java 객체로 정의하고 관리할 수 있도록 도와주는 프레임워크이며, 이러한 객체를 스프링 빈으로 관리한다.
특별한 컴파일러나, 복잡한 옵션, 클래스 로더 조작기를 사용하지 않아도 스프링만 있으면 AOP를 적용할 수 있기 때문에 스프링 AOP는 런타임 방식을 사용한다.
스프링 AOP는 AspectJ 문법을 차용하고 프록시 방식의 AOP를 제공한다. 스프링에서는 AspectJ가 제공하는 어노테이션이나 관련 인터페이스만 사용하고 실제로 AspectJ가 제공하는 컴파일, 로드타임 위버 등은 사용하지 않는다. 따라서 스프링 AOP는 AspectJ를 직접 사용하는 것은 아니다.
Join Point: 추상적인 개념으로 advice가 적용될수 있는 모든 위치(메서드 실행시점, 생성자 호출 시점, 필드값 접근 시점 등)를 말한다.
스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메서드 실행 지점이다.
Pointcut: 조인 포인트 중에서 advice가 적용될 위치를 선별하는 기능이다.
스프링 AOP는 프록시 기반이기 때문에 조인 포인트가 메서드 실행 시점밖에 없기 때문에 포인트컷도 메서드 실행 시점에만 가능하다.
Target: advice의 대상이 되는 객체로 Pointcut으로 결정한다.
Advice: 실질적인 부가 기능 로직을 정의하는 곳이다. 특정 조인 포인트에서 Aspect에 의해 취해지는 조치다.
Advisor: 스프링 AOP에서만 사용되는 용어로 advice + pointcut 한 쌍을 의미한다.
Weaving: pointcut으로 결정한 타겟의 join point에 advice를 적용하는 것을 의미한다.
AOP 프록시: AOP 기능을 구현하기 위해 만든 프록시 객체다.
스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시다. 스프링 AOP의 기본값은 CGLIB 프록시다.
@Aspect 어노테이션을 사용한다면 Advisor를 더욱 쉽게 구현할 수 있다. 스프링 AOP를 사용하기 위해서는 다음과 같은 의존성을 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
해당 의존성을 추가하게 되면 자동 프록시 생성기(AnnotationAwareAspectJAutoProxyCreator)를 사용할 수 있게 되고 이것이 Advisor 기반으로 프록시를 생성하는 역할을 한다.
이와 더불어, 자동 프록시 생성기는 @Aspect를 보고 Advisor로 변환해서 저장하는 작업을 수행한다.
자동 프록시 생성기에 의해 @Aspect에서 Advisor로 변환된 Advisor는 @Aspect Advisor 빌더 내부에 저장된다.
자동 프록시 생성기에 의해 생성된 Advisor는 기존 로직에서 어느 시점에 끼어드는지 보자.
스프링 빈 대상이 되는 객체를 생성한다.(@Bean, 컴포넌트 스캔 대상)
생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
모든 Advisor 빈을 조회한다.
@Aspect Advisor 빌더 내부에 저장된 모든 Advisor를 조회한다.
3, 4에서 조회한 Advisor에 포함되어 있는 포인트컷을 통해 클래스와 메서드 정보를 매칭하면서 프록시를 적용할 대상인지 아닌지 판단한다.
여러 Advisor의 하나라도 포인트컷의 조건을 만족한다면 프록시를 생성하고 프록시를 빈 저장소로 반환한다.
만약 프록시 생성 대상이 아니라면 들어온 빈 그대로 빈 저장소로 반환한다.
빈 저장소는 객체를 받아서 빈으로 등록한다.
Advisor 빈을 조회하고 이후에 @Aspect Advisor 빌더 내부에 저장된 모든 Advisor를 조회하는 로직이 추가된 것을 확인할 수 있다.
@Aspect는 Advisor를 쉽게 만들 수 있도록 도와주는 역할을 하는 것이지 컴포넌트 스캔이 되는 것은 아니기 때문에 반드시 스프링 빈으로 등록해줘야 한다.
명시적 빈 등록: @Bean 어노테이션을 사용하여 빈을 직접 등록
@Component 스캔: 스프링 애플리케이션을 시작할 때 @SpringBootApplication 어노테이션이 있는 클래스의 패키지와 하위 패키지에서 @Component, @Service, @Repository, @Controller 등과 같은 어노테이션을 가진 클래스들을 자동으로 스캔하여 스프링 빈으로 등록
@Import를 사용해서 파일 추가
Advice는 실질적으로 프록시에서 수행하게 되는 로직을 정의하게 되는 곳이다.
스프링에서는 Advice에 관련된 5가지 어노테이션을 제공한다. 어노테이션은 메서드에 붙이게 되는데 해당메서드는 Advice의 로직을 정의하게 되고, 어노테이션의 조율에 따라 포인트컷에 지정된 대상 메서드에서 Advice가 실행되는 시점을 정할 수 있다. 또한 속성값으로 Pointcut을 지정할 수 있다.
@Slf4j
@Aspect
public class AspectV6Advice {
@Around("execution(* com.example.mvc.order..*(..))")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
// @Before 수행
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
// @Before 종료
// Target 메서드 호출
Object result = joinPoint.proceed();
// Target 메서드 종료
// @AfterReturning 수행
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
// @AfterReturning 종료
// 값 반환
return result;
} catch (Exception e) {
// @AfterThrowing 수행
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
// @AfterThrowing 종료
} finally {
//@ After 수행
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
//@ After 종료
}
}
}
@Around 어노테이션을 제외한 나머지 4개의 어노테이션은 @Around 어노테이션의 기능을 Target 메서드 실행 전, 후, 예외, 후에 무조건(finally)으로 분리한 것이다.
모든 Advice 어노테이션은 첫 번째 파라미터로 org.aspectj.lang.JoinPoint를 사용할 수 있는데 JoinPoint의 인터페이스의 주요 기능은 다음과 같다.
@Around를 제외한나머지 4개의 어노테이션은 보통 인자로 JoinPoint를 받아서 사용하고, proceed를 호출하지 않는다. 즉, @Around의 경우 ProceedingJoinPoint를 인자로 받아 타겟 메서드를 실행하는 proceed 코드를 반드시 적어야 target 메서드를 호출하지만 나머지 4개의 어노테이션의 경우, target 메서드를 호출하는 proceed를 명시하지 않아도 알아서 호출된다.
이렇게 어노테이션을 분리함으로서 의도를 명확하게 판단할 수 있다. 하나 더 큰 차이가 있다면 @Around는 입력, 반환값 자체를 다른 객체로 조작이 가능하지만, 나머지 4개의 어노테이션의 경우 입력, 반환값, 객체 자체를 다른객체로 조작할 수 없다.
개념만 알아보려 했는데 내용이 너무 방대하고 어쩌다보니 너무 깊이 들어온 것 같아서 여기까지만 알아보기로 했다..