AOP는 흔히 '관점 지향 프로그래밍'이라는 용어로 번역되는데, 이때 '관점'이라는 용어가 현실적으로 와닿지 않기 때문에 어렵게 느껴질 수 있다. '관점'이라는 용어는 개발자들에게는 '관심사'라는 말로 통용된다. '관심사'는 개발 시 필요한 고민이나 염두에 두어야 하는일일고 생각할 수 있는데, 코드를 작성하면서 염두에 두는 일들은 주로 다음과 같다.
- 파라미터가 올바르게 들어왔을까?
- 이 작업을 하는 사용자가 적절한 권한을 가진 사용자인가?
- 이 작업에서 발생할 수 있는 모든 예외는 어떻게 처리해야 하는가?
위와 같은 고민들은 '핵심 로직'은 아니지만, 코드를 온전하게 만들기 위해서 필요한 고민들인데 전통적인 방식에서는 개발자가 반복적으로 이러한 고민을 코드에 반영하게 된다. AOP는 이러한 고민에 대한 문제를 조금 다른 방식으로 접근한다. AOP가 추구하는 것은 '관심사의 분리'다. AOP는 개발자가 염두에 두어야 하는 일들은 별도의 '관심사'로 분리하고, 핵심 비즈니스 로직만을 작성할 것을 권장한다.
예를 들어 AOP를 이용하면 작성된 모든 메서드의 실행 시간이 얼마인지를 기록하는 기능을 기존 코드의 수정 없이도 작성할 수 있고, 잘못된 파라미터가 들어와서 예외가 발생하는 상황을 기존 코드의 수정 없이도 제어할 수 있다. 스프링이 AOP를 지원한다는 것이 스프링의 가장 중요한 특징 중에 하나로 말하게 된 이유 역시 별도의 복잡한 설정이나 제약 없이 스프링 내에서 간편하게 AOP의 기능들을 구현할 수 있기 때문이다.
1. AOP 용어들
AOP를 구현하기 위해서는 다음과 같은 핵심적인 그림을 이해할 필요가 있다.
개발자의 입장에서 AOP를 적용한다는 것은 기존의 코드를 수정하지 않고도 원하는 관심사들을 엮을 수 있다는 점이다. 위의 그림에서 Target에 해당하는 것이 바로 개발자가 작성한 핵심 비즈니스 로직을 가지는 객체다.
외부에서의 호출은 Proxy 객체를 통해서 Target 객체의 JoinPoint를 호출하는 방식이라고 이해할 수 있다.
그림을 통해 Advice와 JoinPoint의 관계를 좀더 상세하게 표현하면 다음과 같다.
JointPoint는 Target이 가진 여러 메서드라고 보면 된다. (엄밀하게 말하면 스프링 AOP에서는 메서드만이 JoinPoint가 된다.) Target에는 여러 메서드가 존재하기 때문에 어떤 메서드에 관심사를 결합할 것인지를 결정해야 하는데 이 결정을 'Pointcut'이라고 한다.
Pointcut은 관심사와 비즈니스 로직이 결합되는 지점을 결정하는 것이다. 앞의 Proxy는 이 결합이 완성된 상태이므로 메서드를 호출하게 되면 자동으로 관심사가 결합된 상태로 동작하게 된다. 관심사는 위의 그림에서 Aspect와 Adivice라는 용어로 표현되어 있다. Aspect는 조금 추상적인 개념을 의미한다. Aspect는 관심사 자체를 의미하는 추상명사라고 볼 수 있고, Advice는 Aspect를 구현한 코드이다.
2. AOP 실습
AOP 기능은 주로 일반적인 Java API를 이용하는 클래스들에 적용한다. Controller에 적용이 불가능한 것은 아니지만, Controller의 경우 뒤에서 학습하게 될 인터셉터나 필터 등을 이용한다. 예제에서는 서비스 계층에 AOP를 적용한다. AOP의 예제는 1) 서비스 계층의 메서드 호출 시 모든 파라미터들을 로그로 기록하고, 2) 메서드들의 실행 시간을 기록하도록 한다.
2-1. 예제 프로젝트 생성
1) 예제 프로젝트를 생성하고 스프링 5.0.x 버전을 이용할 것이므로, pom.xml에 스프링 버전과 AOP 버전을 수정한다.
스프링의 AOP는 AspectJ라는 라이브러리의 도움을 많이 받기 때문에 스프링 버전을 고려해서 AspectJ의 버전 역시 1.9.0으로 버전을 높여준다!
2) 최종적으로 테스트 코드를 통해서 동작하게 될 것이므로 spring-test, lombok을 추가하고 Junit의 버전을 변경한다.
3) AOP 설정과 관련해서 가장 중요한 라이브러리는 AspectJ Weaver라는 라이브러리이다. 스프링은 AOP 처리가 된 객체를 생성할 때 AspectJ Weaver 라이브러리의 도움을 받아서 동작하므로, pom.xml에 추가해야 한다.
2-2. 서비츠 계층 설계
1) 프로젝트에 org.zerock.service 패키지를 생성하고, 간단한 기능을 가진 서비스 인터페이스와 클래스를 구현한다.
2) SampleService 인터페이스와 SampleServiceImpl 클래스에 아래와 같은 코드를 추가한다.
- SampleService.java
package org.zerock.service;
public interface SampleService {
public Integer doAdd(String str1, String str2)throws Exception;
}
예제로 사용할 객체는 SampleService 인터페이스의 doAdd() 메서드를 대상으로 진행한다. 인터페이스를 구현한 SampleServiceImpl 클래스는 단순히 문자열을 변환해서 더하기 연산을 하는 단순 작업으로 작성한다. SampleServiceImpl을 작성할 때에는 반드시 @Service라는 어노테이션을 추가해서 스프링에서 빈으로 사용될 수 있도록 설정한다.
- SampleServiceImpl.java
package org.zerock.service;
import org.springframework.stereotype.Service;
@Service
public class SampleServiceImpl implements SampleService{
@Override
public Integer doAdd(String str1, String str2) throws Exception{
return Integer.parseInt(str1) + Integer.parseInt(str2);
}
}
2-3. Advice 작성
위의 SampleServiceImpl 코드를 보면 기존에는 코드를 작성할 때 항상 log.info()등을 이용해서 로그를 기록해 오던 부분이 빠진 것을 알 수 있다. 지금까지 해왔던 수많은 로그를 기록하는 일은 '반복적이면서 핵심 로직도 아니고, 필요하기는 한' 기능이기 때문에 '관심사'로 간주할 수 있다. AOP의 개념에서 Advice는 '관심사'를 실제로 구현한 코드이므로 지금부터 로그를 기록해주는 LogAdvice를 설계한다!
AOP 기능의 설정은 XML 방식이 있기는 하지만, 이 책의 예제는 어노테이션만을 이용해서 AOP 관련 설정을 진행한다.
1) 예제 프로젝트에 ogr.zerock.aop 패키지를 생성하고, LogAdvice라는 클래스를 추가한다. 그리고 아래와 같은 코드를 작성한다.
- LogAdvice.java
package org.zerock.aop;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import lombok.extern.log4j.Log4j;
@Aspect
@Log4j
@Component
public class LogAdvice {
@Before("execution(* org.zerock.service.SampleService*.*(..))")
public void logBefore() {
log.info("=========================");
}
}
LogAdvice 클래스의 선언부에는 @Aspect 어노테이션이 추가되어 있다. @Aspect는 해당 클래스의 객체서 Apect를 구현한 것임으로 나타내기 위해서 사용한다. @Component는 AOP와는 관계가 없지만 스프링에서 빈으로 인식하기 위해 사용한다. logBefore()는 @Before 어노테이션을 적용하고 있다. @Before는 BeforeAdvice를 구현한 메서드에 추가한다. @After, @AfterReturning, @AfterThrowing, @Around 역시 동일한 방식으로 적용한다.
Advice 와 관련된 어노테이션들은 내부적으로 Pointcut을 지정한다. Pointcut은 별도의 @Pointcut으로 지정해서 사용할 수도 있다. @Before 내부의 'execution......' 문자열은 AspectJ의 표현식이다. 'excution'의 경우 접근제한자와 특정 클래스의 메서드를 지정할 수 있다. 맨 앞의 '*'는 접근제한자를 의미하고, 맨 뒤의 '*'는 클래스의 이름과 메서드의 이름을 의미한다.
3. AOP 설정
스프링 프로젝트에 AOP를 설정하는 것은 스프링 2버전 이후에는 간단히 자동으로 Proxy 객체를 만들어주는 설정을 추가해 주면 된다.
1) 프로젝트의 root-context.xml을 선택해서 네임스페이스에 'aop'와 'context' 를 추가한다.
2) root-context.xml에 아래와 같은 내용을 추가한다.
root-context.xml 에서는 <component-scan>을 이용해서 'org.zerock.service' 패키지와 'org.zerock.aop' 패키지를 스캔한다. 이 과정에서 SampleServiceImpl 클래스와 LogAdvice는 스프링의 빈으로 등록될 것이고, <aop:aspectj-autoproxy>를 이용해서 LogAdvice에 설정한 @Before가 동작하게 된다.
이클립스가 무료인 관계로 간혹 버그가 존재하기는 하지만, 정상적인 상황이라면 SampleServiceImpl 클래스를 확인해 보면 AOP 가 적용된 후에는 아래와 같은 아이콘이 추가된 것을 확인 할 수 있다. (아이콘이 추가된다고 해서 완벽한 동작이 보장되지는 않지만, 적어도 도움이 되는 것은 사실...ㅎ)
4. AOP 테스트
정상적인 상황이라면 SampleServiceImpl, LogAdvice 는 같이 묶여서 자동으로 Proxy 객체가 생성된다.
1) 테스트 관련 폴더에 org.zerock.service.SampleServiceTests 클래스를 추가하고 아래와 같은 코드를 입력한다.
package org.zerock.service;
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;
import lombok.Setter;
import lombok.extern.log4j.Log4j;
@RunWith(SpringJUnit4ClassRunner.class)
@Log4j
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
public class SampleServiceTests {
@Setter(onMethod_ = @Autowired)
private SampleService service;
@Test
public void testClass() {
log.info(service);
log.info(service.getClass().getName());
}
}
SampleServiceTests에서 가장 먼저 작성해 봐야 하는 코드는 AOP 설정을 한 Target에 대해서 Proxy 객체가 정상적으로 만들어져 있는지를 확인하는 것이다. <aop:aspectj-autoproxy></aop:aspectj-autoproxy>가 정상적으로 모든 동작을 하고, LogAdvice 설정문제가 없다면 service 변수의 클래스는 단순히 org.zerock.service.SampleServiceImpl의 인스턴스가 아닌 생성된 Proxy 클래스의 인스턴스가 된다. testClass()를 실행해 보면 아래와 같은 결과를 보게 된다.
단순히 service 변수를 출력했을 때는 기존에 사용하듯이 SampleServiceImpl 클래스의 인스턴스처럼 보인다. 이것은 toString()의 결과이므로 좀 더 세밀하게 파악하려면 다시 getClass()를 이용해서 파악해야만 한다. com.sun.proxy.$Proxy는 JDK의 다이나믹 프록시 기법이 적용된 결과다.
2) 이를 이용해서 SampleServiceImpl에 있는 코드를 실행하는 테스트 코드를 작성한다.
- SampleServiceTests.java
@Test
public void testAdd() throws Exception{
log.info(service.doAdd("123", "456"));
}
위 코드를 실행하면 LogAdvice의 설정이 같이 적용되어 아래와 같이 로그가 기록되어야 한다.
4-1. args를 이용한 파라미터 추적
LogAdvice가 SampleService의 doAdd()를 실행하기 직전에 간단한 로그를 기록하지만, 상황에 따라서는 해당 메서드에 전달되는 파라미터가 무엇인지 기록하거나, 예외가 발생했을 때 어떤 파라미터에 문제가 있는지 알고 싶은 경우도 많다.
LogAdvice에 적용된 @Before("execution(*org.zerock.service.SampleService*.*(..)")은 어떤 위치에 Advice를 적용할 것인지를 결정하는 Pointcut인데, 설정 시에 args를 이용하면 간단히 파라미터를 구할 수 있다.
1) LogAdvice 클래스에 다음과 같은 코드를 추가한다.
@Before("execution(* org.zerock.service.SampleService*.doAdd(String, String)) && args(str1, str2)")
public void logBeforeWithParam(String str1, String str2) {
log.info("str1:" + str1);
log.info("str2:" + str2);
}
logBeforeWithParam() 에서는 'execution'으로 시작하는 Pointcut 설정에 doAdd()메서드를 명시하고, 파라미터 타입을 지정했다. 뒤쪽의 '&& args (...' 부분에는 변수명을 지정하는데, 이 2종류의 정보를 이용해서 logBeforeWithParam() 메서드의 파라미터를 설정하게 된다.
기존의 테스트 코드를 실행하면 이제 단순한 로그와 더불어 전달된 파라미터 역시 파악할 수 있다.
'&& args'를 이용하는 설정은 간단히 파라미터를 찾아서 기록할 때에는 유용하지만 파라미터가 다른 여러 종류의 메서드에 적용하는 데에는 간단하지 않다는 단점이 있다. 이에 대한 문제는 조금 뒤쪽에서 @Around와 ProceedingJoinPoint를 이용해서 해결할 수 있다.
4-2. @AfterThrowing
코드를 실행하다 보면 파라미터 값이 잘못되어서 예외가 발생하는 경우가 많다. AOP의 @AfterThrowing 어노테이션은 지정된 대상이 예외를 발생한 후에 동작하면서 문제를 찾을 수 있도록 도와줄 수 있다.
1) LogAdvice 클래스에 다음과 같은 코드를 추가한다.
@AfterThrowing(pointcut = "execution(* org.zerock.service.SampleService*.*(..)", throwing="exception")
public void logException(Exception exception) {
log.info("Exception....!!!");
log.info("exception : " + exception);
}
logException()에 적용된 @AfterThrowing은 'pointcut'과 'throwing' 속성을 지정하고 변수 이름을 'exception'으로 지정한다. 테스트 코드에서는 고의적으로 예외가 발생할만한 코드를 작성해서 테스트 한다.
2) SampleServiceTests 클래스에 다음과 같은 코드를 추가한다.
@Test
public void testAddError() throws Exception{
log.info(service.doAdd("123", "ABC"));
}
doAdd()는 숫자로 변환이 가능한 문자열을 파라미터로 지정해야 하는데 고의적으로 'ABC'와 같은 문자를 전달하면 다음과 같은 로그를 보게 된다.
5. @Around와 ProceedingJoinPoint
AOP를 이용해서 좀 더 구체적인 처리를 하고 싶다면 @Around와 ProceedingJoinPoint를 이용해야 한다. @Around는 조금 특별하게 동작하는데 직접 대상 메서드를 실행할 수 있는 권한을 가지고 있고, 메서드의 실행 전과 실행 후에 처리가 가능하다.
ProceedingJoinPoint는 @Around와 같이 결합해서 파라미터나 예외 등을 처리할 수 있다.
1) LogAdvice 클래스에 다음과 같은 코드를 추가한다.
@Around("execution(* org.zerock.service.SampleService*.*(..))")
public Object logTime(ProceedingJoinPoint pjp) {
long start = System.currentTimeMillis();
log.info("Target : " + pjp.getTarget());
log.info("Param : " + Arrays.toString(pjp.getArgs()));
// invoke method
Object result = null;
try {
result = pjp.proceed();
}catch(Throwable e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
log.info("TIME : " + (end - start));
return result;
}
logTime()의 Pointcut 설정은 '...SampleService*.*(..)' 로 지정한다. logTime()은 특별하게 ProceedingJoinPoint라는 파라미터를 지정하는데, ProceedingJoinPoint는 AOP의 대상이 되는 Target이나 파라미터 등을 파악할 뿐만 아니라, 직접 실행을 결정할 수도 있다. @Before 등과 달리 @Around가 적용되는 메서드의 경우에는 리턴 타입이 void가 아닌 타입으로 설정하고, 메서드의 실행 결과 역시 직접 반환하는 형태로 작성해야만 한다.
실행 결과를 보면 @Around가 먼저 동작하고, @Before 등이 실행된 후에 메서드가 실행되는데 걸린 시간이 로그로 기록되는 것을 볼 수 있다.
'Spring' 카테고리의 다른 글
ERROR: org.springframework.test.context.TestContextManager - Caught exception while allowing (3) | 2020.01.05 |
---|---|
[28] AOP와 트랜잭션 - 스프링에서 트랜잭션 관리 (0) | 2020.01.05 |
[26] REST 방식과 Ajax를 이용하는 댓글처리 - 이벤트 처리와 HTML 처리 (0) | 2020.01.04 |
[25] REST 방식과 Ajax를 이용하는 댓글 - Ajax 댓글 처리 3 (0) | 2019.12.31 |
[24] REST 방식과 Ajax를 이용하는 댓글 처리 - Ajax 댓글 처리 2 (0) | 2019.12.31 |