개발 공부/Spring

코드로 배우는 스프링 웹 프로젝트-Part5.AOP와 트랜잭션-chap18.AOP라는 패러다임

maxlafe 2020. 7. 14. 21:41

코드로 배우는 스프링 웹 프로젝트-18.AOP라는 패러다임

코드로 배우는 스프링 웹 프로젝트 - 개정판

2019년 7월 10일 인쇄판

 

Part5. AOP와 트랜잭션

 

Chapter18. AOP라는 패러다임

 

AOP는 흔히 관점 지향 프로그래밍 용어로 번역되는데 이 때 관점이라는 용어가 어렵게 느껴질 수 있다.

관점은 개발자들에게는 관심사 concern으로 통용된다. 개발 시 필요한 고민이나 염두에 두어야 하는 일이라고 생각할 수 있는데, "파라미터가 올바르게 들어왔는지" "이 작업을 하는 사용자가 적절한 권한을 가진 사용자인지" "이 작업에서 발생할 수 있는 예외는 어떻게 처리해야 하는지" 등이 있다.

 

핵심 로직은 아니지만 코드를 온전하게 만들기 위한 고민들로, 전통적인 방식에선 개발자가 반복적으로 이 고민을 코드에 반영한다. AOP는 이러한 고민을 다른 방식으로 접근한다. 바로 "관심사의 분리 sepearte concerns"이다. AOP는 개발자가 염두에 두어야 할 것들을 관심사로 분리하고, 핵심 비즈니스 로직만을 작성할 것을 권장한다.

 

개발자가 작성한 코드와 분리된 관심사를 구현한 코드를 컴파일, 혹은 실행 시점에 결합시킨다.

 

 

18.1 AOP 용어들

 

AOP는 기존의 코드를 수정하지 않고 원하는 기능들과 결합할 수 있는 패러다임.

 

 

 

개발자의 입장에서 AOP를 적용한다는 것은 기존의 코드를 수정하지 않고도 원하는 관심사들을 엮을 수 있다는 점이다. 위 그림에서 Target에 해당하는 것이 개발자가 작성한 핵심 비즈니스 로직을 가지는 객체다.

Target은 순수한 비즈니스 로직을 의미하며 순수한 코어라고 볼 수 있다.

Target을 전체적으로 감싸는 것이 Proxy이다. 내부적으로 Target을 호출하지만 중간에 필요한 관심사들을 거쳐 Target을 호출하도록 자동, 수동으로 작성된다. Proxy의 존재는 직접 코드로 구현할 수도 있지만 대부분 스프링 AOP로 자동 생성되는(auto-proxy) 방식을 이용한다.

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

 

JoinPoint는 Target이 가진 여러 메서드라고 보면 된다. Target에는 여러 메서드가 존재하므로 어떤 메서드에 관심사를 결합할 것인가를 결정하는 것을 Pointcut이라고 한다.

Pointcut은 관심사와 비즈니스 로직이 결합하는 지점을 결정함. 앞의 Proxy는 이 결합이 완성된 상태이므로 메서드를 호출하게 되면 자동으로 관심사가 결합된 상태로 동작한다. 관심사는 Aspect와 Advice라는 용어로 설명할 수 있다. Aspect는 조금 추상적인 개념이다. Aspect는 관심사 자체를 의미하는 추상명사라 할 수 있고 Advice는 Aspect를 구현한 코드다.

 

Advice는 실제 걱정거리를 분리해놓은 코드로 그 동작 위치에 따라 다음으로 분류한다.

Before Advice : Target의 JoinPoint를 호출하기 전에 실행되는 코드. 코드의 실행 자체엔 관여 X

After Returning Advice : 모든 실행이 정상적으로 이루어지 후의 코드

After Throwing Advice : 예외가 발생한 뒤 동작하는 코드

After Advice : 정상적으로 실행되거나 예외가 발생했을 떄 모두 동작하는 코드

Around Advice : 메서드의 실행 자체를 제어할 수 있는 가장 강력한 코드. 직접 대상 메서드를 호출하고 결과나 예외를 처리할 수 있다.

 

