해당 포스팅은 이전 예제로 배우는 스프링 입문 2 포스팅에서 설명한 AOP의 내용에 조금 더 추가하는 내용입니다.

1. AOP

관점 지향 프로그래밍으로 관심사라는 뜻으로 해석되기도 합니다. 더 자세한 의미로는 더 큰 시야를 가지고 구현할 애플리케이션을 바라보았을때 코드를 반복적으로 사용하는 부분을 찾을 수 있고 이런 반복 사용한 영역을 분리시켜 개발자가 핵심 비지니스 로직에만 집중할 수 있도록 하는 프로그래밍 기법을 말합니다.

이론적인 내용보다 코드 구현 개념으로 설명을 추가하자면 기능의 공통적인 처리부분을 관심사하고 개발자가 구현할 비지니스로직을 분리하여 실행시 이를 결합하는 방법으로 접근합니다. AOP의 예를 들자면 작성한 메소드의 실행 시간을 측정하거나 잘못된 파라미터의 접근의 제어등이 있습니다. 결론적으로 기존의 코드를 건드리지 않고 원하는 기능을 결합할 수 것은 AOP의 패러다임으로 볼 수 있습니다.


2. AOP 용어

스크린샷 2019-05-20 오후 8 37 34

Target

비지니스 로직(관심사와 결합될 대상)을 말합니다.

Proxy

어떤 일을 대신 맡는 기능을 합니다. 클라이언트가 타겟을 호출하면 타겟이 직접 호출되는 것이 아니라 타겟을 감싸고 있는 Proxy가 내부적으로 Advice(관심사)를 거쳐 타겟을 호출합니다. 대부분의 경우 스프링 AOP를 사용하여 자동생성(auto-proxy) 방식을 사용합니다.

Advice

구현될 부가기능을 담은 객체(관심사)를 말합니다. 비지니스 로직과 분리되어 부가적인 공통 처리부분 영역을 집중할 수 있으며 Aspect와 같은 관심사영역이지만 Aspect추상적 개념이며 AdviceAspect를 구현한 코드를 뜻합니다.

JoinPoint

타겟이 가진 여러가지 메소드를 뜻하며 다른 의미로는 관심사가 적용될 위치를 말하기도 합니다.

(스프링 AOP에서는 메소드만 JoinPoint 허용)

Pointcut

타겟내에는 여러 메소드 즉 JoinPoint가 존재하는데 어떤 JoinPoint와 어떤 관심사가 결합할지 결정하는 것을 말합니다.

즉 Pointcut은 관심사와 비지니스 로직이 결합되는 지점을 결정하는 것을 정의한 모듈을 뜻하며 TargetPointcut에 의해 자신에게 없는 기능을 가지게 될 수 있습니다.

Aspect

부가기능을 정의한 Advice와 정의한 Advice(관심사)를 어디에 적용할지 결정하는 Pointcut동시에 갖고 있는 모듈을 말합니다.

스크린샷 2019-05-20 오후 8 35 18

3. AOP실습

AspectJ의 버전과 AOP의 버전을 수정하여 실습 진행하였습니다. pom.xml의 설정은 아래와 같습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<properties>
		<java-version>1.8</java-version>
		<org.springframework-version>5.0.7.RELEASE</org.springframework-version>
		<org.aspectj-version>1.9.0</org.aspectj-version>
		<org.slf4j-version>1.7.25</org.slf4j-version>
 </properties>
<dependency>
		    <groupId>org.aspectj</groupId>
		    <artifactId>aspectjweaver</artifactId>
		    <version>${org.aspectj-version}</version>
		</dependency>


실습은 변수 2개를 받아 더하는 기능을 메소드를 구현하였습니다.

1
2
3
4
5
public interface SampleService {
	
	public Integer doAdd(String str1, String str2) throws Exception;
	
}


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Service
public class SampleServiceImpl implements SampleService{

	@Override
	public Integer doAdd(String str1, String str2) throws Exception {
		
		return Integer.parseInt(str1) + Integer.parseInt(str2);
	}
	
}

보통 로직을 처리시 파라미터의 값을 로그로 확인하기 위해 log.info() 등을 반복적으로 처리했습니다.

이런 로그는 필요한 비지니스로직이 아닌 부가적인 기능을 하는 관심사 측면으로 분리할 수 있습니다. 분리하기 위해 패키지를 새로 생성하여 LogAdvice 클래스를 생성합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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( "excution(* org.zerock.service.SampleService*.*(..))")
	public void logBefore() {
		
		log.info("--------------");
		
	}
}
  • Before Advice : Target의 Joinpoint를 호출하기 전에 실행되는 코드
  • After Returning Advice : 모든 실행이 이뤄지고 동작하는 코드
  • After Throwing Advice : 예외 발생 뒤 동작 코드
  • After Advice : 정상 실행되거나 예외발생 구분없이 무조건 동작하는 코드
  • Around Advice : 메소드의 실행자체를 제어하는 코드, 대상을 지정하여 메소드를 호출할 수 있습니다.

