Backend/Spring

[Spring] AOP(Aspect Oriented Programming)

양원준 2023. 6. 9. 14:40
728x90

aop 들어 가기 전,. 기본 용어를 알아보자!

 

관심사 : 애플리케이션을 개발하기 위한 구현 기능들을 지칭, 중요 기능들을 핵심 관심사라고 한다

횡단 관심사 :  여러 모듈에 걸쳐 공통적이고 반복적으로 필요로 하는 처리 내용

횡단 관심사 분리 : 횡단 관심사를 한 곳으로 모는 것, 이를 실현하는 방법을 관점 지향 프로그래밍이라 함

 

 

 

 

 

 

 

그래서 aop가 뭔데?

AOP

관점 지향 프로그래밍

여러 비즈니스 모듈에서 공통으로 사용되는 횡단 관심사를 중심으로 설계, 개발하는 프로그래밍 기법

객체지향 프로그래밍보다 완성도 높인 프로그래밍 패러다임

 

예로) 회원, 상품, 배송 서비스에서 핵심 서비스가 따로 있긴 하지만 인증, 보안, 트랜잭션, 캐싱, 로깅 같은 보조 기능은 모두 필요로 한다

 

AOP 목적

DI는 객체 간의 결합도를 낮춤  <->  AOP는 횡단 관심사와 이에 영향 받는 객체 간 결합도를 낮춤

 

AOP 장점

전체 코드에 흩어져 있는 관심사들이 하나의 장소로 응집

비즈니스 모듈들은 본질적인 처리들을 위한 핵심 기능에 대한 코드만 기술, 그 외 공통 관심사들은 공통 모듈로 옮겨지므로 코드가 깔끔해짐

 

AOP 적용

횡단 관심 모듈을 각각 독립된 모듈로 중복 없이 작성하고, DI 설정을 통해 핵심 관심 모듈의 적합한 위치에 결합

 

 

 

 

 

 

 

AOP 주요 용어

adivce : 조인 포인트에 삽입되어 실행되는 코드, 횡단 관심사 구현 부분

join point : 횡단 관심사가 실행되는 지점, advice 적용 가능한 지점들 -> 클래스 인스턴스 생성 시점, 메소드 호출 시점, 예외 발생 시점 등

pointcut : 수많은 join point 중에서 실제 적용될 지점들

aspect : 공통 관심사의 추상적 명칭

target : 핵심 로직을 구현하는 클래스, 횡단 관심사를 적용 받게 되는 대상으로 advice가 적용되는 객체

introduction : target에는 없는 새로운 메서드나 인스턴스 변수들을 추가하는 것

weaving : advice와 target이 결합되어 proxy 객체를 만드는 과정

proxy : advice가 적용되었을 때 만들어지는 객체

 

 

 

 

 

advice 유형

before : 조인포인트 앞에서 실행

after : 조인포인트 뒤에서 실행(예외, 일반종료)

after returning : 조인포인트가 완전히 종료된 다음에 실행(일반종료)

around : 조인포인트 앞뒤에서 실행(예외, 일반종료)

after throwing : 조인포인트에서 예외가 발생했을 때 실행

 

 

 

 

 

 

 

스프링 aop 설계 방식은 프록시 패턴 방식을 통해 구현

프록시 패턴?

프록시 객체는 원래 객체를 감싸고 있는 객체, 프록시 객체가 원래 객체를 감싸 client 요청을 처

어떤 객체를 사용하고자 할때, 객체를 직접적으로 참조 하는 것이 아니라, 그 객체를 대행(proxy)하는 객체를 통해 대상객체에 접근

스프링 컨테이너 초기화 과정에서 스프링 빈 객체를 대행할 프록시 객체 생성

객체 핵심 코드에 대한 영향없이 객체의 접근 전/후(aspect)에 대한 중요 처리 가능

 

간단하게, 대상 객체에 접근할때 메서드 호출을 가로채서 실제 메서드 실행을 위임하기 전에 로깅 또는 보안 검사와 같은 원하는 동작을 적용 가능해진다

 

