개발/[Spring] 블로그 만들기

[코드로 배우는 스프링 웹 프로젝트] 18강. AOP라는 패러다임 (Aspect-oriented programming/트랜잭션/AOP 용어 정리)

ee2ee2 2022. 5. 8. 14:34
728x90
반응형

해당 프로젝트는 코드로 배우는 스프링 웹 프로젝트(개정판)을 기반으로 진행됩니다.


AOP(Aspect-oriented programming)는 '관점 지향 프로그래밍'이라는 용어로 해석되는데 이때 '관점(Aspect)'는 '관심사(concern)'라는 말로 통용된다.

주로, 아래와 같이 핵심 로직은 아니지만, 코드를 온전하게 만들기 위해서 필요한 고민들이다.

  • 파라미터가 올바르게 들어왔는가?
  • 이 작업을 하는 사용자가 적절한 권한을 가진 사용자일까?
  • 이 작업에서 발생할 수 있는 모든 예외는 어떻게 처리할까?

AOP가 추구하는 것은 이와 같은 반복적인 부분을 '관심사의 분리(separate concerns)'하는 것이다. 즉, AOP는 개발자가 염두에 두어야하는 일들은 별도의 '관심사'로 분리하고,  핵심 비즈니스 로직만을 작성할 것을 권장한다.


18.1 AOP 용어들

AOP는 기존의 코드를 수정하지 않고, 원하는 기능들과 결합할 수 있는 패러다임이다. 개발자의 입장에서 AOP적용은 기존의 코드를 수정하지 않고도 원하는 관심사(cross-concern)들을 엮을 수 있다는 점이다.

Target : 순수한 비즈니스 로직을 의미하고, 어떠한 관심사들과도 관계를 맺지않은 순수한 코어(core)

Proxy : Target을 전체적으로 감싸고 있는 존재. Proxy는 내부적으로 Target을 호출하지만 중간에 필요한 관심사들을 거쳐서 Target을 호출하도록 자동/수동으로 작성됨. 대부분의 경우 스프링 AOP 기능을 이용해 자동으로 생성되는 방식(auto-proxy)을 이용

JoinPoint : Target 객체가 가진 메소드. 외부에서의 호출은 Proxy객체를 통해서 Target 객체의 JoinPoint를 호출하는 방식이라고 이해할 수 있음.

(Target에는 여러 메소드가 존재하기 때문에 어떤 메소드에 관심사를 결합할 것인지 결정해야하는데 이 결정을 'Pointcut'이라고 함)

Advice : Aspect(관심사 자체)를 구현한 코드 (즉, 관심사를 분리해놓은 코드)


18.2 AOP 실습

18.2.1 예제 프로젝트 생성

예제 실습을 위해 ex04라는 프로젝트를 생성하고, 기본 패키지는 'org.zerock.controller'로 설정한다. 예제 프로젝트는 스프링 5.0.x 버전을 사용할 것이고, AOP/테스트 코드 등 사용을 위해 pom.xml에 아래와 같이 수정 및 추가한다.

pom.xml

	<groupId>org.zerock</groupId>
	<artifactId>controller</artifactId>
	<name>ex04</name>
	<packaging>war</packaging>
	<version>1.0.0-BUILD-SNAPSHOT</version>
	<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>
    
...생략

		<!-- add test lib -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
		
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.0</version>
			<scope>provide</scope>
		</dependency>
		
		<!-- Test -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.7</version>
			<scope>test</scope>
		</dependency>
        
        
..생략
		<!-- AspectJ -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>	
		
		<!-- AspectJ Weaver-->
        <!-- 스프링 AOP 처리가 된 객체를 생성할 때 AspectJ Weaver라이브러리의 도움을을 받아 동작함 -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>

18.2.2 서비스 계층 설계

프로젝트에 org.zerock.service 패키지를 생성하고, 서비스 인터페이스와 클래스를 구현한다.

SampleService 인터페이스

package org.zerock.service;

public interface SampleService {
	public Integer doAdd(String str1, String str2) throws Exception;
}

SampleServiceImpl 클래스

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);
	}

}

18.2.3 Advice 작성

위 코드를 보면 항상 작성했었던 log.info() 를 이용해서 로그를 기록했던 부분이 빠져있다. 이 부분이 바로 '반복적이면서 핵심 로직도 아니고, 필요하기는 한'기능이기 때문에 '관심사'로 간주할 수 있다. AOP 개념에서 Advice는 '관심사'를 실제로 구현한 코드를 뜻한다. 로그를 기록해주는 LogAdvice를 설계해보자.