Advice는 과거에선 별도의 인터페이스를 구현하고 이를 클래스로 구현하는 방식으로 제작했으나 스프링 버전3 이후엔 어노테이션만으로 설정 가능. Target에 어떤 Advice를 적용시킬 것인지 XML을 이용한 설정을 이용할 수 있고 어노테이션 이용도 가능.

 

Pointcut는 Advice를 어떤 JoinPoint에 결합할 것인지 결정한다.

AOP에서 Target은 결과적으로 Pointcut에서 자신에게 없는 기능들을 가지게 된다.

다양한 형태로 선언할 수 있는데 주로 사용되는 설정들은 다음과 같다.

 

execution(@execution) : 메서드를 기준으로 Pointcut 설정

within(@within) : 특정 타입 기준 Pointcut 설정

this : 주어진 인터페이스를 구현한 객체를 대상으로 설정

args(@args) : 특정 파라미터를 가지는 대상들만을 설정

@annotation : 특정 어노테이션을 가지는 대상들만 설정.

 

 

18.2 AOP 실습

 

주로 JAVA API를 이용하는 클래스들에 적용.

Controller에 적용도 가능하지만, Controller는 뒤에서 또 다룬다.

AOP의 예제는 1) 서비스 계층 메서드 호출 시 모든 파라미터들을 로그로 기록하고 @) 메서드들의 실행 시간을 기록

 

 

18.2.1 예제 프로젝트 생성

 

ex04라는 이름으로 생성

 

스프링 버전 수정

AspectJ라는 라이브러리 도움을 받음으로 AspectJ 버전도 1.9.0으로 업그레이드.

 

	<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>

spring-test, lombok, junit 추가 및 버전 업데이트

AOP 설정과 가장 중요한 라이브러리 AspectJ Weaver 추가.

 

		<!-- AspectJ -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjrt</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
		<dependency>
			<groupId>org.aspectj</groupId>
			<artifactId>aspectjweaver</artifactId>
			<version>${org.aspectj-version}</version>
		</dependency>

 

코드로 첨부한 것 이외의 설정은 ex03 프로젝트 참고.

 

 

18.2.2 서비스 계층 설계

 

프로젝트에 org.zerock.service 패키지 생성, SampleService 인터페이스와 Impl 클래스 추가.

package org.zerock.service;

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

 

예제로 사용할 객체는 SampleService 인터페이스의 doAdd 메서드를 대상으로 함

인터페이스를 구현한 SampleServiceImpl 클래스는 단순히 문자열을 변환해서 더하기 연산을 하는 단순 작업 작성.

반드시 @Service 어노테이션을 추가해 스프링에서 빈으로 사용될 수 있도록 설정.

 

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를 설계한다.

AOP 기능 설정에 XML도 가능하지만 어노테이션만을 이용하도록 하자.

org.zerock.aop 패키지 생성, LogAdvice 클래스 추가.

 

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 어노테이션 추가. 

@Component 는 AOP와는 관계없지만 스프링에 빈으로 인식하기 위한 것.

logBefore 메서드는 @Before 어노테이션을 적용한다. BeforeAdvice를 구현한 것으로 @After, @AfterReturning, @AfterThrowing, @Around 역시 동일한 방식으로 작동.

 

Advice 관련 어노테이션들은 대부분 내부적으로 Pointcut을 지정한다. 별도의 @Pointcut을 지정해 사용할 수도 있다.

@Before 내부의 문자열은 AspectJ의 표현식이다.

execution은 접근제한자와 특정 클래스의 메서드를 지정할 수 있다.

맨 앞의 *는 접근제한자고, 맨 뒤의 *는 클래스의 이름과 메서드의 이름을 의미한다.

 

 

18.3 AOP 설정

 

스프링2버전 이후엔 간단히 자동으로 Proxy 객체를 만들어주는 설정만 추가하면 됨.

root-context.xml을 선택해 namespace에 aop와 context추가

 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
	
	<!-- Root Context: defines shared resources visible to all other web components -->
	<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>
</beans>

 

 

root-context.xml에선 <component-scan>으로 service와 aop 패키지 스캔. 그 과정에서 SampleServiceImpl 클래스와 LogAdvice는 스프링의 빈으로 등록되고 <aop:aspectj-autoproxy>를 이용해 LogAdvice에 설정한 @Before이 동작.

 