@Aspect어노테이션은 Aspect로 구현한 것을 나타내기 위해 사용하며 @Before 는 BeforeAdvice를 구현한 메소드에 추가하는 것을 말합니다. Advice와 관련된 어노테이션들은 내부적으로 Pointcut을 지정하고 excution 문자열의 사용은 AspectJ의 표현식입니다.

(* org.zerock.service.SampleService*.*(..)) 맨앞에 위치한 *는 접근제한자, 맨뒤에 *는 각각 클래스의 이름과 메소드 이름을 뜻합니다.

마지막으로 pom.xml 에 아래와 같은 내용을 추가합니다.

ComponentScan으로 aop패키지와 serivce패키지를 스캔 후 SampleServiceImpl과 LogAdvice는 빈으로 등록되어 LogAdvice에서 설정한 @Before가 동작하게 됩니다.

1
2
<context:component-scan base-package="org.zerock.aop"></context:component-scan>
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

4. AOP 테스트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@RunWith(SpringJUnit4ClassRunner.class)
@Log4j
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
public class SampleServiceTests {
	
//	@Autowired
	@Setter(onMethod_ = @Autowired)
	private SampleService service;
	
	@Test
	public void testClass() {
		log.info(service);
		log.info(service.getClass().getName());
	}
}

위 코드는 AOP설정한 Target(ServiceImpl)에 대해 Proxy 객체가 생성되었는지 확인하는 테스트입니다.

정상적으로 작동한다면 아래처럼 첫번째 로그는 SampleImpl의 인스턴스 두번째 로그는 Proxy 인스턴스를 확인 할 수 있습니다.

스크린샷 2019-05-21 오전 12 22 44


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@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 testAdd() throws Exception{
		
		log.info(service.doAdd("123", "123"));
		
	}
}

조금 더 확실하게 AOP 기능을 확인하기 위해 위와 같은 코드를 작성했을때 결과를 확인해보겠습니다.


스크린샷 2019-05-21 오전 12 26 44

AOP 실습에서 작성한 LogAdvice 클래스가 적용되어 위와 같은 결과를 나타내었습니다.


4-1. @AfterThrowing

AOP의 @AfterThrowing이 지정된 대상이 예외가 발생한 후 동작하면서 문제를 찾아주는 역할을 합니다.

1
2
3
4
5
6
@AfterThrowing(pointcut = "execution(* org.zerock.service.SampleService*.*(..))", throwing = "exception")
	public void logException(Exception exception) {
		
		log.info("Exception !");
		log.info("Exception : " + exception);
	}


1
2
3
4
5
6
@Test
	public void testAddError() throws Exception{
		
		log.info(service.doAdd("안녕", "123"));
		
	}

LogAdvice 클래스와 함께 테스트코드를 작성해봤습니다. 아래와 같은 결과로 어떠한 input 즉 파라미터가 Exception을 발생시켰는지 확인할 수 있었습니다.

스크린샷 2019-05-21 오전 12 48 44

4-2. @Around

구체적인 처리가 필요하다면 @AroundProceedingJoinPoint를 사용합니다. 직접 대상 메소드를 실행할 수 있으며 실행 전/후 처리도 가능하다는 장점을 가지고 있고 ProceedingJoinPoint@Around와 결합해 예외처리 또한 가능합니다.

아래의 코드는 pointcutorg.zerock.service.SampleService*.*(..)이며 ProceedingJoinPointTarget, Param을 확인하는 테스트 코드입니다. 또한 @Around가 적용되는 메소드는 리턴타입이 void가 아닌 반환하고자 하는 리턴타입으로 지정해줘야 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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()));
		
		Object result = null;
		
		try {
			result = pjp.proceed();
		}catch(Throwable e) {
			e.printStackTrace();
		}
		
		long end = System.currentTimeMillis();
		
		log.info("TIME : " + (end - start));
		
		return result;
	}

테스트코드를 실행하면 다음과 같이 현재 Spring AOP가 적용된 TargetParam 그리고 반환 시간 및 doAdd 메소드가 실행된

결과를 확인할 수 있습니다.

스크린샷 2019-05-21 오전 12 58 37

이를 통해 기존의 Controller를 포함해 ServiceImpl에서 사용했던 각 메소드의 로그 구현 부분을 AOP로 변경하여 기존의 코드를 건드리지 않고 독립적으로 비지니스로직부가기능(관심사)를 분리하여 활용할 수 있다는 점을 알 수 있었고, 예제의 코드는 비록 규모가 작아 체감하지 못했지만 대규모 서비스에선 AOP를 통해 코드의 재사용에 대한 유지보수 효율성과 성능에 매우 중요할 것 같다는 생각을 하였습니다.


Reference