관점 지향 프로그래밍(Aspect-Oriented Programming, AOP)은 Spring 프레임워크의 핵심 프로그래밍 모델 중 하나다.
AOP는 Aspect(애스펙트)를 사용해 다양한 기능들을 분리한다. 핵심 기능과 부가 기능을 분리하고, 분리된 부가 기능을 어디에 적용할지 선택한다.
Aspect(애스펙트)
부가 기능을 어디에 적용할지 정의한 것이다.
분리한 부가 기능과 그 기능들을 어디에 적용할지 선택하는 기능을 합해 하나의 모듈로 만든 것이다.
액스펙트는 관점을 의미하는데, 애플리케이션을 바라보는 관점을 하나의 기능→횡단 관심사(cross-cutting concerns) 관점으로 보는 것이다.
많은 프로젝트는 OOP 패러다임을 지향하며 프로그래밍을 하고 있다.
OOP의 핵심은 공통 목적을 띄고 있는 데이터와 동작을 묶어 하나의 객체로 정의하는 것이다. 객체를 적극적으로 활용함으로써 기능을 재사용할 수 있는 것이 장점이다.
객체를 잘 활용하기 위해 관심사 분리(Separation of Concerns, SoC) 디자인 원칙을 준수해야 한다. 관심사 분리는 모듈화의 핵심이다.
AOP는 기존에 사용하던 OOP를 대체하기 위한 것이 아닌, 횡단 관심사를 깔끔하게 처리하기 위해 OOP의 부족한 부분을 보조하는 목적으로 개발되었다.
OOP만 사용해선 횡단 관심사 코드를 깔끔하게 분리하고 비즈니스 코드에 적용하기 어려웠다. 이러한 OOP의 관심사 분리에 대한 한계적인 부분을 해결하고자 AOP가 등장하게 되었다.
OOP의 모듈화의 핵심 단위는 클래스이고, AOP의 모듈화의 핵심 단위는 관점이다. Aspect는 여러 유형과 객체 간에 발생하는 문제의 모듈화를 가능하게 한다.
중요한 기본 개념
핵심 기능 : 업무 로직을 포함하는 기능
부가 기능 : 핵심 기능을 도와주는 부가 기능(로깅, 보안, 트랜잭션 등), 단독으로 사용되지 않으며 핵심 가능과 함께 사용됨
Aspect : 부가 기능을 정의한 코드인 어드바이스(Advice)와 어드바이스를 어디에 적용할지 결정하는 포인트컷(PointCut)을 합친 개념
(Advice + PointCut = Aspect)
AOP가 필요한 이유
소프트웨어 개발에서 변경 지점은 하나가 될 수 있도록 모듈화 되어야 한다.
부가 기능처럼 특정 로직을 애플리케이션 전반에 적용하는 문제는 OOP 방식으로 해결하기 어렵다. 핵심 기능과 부가 기능을 분리하는 AOP 방식이 필요한 이유다.
여러 객체에 공통으로 적용되는 기능이다. 어드바이스와 포인트컷을 모듈화해 애플리케이션에 포함되는 횡단 기능이다.
여러 어드바이스와 포인트컷이 함께 존재한다.
S : 메서드 실행 전의 포인트
E : 메서드 수행 후의 포인트
이러한 포인트를 어드바이스가 적용될 조인 포인트라고 부른다.
클래스의 초기화, 객체 인스턴스화, 메소드 호출, 필드 접근, 예외 발생과 같은 애플리케이션 실행 흐름에서의 특정 포인트를 의미한다.
횡단 관심은 조인 포인트 전후로 AOP에 의해 자동으로 추가된다.
스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메소드 실행 지점으로 제한된다.
조인 포인트에서 수행되는 코드를 의미하며 부가 기능에 해당한다.
애스펙트를 언제 핵심 코드에 적용할지를 정의하며 시스템 전체 애스펙트에 API 호출을 제공한다.
메소드를 호출하기 전에 각 상세 정보와 모든 메소드를 로그로 남기기 위해 메소드 시작 전의 포인트인 조인 포인트 S를 선택한다.
조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능이다.
AspectJ 표현식을 사용해 지정하며, 프록시를 사용하는 스프링 AOP는 메서드 실행 지점만 포인트컷으로 선별이 가능하다.
포인트컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는 것이다. 어드바이스를 핵심 코드에 적용하는 것을 의미한다.
핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가할 수 있다.
AOP 기능을 구현하기 위해 만든 프록시 객체다.
스프링에서 AOP 프록시는 JDK 동적 프록시 혹은 CGLIB 프록시다.
핵심 기능을 담고 있는 모듈로, 타겟은 부가 기능을 부여할 대상이 된다.
어드바이스를 받는 객체로 포인트컷으로 결정된다.
하나의 어드바이스와 하나의 포인트 컷으로 구성된다. 스프링 AOP에서만 사용되는 용어다.
어드바이스는 기본적으로 순서를 보장하지 않는다.
순서를 지정하고 싶으면 @Aspect
적용 단위로 org.springframework.core.annotation.@Order
애너테이션을 적용하면 된다. 어드바이스 단위가 아닌, 클래스 단위로 적용할 수 있으며 하나의 애스펙트에 여러 어드바이스가 존재할 경우 순서를 보장 받을 수 없다.
애스펙트를 별도의 클래스로 분리해야 한다.
조인 포인트 실행 전에 실행한다. 타겟 메서드가 실행되기 전에 처리해야 할 필요가 있는 부가 기능을 호출 전에 공통 기능을 ㅅ실행한다.
Befor Advice를 구현한 메서드는 일반적으로 리턴 타입이 void다.
에서드에서 예외를 발생시킬 경우 대상 객체의 메서드가 호출되지 않게 된다는 점이 있다.
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
작업 흐름을 변경할 수 없으며 메서드 종료 시 자동으로 다음 타겟이 호출된다.
조인 포인트가 정상 완료된 후 실행한다.
메서드가 예외 없이 실행된 후에 공통 기능을 실행한다.
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
메서드 실행이 정상적으로 반환될 때 실행된다.
returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 하고, returning 절에 지정된 타입의 값을 반환하는 메서드만 대상을 실행한다.
메서드가 예외를 던지는 경우 실행한다. 메서드를 실행하는 도중 예외가 발생한 경우 공통 기능을 실행한다.
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}
메서드 실행이 예외를 던져서 종료될 때 실행한다.
throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 하며, 지정된 타입과 맞는 예외를 대상으로 실행한다.
조인 포인트의 동작과는 상관없이 실행한다. 예외 동작의 finally를 생각하면 된다.
메서드 실행 후 공통 기능을 실행한다. 일반적으로 리소스를 해제하는데 사용한다.
메서드 호출 전후에 수행하며 가장 강력한 어드바이스다. 조인 포인트의 실행 여부 선택과 반환 값, 예외 변환 등이 가능하다.
메서드 실행 전후에 예외 발생 시점에 공통 기능을 실행한다.
try ~ catch ~ finally
가 들아간 구문 처리도 가능하다.
어드바이스의 첫 번째 피라미터는 ProceedingJoinPoint
를 사용해야 한다. proceed()
를 통해 대상을 실행하고, 여러 번 실행할 수 있다.
@Around만 있어도 모든 기능 수행이 가능하다.
가장 강력한 어드바이스이고, 대부분의 기능을 제공하나 타겟 등 고려할 사항이 있을 때 정상적으로 작동되지 않는 경우가 있다.