interface Server {
	public void response();
}


class HttpServer implements Server {
	private String filename;
	public HttpServer(String filename) {
		this.filename = filename;
	}
    
	private void response() {
		System.out.println("response " + filename);
	}
}


class ProxyServer implements Server {
	private String filename;
	private Server server;
		public ProxyServer(String filename) {
			this.filename = filename;
		}
        
public void response() {
	if (server == null) //새로 추가
	{
		server = new HttpServer(filename);
	}
	server. response();
	}
}


class ProxyExample {
	public static void main(String[] args) {
		Server server1 = new ProxyServer("main_1.html");
		Server server2 = new ProxyServer("main_2.html");
        
		server1. response();
		server2. response();
	}
}

메서드가 많아 질때 메서드에 동일한 기능을 하는 코드를 넣고 싶다면, 각각 메서드에 넣는 것은 효율성이 떨어짐

 

위와 같이, 프록시 객체는 원래 객체와 같은 인터페이스를 구현, 원래 객체를 주입 받아서 인터페이스의 메서드들을 위임받아 사용하고, 원하는 추가 코드를 넣어준다

 

 

 

 

 

 

 

AOP 구현 3가지 방법

  1. XML을 이용한 aop 구현
  2. Annotation을 이용한 aop 구현
  3. 자바 코드를 이용한 aop 구현

 


XML을 이용한 AOP 구현

pom.xml 설정

<properties>
	<slf4j.version>1.7.36</slf4j.version>
	<logback.version>1.2.11</logback.version>
    <org.aspectj-version>1.9.8</org.aspectj-version>
</properties>

<dependencies>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>${slf4j.version}</version>
		<scope>compile</scope>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>${logback.version}</version>
		<scope>runtime</scope>
	</dependency>
    <dependency>
		<groupId>org.aspectj</groupId>
		<artifactId>aspectjrt</artifactId>
		<version>${org.aspectj-version}</version>
	</dependency>
	<dependency>
		<groupId>org.aspectj</groupId>
		<artifactId>aspectjtools</artifactId>
		<version>${org.aspectj-version}</version>
	</dependency>
<dependencies>

로깅 프레임워크 : slf4j-api, logback-classic

자바 aop 라이브러리 : aspectjrt, aspectjtools

spring-aop는 spring-context 설치하면 자동으로 추가된다

 

 

 

DI, AOP를 이용한 인터페이스 기반의 컴포넌트화를 완성

AOP를 통해 횡단 관심사와 이에 영향 받는 객체 간 결합도를 낮춘다

 

 

//StudentVO
public class StudentVO {
	private String id;
	private String passwd;
	private String username;
	private String snum;
	private String depart;
	private String mobile;
	private String email;
    
	public String getId() {
		return id;
	}
...............

	@Override
	public String toString() {
	return "StudentVO [id=" + id + ", passwd=" + passwd + ", username=" + username + ", snum=" + snum + ", depart="+ depart + ", mobile=" + mobile + ", email=" + email + "]";
	}
................
}

 

//MemberDAO
public interface MemberDAO {
	public StudentVO read(String id) throws Exception;
	public void add(StudentVO student) throws Exception;
}
//MemberDAOImpl
public class MemberDAOImpl implements MemberDAO {

	private Map<String, StudentVO> storage = new HashMap<String, StudentVO>();
	public StudentVO read(String id) throws Exception { return storage.get(id); }
	public void add(StudentVO student) throws Exception {
		storage.put(student.getId(), student);
	}
    
}

 

 

//MemberService

public interface MemberService {
	public StudentVO readMember(String userid) throws Exception;
	public void addMember(StudentVO student) throws Exception;
}
//MemberServiceImpl

public class MemberServiceImpl implements MemberService {

	private MemberDAO memberDAO;
	public MemberServiceImpl(MemberDAO memberDAO) {
		this.memberDAO = memberDAO;
	}
    