LogAdvice.java

  • @Aspect : 해당 클래스의 객체가 Aspect를 구현한 것을 나타내기 위해 사용
  • @Component : AOP와는 관계가 없지만 스프링에서 빈(Bean)으로 인식하기 위해서 사용함.
  • @Before : Target의 JoinPoint를 호출하기 전에 실행되는 코드임을 명시
  • execution : AspectJ의 표현식으로 접근제한자와 특정 클래스의 메소드를 지정할 수 있음. (맨 앞의 *는 접근제한자를 의미하고, 맨 뒤의 *는 클래스 이름과 메소드의 이름을 의미함.)
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==========");
	}
}

18.3 AOP 설정

스프링 프로젝트에서 AOP를 설정해보자. 프로젝트의 root-context.xml을 선택해서 Namespace에서 'aop'와 'context'를 추가한다.

root-context.xml에 아래의 내용을 추가한다.

root-context.xml

<component-scan>을 이용해서 'org.zerock.service'패키지와 'org.zerock.aop'패키지를 스캔한다. 이 과정에서 스프링의 빈(객체)으로 등록될 것이고, <aop:aspectj-autoproxy>를 이용해서 LogAdvice에 설정한 @Before가 동작하게 된다.

	<context:annotation-config></context:annotation-config>
	
	<context:component-scan base-package="org.zerock.service"></context:component-scan>
	<context:component-scan base-package="org.zerock.aop"></context:component-scan>
	
	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

18.4 AOP 테스트

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());
	}
}

테스트 수행 결과

가장 먼저 확인해야할 부분은 AOP 설정을 한 Target에 대해서 Proxy 객체가 정상적으로 만들어져 있는지를 확인하는 것이다. <aop:aspectj-autoproxy>가 정상정으로 모든 동작을 완료했다면, service 변수의 클래스는 단순히org.zerock.service.SampleServiceImple의 인스턴스가 아닌 생성된 Proxy 클래스의 인스턴스가 된다.

 

SampleServiceTests 클래스 일부

	@Test
	public void testAdd() throws Exception{
		log.info(service.doAdd("123", "456"));
	}

테스트 수행 결과

Advice가 테스트 메소드 실행 전 먼저 수행됨을 확인할 수 있다.


18.4.1 args를 이용한 파라미터 추적

상황에 따라 해당 메소드에 전달되는 파라미터가 무엇인지 기록하거나, 예외가 발생했을 때 어떤 파라미터에 문제가 있는지 알고 싶을 때 아래와 같이 작성할 수 있다. 

LogAdvice 클래스의 일부

'execution'으로 시작하는 Pointcut 설정에서 doAdd()메소드를 명시하고, 파라미터 타입을 지정한다. 추가로, && args 부분에는 변수명을 지정하는데, 이 2종류의 정보를 이용해서 logBeforeWithParam()메소드의 파라미터를 설정한다.

	@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);
	}

SampleServiceTests  테스트 수행 결과

단, && args를 이용하는 설정은 간단히 파라미터를 찾아서 기록할 때에는 유용하지만 파라미터가 다른 여러 종류의 메소드에 적용은 어렵다는 단점이 있다.


18.4.2 @AfterThrowing

@AfterThrowing 어노테이션은 지정된 대상이 예외를 발생할 후에 동작하면서 문제를 찾을 수 있는 기능을 갖는다.

LogAdvice 클래스의 일부

	@AfterThrowing(pointcut="execution(* org.zerock.service.SampleService*.*(..))", throwing="exception")
	public void logExeception(Exception exception) {
		log.info("Exception....!!!!");
		log.info("exception : " + exception);
	}

SampleServiceTests  클래스 일부

	@Test
	public void testAddError() throws Exception{
		log.info(service.doAdd("123", "ABC"));
	}

SampleServiceTests 테스트 수행 결과


18.5 @Around와 ProceedingJoinPoint

@Around 직접 대상 메소드를 실행할 수 있는 권한을 가지고 있고, 메소드의 실핸 전과 후에 처리가 가능하다. ProceedingJoinPoint@Around와 같이 결합해서 파라미터나 예외 들을 처리할 수 있다.

LogAdvice 클래스의 일부

: @Around가 적용된 메소드의 경우 리턴 타입이 void가 아닌 타입으로 설정하고, 메소드의 실행 결과 역시 직접 반환하는 형태로 작성해야 한다.

	@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) {
			// TODO: handle exception
			e.printStackTrace();
		}
		
		long end = System.currentTimeMillis();
		
		log.info("TIME : " + (end-start));
		
		return result;
	}

SampleServiceTests 테스트 수행 결과

@Around가 먼저 동작하고, @Before 등이 실행한 후에 메소드가 실행되는데 걸린 시간이 로그로 기록됨을 확인힐 수 이다.


다음 시간은 스프링에서 트랜잭션 관리에 대해 작성할 것이다!