AOP
는 스프링의 가장 중요한 세 가지 특징인 IoC(제어의 역전)
, DI(의존성 삽입)
, AOP(관점 지향 프로그래밍)
중 하나이다.
AOP
는 OOP
를 대신하는 새로운 개념이 아니라, 기존 OOP
를 더욱 보완, 확장하여 OOP
를 OOP
답게 사용할 수 있도록 도와주는 개념이다.
자바에서 AOP도구 중 대표적으로는 AspectJ, JBossAOP, SpringAOP가 존재 한다.
OOP
를 통해서 지금까지 객체를 재사용함으로써 개발자들은 반복되는 코드의 양을 굉장히 많이 줄일수가 있었다.
하지만 AOP
도입 전까지는 객체의 재사용에도 불구하고 필수적으로 반복되는 코드를 없앨수는 없었다.
트랜잭션, 로그, 권한 체크, 인증, 예외 처리 등
AOP의 필요성을 이해하는 가장 기초가 되는 개념은 '관심의 분리(Separation of Concerns)' 이다.
기능을 비지니스 로직과 공통 모듈로 구분한 후에 개발자의 코드 밖에서 필요한 시점에 비지니스 로직에 삽입하여 실행되도록 한다.
핵심관점(비즈니스 로직) + 횡단관점(트랜잭션, 로그, 권한 체크, 인증, 예외 처리 등)으로 관심의 분리를 실현
즉, OOP
에서는 공통적인 핵심 기능을 각 객체의 종단으로 입력했다면, AOP
는 핵심 기능에서 중복되는 공통적인 기능을 종단간으로 삽입할 수 있도록 한 것이다.
DI가 의존성(new)의 주입이라면, AOP는 기능(logic)의 주입이라 할 수 있다.
AOP
를 통해서 중복 코드 제거, 효율적인 유지보수, 높은 생산성, 재활용성 극대화, 변화 수용 용이 등의 이점을 얻을 수 있다.
위 그림에서 처럼 계좌이체
, 입출금
, 이자계산
이라는 로직을 처리할 때, 모두 똑같이 로깅
, 보안
, 트랜잭션
을 처리해줘야한다.
따라서 모든 로직에 똑같은 코드가 반복적으로 삽입될 수 밖에 없다.
하지만 AOP에서는 로깅
, 보안
, 트랜잭션
이라는 공통 관심(Aspect)을 따로 빼내어 계좌이체
, 입출금
, 이자계산
이라는 핵심 관심에 횡단으로 삽입해 주는 것이다.
이렇게 각 객체별로 처리했던 것을 각 관점별로 외부에서 접근을 하는것이 AOP의 핵심이다.
즉 개발자는 계좌이체
, 입출금
, 이자계산
과 같은 핵심 기능을 만들고, 공통적인 관심 기능(로깅
, 보안
, 트랜잭션
)을 처리하는 모듈을 분리해서 개발한 후, 필요한 시점에 자동으로 공통적인 관심 기능이 삽입되도록 하는것이다.
Aspect
Advice와 Pointcut을 합쳐서 하나의 Aspect라고 한다.
여러 객체에 공통으로 적용되는 기능으로, 즉 일정한 패턴을 가지는 클래스에 Advice를 적용하도록 지원할 수 있는 것을 Aspect라고 한다.
트랜잭션 기능/로그 기능/보안 기능/인증 기능 등
Weaving
AOP에서 Joinpoint들을 Advice로 감싸는 과정을 Weaving이라고 한다.
Weaving 하는 작업을 도와주는 것이 AOP 툴이 하는 역할이다.
Advice
Joinpoint에서 실행되어야 하는 코드, 공통 관심, 횡단 관점에 해당한다.
언제, 어떤 공통 관심 기능을 핵심 로직에 적용할 지를 정의 하고 있다.
[EX] 메소드 호출 전(언제)에 트랜잭션 시작(공통 기능)기능을 적용한다는 것을 정의
Target
실질적인 비지니스 로직을 구현하고 있는 코드, 핵심 관점에 해당한다.
Advice를 받을 대상, 즉 객체로 비지니스 로직을 수행하는 클래스일수도 있지만, 프록시 객체(Object)가 될 수도 있다.
Pointcut
Joinpoint의 부분 집합으로서 실제 Advice가 적용되는 Joinpoint를 나타낸다.
스프링에서는 정규식이나 AspectJ 문법을 이용해, Target 클래스와 Advice가 결합(Weaving)될 때 둘 사이의 결합 규칙을 정의할 수 있다.
[EX] Advice가 실행된 Target의 특정 메소드등을 지정
Pointcut을 이용하면 Advice 메소드가 적용될 비즈니스 메소드를 정확하게 필터링 할 수 있다.
execution
: 가장 정교한 Pointcut을 만들수 있고, 리턴타입 패키지경로 클래스명 메소드명(매개변수)
within
: 타입패턴 내에 해당하는 모든 것들을 Pointcut으로 설정
bean
: bean이름으로 Pointcut
표현식 | 설명 |
---|---|
* | 모든 리턴타입 허용 |
void | 리턴타입이 void인 메소드 선택 |
!void | 리턴타입이 void가 아닌 메소드 선택 |
표현식 | 설명 |
---|---|
com.gil.demo.controller | com.gil.demo.controller 패키지만 선택 |
com.gil.demo.controller.. | com.gil.demo.controller 패키지로 시작하는 모든 패키지 선택 |
표현식 | 설명 |
---|---|
MemberDTO | 정확하게 MemberDTO 클래스만 선택 |
*DTO | 이름이 DTO로 끝나는 클래스만 선택 |
BaseObject+ | 클래스 이름 뒤에 '+'가 붙으면 해당 클래스로부터 파생된 모든 자식 클래스 선택, 인터페이스 이름 뒤에 '+'가 붙으면 해당 인터페이스를 구현한 모든 클래스 선택 |
표현식 | 설명 |
---|---|
*(..) | 모든 메소드 선택 |
update*(…) | 메소드명이 update로 시작하는 모든 메소드 선택 |
표현식 | 설명 |
---|---|
(..) | 모든 매개변수 |
(*) | 반드시 1개의 매개변수를 가지는 메소드만 선택 |
(com.gil.demo.dto.MemberDTO) | 매개변수로 MemberDTO를 가지는 메소드만 선택. 꼭 풀패키지명을 명시해줘야 함 |
(!com.gil.demo.dto.MemberDTO) | 매개변수로 MemberDTO를 가지지 않는 메소드만 선택 |
(Integer,…) | 한개 이상의 매개변수를 가지되, 첫번째 매개변수의 타입이 Integer인 메소드만 선택 |
(Integer, *) | 반드시 두 개의 매개변수를 가지되, 첫번째 매개변수의 타입이 Integer인 메소드만 선택 |
Spring에서는 프록시를 이용해서 메소드 호출 시점에 Aspect를 적용하기 때문에 구현 가능한 Advice의 종류는 다음과 같다.
Before Advice
대상 객체의 메소드 호출 전에 공통 기능을 실행한다.
After Advice
익셉션 발생 여부에 상관없이 대상 객체의 메소드 실행 후 공통 기능을 실행한다.
try - catch - finally의 finally 블록과 비슷하다.
After Returning Advice
대상 객체의 메소드가 익셉션 없이 정상적으로 실행된 이후에 공통 기능을 실행한다.
After Throwing Advice
대상 객체의 메소드를 실행하는 도중 익셉션이 발생한 경우에 공통 기능을 실행한다.
Around Advice
대상 객체의 메소드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능을 실행하는데 사용된다.
이 중 Around Advice가 널리 사용되는데, 대상 객체의 메소드를 실행 하기 전/후, 익셉션 발생 시점 등 다양한 시점에 원하는 기능을 삽입할 수 있기 때문이다.
캐시 기능, 성능 모니터링 기능과 같은 Aspect를 구현할 때에는 Around Adivce를 주로 이용한다.
어드바이스 메소드를 의미있게 구현하려면 클라이언트가 호출한 비즈니스 메소드의 정보가 필요하다.
이럴때 JoinPoint 인터페이스가 제공하는 API들을 사용한다.
예외가 발생하였을때, 예외가 발생한 메소드 이름 등을 기록
메소드 | 설명 |
---|---|
Signature getSignature() | 클라이언트가 호출한 메소드의 시그니처(리턴타입, 매개변수) 정보가 저장된 Signature 객체를 리턴 |
Object getTarget() | 클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체를 리턴 |
Object[] getArgs() |
메소드 | 설명 |
---|---|
String getName() | 클라이언트가 호출한 메소드 이름 리턴 |
String toLongString() | 클라이언트가 호출한 비즈니스 메소드의 리턴타입, 이름, 매개변수(시그니처)를 패키지 경로까지 포함하여 리턴) |
String toShortString() | 클라이언트가 호출한 메소드 시그니처를 축약한 문자열로 리턴 |
String getDeclaringTypeName() | 클라이언트가 호출한 메소드를 가지는 클래스 풀패키지명을 리턴 |
Advice를 핵심로직코드에 적용하는것을 의미하며 3가지 방식이 존재한다.
일반적으로 컴파일시와 클래스 로딩 시에 weaving하는 방식은 AspectJ 라이브러리를 추가하여 구현할때 사용된다.
Compile-time Weaving
Load-time에 대한 절차가 없어서 퍼포먼스 하락 없이 구성이 가능하다.
Lombok과 같이 compile시 간섭하는 plugin들과 충돌이 발생한다.
Class Load-time Weaving
applicationContext에 로드된 객체들을 불러온 뒤, AspectJ weaver에 의해 객체들을 weaving한다.
객체들을 전부 불러온 뒤 weaving을 하기 때문에 약간의 퍼포먼스 하락이 있다.
Run-time weaving
Spring AOP에서 사용하는 방식으로 소스코드나 클래스 정보 자체를 변경하지 않고 중간에 프록시 객체를 생성하여 AOP를 적용한다.
스프링은 Aspect의 적용 대상(Target)이 되는 객체에 대한 Proxy
를 만들어 제공한다.
대상 객체(Target)를 사용하는 코드는 대상 객체(Target)를 Proxy
를 통해서 간접적으로 접근하게 되며, Proxy
는 공통기능(Advice)을 실행한 뒤 대상객체(Target)의 실제 메서드를 호출하거나 또는 대상객체(Target)의 실제 메소드가 호출된 뒤 공통기능(Advice)을 실행한다.
Proxy
는 타겟을 감싸서 요청을 대신 받아주는 랩핑 클래스이다.
Spring에서는 Proxy를 이용해 객체지향의 5대원칙 중 하나인 OCP를 적용하고 있다.
Open-Close Principal : 개방폐쇄의 원칙
'소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고,
수정에 대해서는 닫혀 있어야 한다.'는 프로그래밍 원칙
빈 후처리기들 중에서 자동으로 프록시를 생성하기 위해 DefaultAdvisorAutoProxyCreator
라는 클래스를 사용한다.
이 클래스는 어드바이저를 이용한 자동 프록시 생성기이다. 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록시킬 수 있다.
DefaultAdvisorAutoProxyCreator 빈 후처리가 등록되어 있다면, 스프링은 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보낸다.
이 후처리기를 통해 일일이 ProxyFactoryBean을 빈으로 등록하지 않아도 여러 타깃 오브젝트에 자동으로 프록시를 적용시킬 수 있다.
아래 내용은 AOP(Aspect Oriented Programming, 관점 지향 프로그래밍), 저장로그 예제 [더블에스 devlog]님의 블로그 내용이 출처임을 밝힙니다.
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
@Component // 스프링에서 관리하는 bean
@Aspect // AOP bean
public class LogAdvice {
// private : 외부에서 로그를 가로채지 못하도록 하기 위해
// static final : 로그 내용이 바뀌지 않으므로
// 로깅툴을 사용하는 이유 : sysout명령어는 IO리소스를 많이 사용하여 시스템이 느려질 수 있다, 로그를 파일로 저장하여 분석할 필요가 있다.
private static final Logger logger = LoggerFactory.getLogger(LogAdvice.class);
// PointCut - 실행 시점
// @Before, @After, @Around
// 컨트롤러, 서비스, DAO의 모든 method를 실행 전후에 logPrint method가 자동으로 실행된다.
// .. : 하위의 모든 디렉토리를 의미
// *(..) : * - 하위의 모든 메서드, (..) - 모든 매개변수
@Around("execution(* com.example.spring02.controller..*Controller.*(..))"
+ " or execution(* com.example.spring02.service..*Impl.*(..))"
+ " or execution(* com.example.spring02.model..dao.*Impl.*(..))")
public Object logPrinnt(ProceedingJoinPoint joinPoint) throws Throwable{
// 실행 시간 체크 : 시작시간
long start = System.currentTimeMillis();
// 핵심로직으로 이동
Object result = joinPoint.proceed();
// 클래스 이름
String type = joinPoint.getSignature().getDeclaringTypeName();
String name = "";
if (type.indexOf("Controller") > -1) {
name = "Controller:";
} else if (type.indexOf("Service") > -1) {
name = "ServiceImpl:";
} else if (type.indexOf("DAO") > -1) {
name = "DAO:";
}
// 메서드 이름
logger.info(name+type+"."+joinPoint.getSignature().getName()+"()");
// 파라미터 이름
logger.info(Arrays.toString(joinPoint.getArgs()));
// 실행 시간 체크 : 종료시간
long end = System.currentTimeMillis();
// 실행 시간 체크 : 연산
long time = end-start;
logger.info("실행 시간:"+time);
return result;
}
}
스프링(Spring) 개발 - (16) AOP 설정하기 (부제: Controller에도 AOP 적용하기)[흔한 개발자의 개발 노트]
스프링 AOP 개념 및 Proxy를 이용한 구동원리 Posted by Jun Young on
좋은 글 감사합니다!