다음과 같은 아이콘이 추가된다.

 

 

18.3.1 Java 설정을 이용하는 경우

 

org.zerock.service와 org.zerock.aop 패키지는 @ComponentScan을 이용하고 @EnableAspectJAutoProxy를 RootConfig에 추가.

 

@Configuration
@ComponentScan(basePackages={"org.zerock.service"})
@ComponentScan(basePackages="org.zerock.aop")
@EnableApectJAutoProxy

@MapperScan(basePackages={"org.zerock.mappeR"})

 

 

18.4 AOP 테스트

 

정상적인 상황이라면 SampleServiceImpl, LogAdvice는 같이 묶여 자동으로 Proxy 객체가 된다.

테스트 폴더에 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)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
//Java Config
//@ContextConfiguration(classes={org.zerock.config.RootConfig.class, org.zerock.config.ServletConfig.class})
@Log4j
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가 정상적으로 모든 동작을 하고 LogAdvice 설정 문제가 없다면 service 변수의 클래스는 단순히 org.zerock.service.SampleServiceImpl의 인스턴스가 아닌 생성된 프록시 클래스의 인스턴스가 된다.

 

단순히 service 변수를 출력했을 땐 기존에 사용하듯 SampleServiceImpl 클래스 인스턴스처럼 보이지만

toString()을 거쳐 getClass를 이용해 파악하면 정확하게 알 수 있따.

com.sun.proxy.$Proxy는 JDK의 다이나믹 프록시 기법이 적용된 결과

 

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)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
//Java Config
//@ContextConfiguration(classes={org.zerock.config.RootConfig.class, org.zerock.config.ServletConfig.class})
@Log4j
public class SampleServiceTests {
	@Setter(onMethod_=@Autowired)
	private SampleService service;
	
	@Test
	public void testClass() {
		log.info(service);
		log.info(service.getClass().getName());
	}
	
	@Test
	public void testAdd() throws Exception {
		log.info(service.doAdd("123",  "456"));
	}
}

 

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

LogAdvice가 SampleService의 doAdd를 실행하기 직전에 간단한 로그를 기록하지만 상황에 따라 해당 메서드의 파라미터가 무엇인지 기록하거나 예외 발생 시 어떤 파라미터에 문제가 있나 알고 싶을 때가 있다.

 

LogAdvice에 적용된 @Before 은 어떤 위치에 Advice를 적용할 것인지를 결정하는 Pointcut인데 설정 시에 args를 이용해 간단히 파라미터를 구할 수 있다.

 

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("=============");
	}
	
	@Before("execution(* org.zerock.service.SampleService*.doAdd(String, String)) && args(str1, str2)")
	public void logBeforeWithParam(String str1, String st2) {
		log.info("str1 : " + str1);
		log.info("str2 : " + str2);
	}
}

 위치에 Advice를 적용할 지 정하는 @Before(execution)에서 설정 시에 arg를 이용해 간단하게 파라미터를 구할 수 있다.

 

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

 

 

18.4.2 @AfterThrowing

 

코드를 실행하다보면 파라미터 값이 잘못되어 예외가 발생하는 경우가 있다. @AfterThrowing은 지정된 대상이 예외를 발생한 후 동작하면서 문제를 찾을 수 있도록 도와줄 수 있다.

 

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 속성을 지정하고 변수 이름을 exceiption으로 지정한다.

 

 

Test 코드 일부.

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

 

 

18.5 @Around와 ProceedingJoinPoint

 

AOP를 이용해 좀더 구체적인 처리를 하고 싶다면 @Around와 @ProceedingJoinPoint를 이용할 수 있다.

@Around는 조금 특별하게 동작하는데 직접 대상 메서드를 실행할 권한을 가지고 메서드 실행 전과 후에 처리가 가능하다.

@ProceedingJoinPoint는 @Around와 결합해 파라미터나 예외를 처리할 수 있다.

 

	
	@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 파라미터를 지정하는데 이는 AOP의 대상이 되는 Target이나 파라미터를 파락할 뿐 아니라 직접 실행을 결정할 수도 있다.

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