	public StudentVO readMember(String userid) throws Exception {
		return memberDAO.read(userid);
	}
    
	public void addMember(StudentVO student) throws Exception {
		memberDAO.add(student);
	}
}

 

 

//MemberAspect

public class MemberAspect {
	public void beforeMethod(JoinPoint jp) {
		System.out.println(” [BeforeMethod] : 메소드 호출 전 ”);
		Signature sig = jp.getSignature();
		System.out.println(” 메소드 이름: ” + sig.getName());
		Object[] obj = jp.getArgs();
		System.out.println(” 인수 값: ” + obj[0]);
	}
    
	public void afterMethod() {
		System.out.println(” [AfterMethod] : 메소드 호출 후 ”);
	}
    
	public void afterReturningMethod(JoinPoint jp, StudentVO member) {
		System.out.println(” [afterReturningMethod] : 메소드 호출 후 ”);
		Signature sig = jp.getSignature();
		System.out.println(” 메소드 이름: ” + sig.getName());
		Object[] obj = jp.getArgs();
		System.out.println(” 인수 값: ” + obj[0]);
	}
    
	public StudentVO aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
		System.out.println(” [AroundMethod before] : 메소드 호출 전”);
		StudentVO member = (StudentVO) pjp.proceed();
		System.out.println(” [AroundMethod after] : 메소드 호출 후");
		return member;
	}
    
