AOP는 과거 방식인 '관심사 + 비즈니스 로직'을 분리하여 별도의 코드로 작성하게 하고 실행 시 이를 결합하는 방식
AOP란, 관점 지향 프로그래밍을 말한다
개발 시 관점은 관심사라는 말로 통용된다. 관심사란 개발 시 필요한 고민이나 염두에 두어야 할 일이라고 생각할 수 있다. 코드를 작성하면서 염두해야 할 일들은 주로 아래와 같다
이러한 고민들은 핵심 로직은 아니지만, 코드를 온전하게 만들기 위해 필요한 고민들이고 반복적으로 발생한다.
AOP가 추구하는 것은 이러한 관심사의 분리. 즉, 개발자가 염두에 두어야 할 일들은 별도의 관심사로 분리하고, 핵심 로직만을 작성하는 것이다.
관심사는 일종의 주변 로직이라고 볼 수 있다.
예를 들어서 나눗셈을 구현할 때,
이와 같이 가장 중요한 로직은 아니지만 사전 조건이나 사후 조건들을 관심사라고 볼 수 있다.
AOP는 '관심사 + 비즈니스 로직'을 분리하여 별도의 코드로 작성한다.
이는 과거에 비즈니스 로직을 작성하면서 그 내부에 필요한 관심사를 처리하는 방식과 정반대의 접근 방식
개발자가 작성한 코드와 분리된 관심사를 구현한 코드를 컴파일 또는 실행 시점에 결합시킨다. 실제 실행은 결합된 상태의 코드가 실행되기 때문에 개발자는 핵심 비즈니스 로직에만 근거하여 코드를 작성하고 나머지는 어떤 관심사들과 결합할 것인지를 설정하는 것 만으로 개발을 마칠 수 있게 된다.
예를 들어 AOP를 이용하면 작성된 모든 메서드의 실행 시간이 얼만인지를 기록하는 기능을 기존 코드의 수정 없이도 작성 가능하고, 잘못된 파라미터가 들어와서 예외가 발생하는 상황 또한 기존 코드 수정 없이 제어 가능하다.
개발자 입장에서 AOP를 적용한다는 것 = 기존 코드의 수정 없이 원하는 관심사(cross-concern)들을 엮을 수 있다는 것. Target이 개발자가 작성한 핵심 비즈니스 로직을 가지는 객체이다.
Target
Target은 순수 비즈니스 로직을 의미하고, 어떠한 관심사들과도 관계를 맺지 않는 순수한 코어
Proxy
Target을 전체적으로 감싸는 부분을 Proxy라고 한다.
Proxy는 내부적으로 Target을 호출하지만, 중간에 필요한 관심사들을 거쳐서 Target을 호출하도록 자동 또는 수동으로 작성된다.
대부분의 경우 Proxy는 스프링 AOP 기능을 이용하여 자동 생성되는 방식을 사용한다. (auto-proxy)
JoinPoint
JoinPoint는 Target 객체가 가진 메서드. 외부에서의 호출은 Proxy 객체를 통해서 Target 객체의 JoinPoint를 호출하는 방식이다.
JoinPoint는 Target이 가진 여러 메서드. Target에는 여러 메서드가 존재하기 때문에 어떤 메서드에 어떤 관심사를 결합할 것인지 결정해야 하는데, 이 결정을 PointCut이라고 한다.
Aspect : 추상적인 개념. 관심사 자체를 의미하는 추상명사
Advice : Aspect를 구현한 코드. 실제 관심사를 분리해 놓은 코드를 의미한다.
Advice의 동작 위치에 따른 구분
구분 | 설명 |
---|---|
Before Advice | Target의 JoinPoint를 호출하기 전 실행되는 코드. 코드의 실행 자체에는 관여하지 않는다. |
After Returing Advice | 모든 실행이 정상적으로 이루어진 후에 동작하는 코드 |
After Throwing Advice | 예외가 발생한 뒤에 동작하는 코드 |
After Advice | 정상적으로 실행되었을 때나 예외가 발생했을 때를 구분하지 않고 실행되는 코드 |
Around Advice | 메서드의 실행 자체를 제어할 수 있는 코드. 직접 대상 메서드를 호출하고 결과나 예외를 처리한다. 가장 강한 코드 |
Target에 어떤 Advice를 적용할 것인지는 XML 설정 또는 어노테이션을 이용한 설정이 가능하다
PointCut
관심사와 비즈니스 로직이 결합되는 지점을 결정하는 것. Advice를 어떤 JoinPoint에 결합할 것인지를 결정한다.
Proxy는 이 결합이 완성된 상태이므로 메서드를 호출하면 자동으로 관심사가 결합된 상태로 동작하게 된다.
AOP에서 Target은 결과적으로 PointCut에 의해서 자신에게는 없는 기능들을 가지게 된다.
주로 사용되는 설정
구분 | 설명 |
---|---|
execution (@execution ) | 메서드를 기준으로 PointCut 설정 |
within (@within ) | 특정한 타입 (클래스)을 기준으로 PointCut 설정 |
this | 주어진 인터페이스를 구현한 객체를 대상으로 PointCut 설정 |
args (@args ) | 특정 파라미터를 가지는 대상들만을 PointCut 설정 |
@annotation | 특정한 어노테이션이 적용된 대상들만을 PointCut 설정 |
실습 내용
1. 서비스 계층의 메서드 호출 시 모든 파라미터들을 로그로 기록
2. 메서드들의 실행 시간을 기록
AOP 기능은 주로 일반적인 Java API를 이용하는 클래스 (POJO)들에 적용한다.
npx degit Zueon/spring-sts-template-xml aop-test
xml 설정을 이용하는 스프링 프로젝트를 aop-test라는 이름으로 생성한다.
생성 후 인텔리제이로 프로젝트 오픈힌 후에,
프로젝트 구조 ( command + ; ) → Facets에서 왼쪽 위 + 클릭 → Web 선택 → 프로젝트 선택 후 OK
생성된 Web의 Deploymeny Descripter Location을 src/main/webapp으로 수정한다.
Web Resource Directories도 수정
수정 후 아래쪽에 Create Artifact 클릭
Available Elements들 WEB-INF/lib 에 넣어주기
기존에 있던 webapp 말고 새로 생성된 webapp은 지워준다잉
이제 끝 ^^....
AOP 설정과 관련하여 가장 중요한 라이브러리는 AspecJ Weaver라는 라이브러리이다. 스프링은 AOP 처리가 된 객체를 생성할 때 AspecJ Weaver 라이브러리의 도움을 받아서 동작하기 때문에 이를 pom.xml에 추가해준다.
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.9.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
프로젝트에 com.ze.service 패키지를 생성하고, 서비스 인터페이스와 클래스를 구현한다
package com.ze.service;
public interface SampleService {
public Integer doAdd(String str1, String str2) throws Exception;
}
예제로 사용할 객체는 SampleService 인터페이스의 doAdd()
메서드를 대상으로 진행한다.
package com.ze.service;
import org.springframework.stereotype.Service;
@Service // Service 역할임을 나타내는 어노테이션
public class SampleServiceImpl implements SampleService{
@Override
public Integer doAdd(String str1, String str2) throws Exception {
return Integer.parseInt(str1) + Integer.parseInt(str2);
}
}
서비스를 구현한 클래스는 단순히 문자열을 숫자로 바꿔서 더한 후 반환하는 메서드이다.
@Service
어노테이션을 추가하여 스프링에서 빈으로 등록될 수 있도록 한다.
로그를 기록하는 것 (log.info()
)은 반복적이면서 핵심 로직은 아니고 필요하기는 한 기능이다 → 관심사로 간주할 수 있음
AOP 개념에서 Advice는 관심사(Aspect)를 실제로 구현한 코드이므로 LogAdvice를 작성한다.
com.ze.aop 패키지를 생성한 후 LogAdvice 클래스를 추가한다.
package com.ze.aop;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Log4j2
@Component
public class LogAdvice {
@Before("execution(* com.ze.service.SampleService*.*(..))")
public void logBefore(){
log.info("-------------------------");
}
}
@Aspect
: 해당 클래스의 객체가 Aspect(관심사)를 구현한 것임을 나타낸다.
@Component
: 스프링에서 빈으로 인식하기 위한 어노테이션
@Before
: BeforeAdvice를 구현한 메서드임을 나타낸다.
@After
, @AfterReturing
, @AfterThrowing
, @Around
도 동일하다.
Advice와 관련된 어노테이션들은 내부적으로 PointCut을 지정한다. PointCut은 별도의 @PointCut
으로 지정하여 사용 또한 가능하다.
execution ... 문자열 : AspectJ의 표현식
프로젝트의 root-context.xml에 아래 내용을 추가한다.
<context:component-scan base-package="com.ze.service"/>
<context:component-scan base-package="com.ze.aop"/>
<aop:aspectj-autoproxy/>
context:component-scan
: 지정된 패키지를 스캔하여 스프링 빈으로 등록한다.aop:aspectj-autoproxy
: LogAdvice에 설정한 @Before
가 동작하게 된다@Before
정상 동작할 경우 SampleServiceImpl, LogAdvice는 같이 묶여서 자동으로 Proxy 객체가 생성된다.
테스트 폴더에 com.ze.service.SampleServiceTests 클래스 추가
package com.ze.service;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
@Log4j2
public class SampleServiceTests {
@Setter(onMethod_ = {@Autowired})
private SampleService service;
@Test
public void testClass(){
log.info(service);
log.info(service.getClass().getName());
}
}
위 코드를 통해서 AOP 설정을 한 Target에 대해서 Proxy 객체가 정상적으로 만들어졌는지 확인한다.
성공하면 위 처럼 객체가 출력된다.
<aop:aspectj-autoproxy/>
설정으로 인해서 service 변수의 클래스는 단순히 SampleServiceImpl의 인스턴스가 아닌 생성된 Proxy 클래스의 인스턴스가 된다.
INFO 로그의 첫번째 라인을 보면 단순히 service 변수를 출력한 결과이다. 이를 보면 SampleServiceImpl의 인스턴스처럼 보이는데, 이는 toString()
의 결과이다
두번째 라인은 JDK의 다이나믹 프록시 기법이 적용된 결과
@Test
public void addTest() throws Exception {
log.info(service.doAdd("1", "2"));
}
이전 포스트의 LogAdvice는 SampleService의 doAdd()
를 실행하기 전 간단한 로그를 기록하기만 한다.
해당 메서드에 전달되는 파라미터가 정확히 무엇인지 기록하거나 예외가 발생했을 경우 어떤 파라미터에 문제가 있는지 알기 위해서는 설정 시 args를 이용하도록 한다.
LogAdvice에 적용된 @Before("execution(* com.ze.service.SampleService*.*(..))")
는 어떤 위치에 Advice를 적용할 것인지 결정하는 PointCut이고 해당 부분에 args를 추가해준다.
@Before("execution(* com.ze.service.SampleService*.doAdd(String, String)) && args(str1, str2)")
public void logBeforeWithParam(String str1, String str2){
log.info("str1 : " + str1);
log.info("str2 : " + str2);
}
}
execution(* com.ze.service.SampleService*.doAdd(String, String))
SampleService 인터페이스의 doAdd()
를 명시하고 파라미터의 타입을 지정한다.
&& args(str1, str2)
변수명 지정
이후 기존의 테스트 코드를 다시 수행한다.
@AfterThrowing
파라미터 값이 잘못되어 예외가 발생하는 경우를 찾을 수 있도록 도와준다. @AfterThrowing
어노테이션은 지정된 대상이 예외를 발생한 후 동작한다
@AfterThrowing(pointcut = "execution(* com.ze.service.SampleService*.*(..))", throwing = "exception")
public void logException(Exception exception){
log.info("<< Exception >>");
log.info("exception : " + exception);
}
@Test
public void errorTest() throws Exception {
log.info(service.doAdd("123", "ABC")); // 고의적으로 오류가 발생하도록 작성한다.
}
짱이다~
@Around
와 ProceedingJoinPoint@Around
직접 대상 메서드를 실행할 수 있는 권한이 있으며 메서드의 실행 전후에 처리가 가능하다.
ProceedingJoinPoint
@Around
와 결합하여 파라미터나 예외 등을 처리한다.
@Around("execution(* com.ze.service.SampleService*.*(..))")
public Object logTime(ProceedingJoinPoint pjp){
long startTime = System.currentTimeMillis();
log.info("Target : " + pjp.getTarget());
log.info("Param : " + Arrays.toString(pjp.getArgs()));
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
log.info("TIME : " + (endTime - startTime));
return result;
}
pjp.getTarget())
: AOP의 대상이 되는 Target 찾기
pjp.getArgs()
: 파라미터 파악
pjp.proceed()
: 직접 실행을 결정한다.
@Around
가 적용되는 메서드는 반드시 리턴타입이 void가 아닌 형태로 설정되어야 한다.
기존의 테스트 코드를 실행하면 아래와 같은 로그가 출력된다.
@Around
동작 → @Before
동작 → @Around
동작