	public void afterThrowingMethod(Throwable ex) {
	// 메소드 호출이 예외를 내보냈을 때 호출되는 Advice
		System.out.println(” [AfterThrowingMethod] : 예외 발생 후");
		System.out.println(” exception value = " + ex.toString());
	}
}

각 메서드가 advice

 

joinpoint : 메서드 명과 인수 값 접근 가능

afterReturning : 메서드 수행 후 반환한 값 접근 가능

around advice : proceed 함수를 통해 메서드 수행을 반드시 처리, 반환값 반드시 리턴

trowing advice : aop 대상이 되는 메서드에서 예외가 발생할 경우에만 동작

 

//applicationContext.xml

//xml 방식 di 설정
<bean id="memberDAO" class="org.tukorea.di.persistence.MemberDAOImpl">
</bean>

<bean id="memberService" class="org.tukorea.di.service.MemberServiceImpl">
	<constructor-arg ref="memberDAO"/> <!-- Constructor Setting -->
</bean>

//ascpect 클래스
<bean id="memberAspect" class="org.tukorea.aop.MemberAspect">
</bean>

<aop:config>
	<aop:aspect id="testAspect" ref="memberAspect">
    	//expression = 호출되는 메서드, 클래스 명 지정 방식
		<aop:pointcut id="readMethod" expression="execution(* read(String))"/>
        //method = ascpect에 정의된 advice
		<aop:before pointcut-ref="readMethod" method="beforeMethod" />
		<aop:after pointcut-ref="readMethod" method="afterMethod"/>
		<aop:after-returning pointcut-ref="readMethod" method="afterReturningMethod" returning="member"/>
		<aop:around pointcut-ref="readMethod" method="aroundMethod"/>
		<aop:after-throwing pointcut-ref="readMethod" method="afterThrowingMethod" throwing="ex"/>
	</aop:aspect>
</aop:config>

applicationContext.xml 의 aop 태그

 

aop:config : aop정의를 위한 최상위 엘리먼트

aop:aspect : aspect를 정의

aop:pointcut : pointcut을 정의

aop:before : before advice를 정의

aop:after : after advice를 정의

aop:after-returning : after-returning advice를 정의

aop:around : around advice를 정의

aop:after-throwing : after-throwing advice를 정의

 

Advice 코드 실행 흐름(before Advice)

 

PointCut 기술 방법

execution() 표현식

  • 메서드의 수식자, throws 는 생략 가능
  • 메서드의 반환값, 패키지와 클래스 명은 와일드 카드 '*'를 이용 가능
  • 패키지, 클래스 명은 생략 가능
  • 해당 패키지 및 하위 패키지를 포함하여 일치시키려면 '..'를 이용

 

execution() 표현식 예

execution(* org.tukorea.di.persistence.MemberDAOImpl.write(..)

리턴 타입과 파라미터 상관 없이, 클래스는 위와 같고, 메소드 명이 write인 메소드를 선정하는 포인트컷 표현식

 

execution(* write(int, int))

리턴 타입 상관없이, 모든 패키지, 클래스 내, 메소드 이름이 write이며, 두개의 int 타입의 파라미터를 가진 모든 메소드를 선정하는 포인트컷 표현식

 

execution(* *(..))

리턴 타입과 파라미터의 종류, 개수에 상관없이 모든 패키지, 클래스 내의 모든 메소드를 선정하는 포인트컷 표현식

 

 

within() 표현식

특정 타입에 속하는 메소드를 포인트컷으로 설정할 때 사용

execution()의 여러 조건 중에서 타입 패턴 만을 적용하기 때문에 execution 표현식 문법보다 간단하다

 

within(org.tukorea.aop..*)

org.tukorea.aop 및 모든 서브패키지가 포함하고 있는 모든 메소드

 

within(org.tukorea.aop.*)

org.tukorea.aop 패키지 밑의 인터페이스와 클래스에 있는 모든 메소드

 

within(org.tukorea.aop.xml)

org.tukorea.aop 패키지의 xml 클래스의 모든 메소드

 

 

와일드 카드

* : 기본적으로 임의의 문자열, 패키지를 표현할 때는 패키지 1개 계층, 메서드의 매개변수를 표현할 때는 임의의 인수 1개

.. : 패키지를 표현할 때는 패키지 0개 이상 계층, 메서드의 매개변수를 표현할 때는 임의의 인수 0개 이상

+ : 클래스 명 뒤에 붙여 쓰며 해당 클래스와 해당 클래스의 서브 클래스, 혹은 구현 클래스 모두를 의미

 

 

 


애노테이션 기반의 aop 구현

 

//MemberAspect

@Aspect
@Component
Public class MemberAspect {
	@Before("execution(* read(String))")
	public void beforeMethod(JoinPoint jp) {
		System.out.println(” [BeforeMethod] : 메소드 호출 전 ”);
		Signature sig = jp.getSignature();
		System.out.println(” 메소드 이름: ” + sig.getName());
		Object[] obj = jp.getArgs();
		System.out.println(” 인수 값: ” + obj[0]);
	}
    
	@After("execution(* read(String))")
	public void afterMethod() {
		System.out.println(” [AfterMethod] : 메소드 호출 후 ”);
	}
    
	@AfterReturning(value = "execution(* read(String))", returning = "member")
	public void afterReturningMethod(JoinPoint jp, StudentVO member) {
		System.out.println(” [afterReturningMethod] : 메소드 호출 후 ”);
		Signature sig = jp.getSignature();
		System.out.println(” 메소드 이름: ” + sig.getName());
		Object[] obj = jp.getArgs();
		System.out.println(” 인수 값: ” + obj[0]);
	}
    
	@Around("execution(* read(String))")
		public StudentVO aroundMethod(ProceedingJoinPoint pjp) throws Throwable {
		System.out.println(” [AroundMethod before] : 메소드 호출 전”);
		StudentVO member = (StudentVO) pjp.proceed();
		System.out.println(” [AroundMethod after] : 메소드 호출 후");
		return member;
	}
    
	@AfterThrowing(value = "execution(* read(String))", throwing = "ex")
	public void afterThrowingMethod(Throwable ex) {
		// 메소드 호출이 예외를 내보냈을 때 호출되는 Advice
		System.out.println(” [AfterThrowingMethod] : 예외 발생 후");
		System.out.println(” exception value = " + ex.toString());
	}
}

aspect 선언 : @Aspect

di 컴포넌트 선언 : @Component

advice 선언 : @Before, @After, @AfterReturning, @ Arround, @AfterThrowing

pointcut 선언 : @Pointcut( " execution()" )

 

@Before("execution(* read(String)) " ), @After("exection(* read(String)) " )

반환형 : void, 매개변수없음

pointcut : "execution(* read(String))"

@AfterReturning(value = "execution(* read(String))", returning = "member")

메서드의 반환형 정의 : returning = "member"

pointcut : "exection(* read(String), )", returning = "member")

@Around(" execution(* read(String)) ")

메서드의 반환형은 aop 대상의 메서드 반환형과 호환

pointcut : "execution(* read(String)) " 

@AfterThrowing(value = " execution(* read(String)) " , throwing = "ex")

메서드의 매개변수에 캐치할 예외를 선언

pointcut : "execution(* read(String))"

 

 

DI annotation 설정

@Component
public class MemberDAOImpl implements MemberDAO {
		private Map<String, StudentVO> storage = new HashMap<String, StudentVO>();
		public StudentVO read(String id) throws Exception { return storage.get(id); }
		public void add(StudentVO student) throws Exception {
				storage.put(student.getId(), student);
		}
}
@Component
public class MemberServiceImpl implements MemberService {
	@Autowired
	private MemberDAO memberDAO;
	public StudentVO readMember(String userid) throws Exception {
		return memberDAO.read(userid);
	}
    
	public void addMember(StudentVO student) throws Exception {
		memberDAO.add(student);
	}
}

위와 같이 @Component, @Autowired를 이용하여 di 설정

 

 

//applicationContext.xml
//애노테이션 방식 di 설정
<context:component-scan base-package="org.tukorea.di.persistence"></context:component-scan>
<context:component-scan base-package="org.tukorea.di.service"></context:component-scan>
<context:component-scan base-package="org.tukorea.aop"></context:component-scan>

//aspect 애노테이션이 적용된 빈 객체를 aspect로 사용하게 됨
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

위와 같이 di, aop 설정

<aop:aspectj-autoproxy / > : aop 설정을 애노테이션으로 적용하여 사용

 

 

//MemberSampleMain

package org.tukorea.di.main;
import org.tukorea.di.domain.StudentVO;
import org.tukorea.di.service.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class MemberSampleMain {
	private static ApplicationContext ctx = null;
	public static void main(String[] args) throws Exception {
		System.out.println("안녕하세요 AOP-ANNOTATION");
        
		ctx = new GenericXmlApplicationContext("classpath:applicationContext.xml");
		MemberService memberService = ctx.getBean(MemberService.class); // by Class Name
        
		StudentVO vo = new StudentVO(); vo.setId("yang");
		memberService.addMember(vo);

		StudentVO member = memberService.readMember("yang");
		System.out.println(member);
	}
}

 

 

 


JAVA 기반 AOP 구현

//JavaConfig

package org.tukorea.di.config;
.....................................................................................
@Configuration
@EnableAspectJAutoProxy
public class JavaConfig {
	@Bean
	public MemberDAO memberDAO() {
		return new MemberDAOImpl();
	}
	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberDAO());
	}
	@Bean
	public MemberAspect memberAspect() {
		return new MemberAspect();
	}
}

javaconfig 클래스를 만들어 aop정의

@EnableAspectJAutoProxy 선언이 필요  == aop:aspectj-autoproxy 태그와 같은 역할

 

 

package org.tukorea.di.main;
import org.tukorea.di.domain.StudentVO;
import org.tukorea.di.service.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberSampleMain {
	private static ApplicationContext ctx = null;
	public static void main(String[] args) throws Exception {
		System.out.println("안녕하세요 AOP-JAVA");

		ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
		MemberService memberService = ctx.getBean(MemberService.class); // by Class Name

		StudentVO vo = new StudentVO(); vo.setId("yang");
		memberService.addMember(vo);

		StudentVO member = memberService.readMember("yang");
		System.out.println(member);
	}
}
728x90

'Backend > Spring' 카테고리의 다른 글

[Spring] Mybatis_2  (0) 2023.06.09
[Spring] Mybatis  (0) 2023.06.09