스터디 발표 내용

7 . 스프링의 핵심 기술 !! - AOP 프로그래밍

비뀨_ 2021. 11. 14. 03:27

AOP( Aspect Oriented Programming : 관점 지향 프로그래밍 )란

OOP( Object Oriented Programming : 객체 지향 프로그래밍)을 보완하는 수단으로 ,

 

여러 곳에서 쓰이는 공통 기능을 모듈화하고, 쓰이는 곳에 필요할 때 연결함으로 ,

유지보수, 재사용이 용이하도록 프로그래밍 하는 것이다.

 

그냥 OOP 쓰면 되지 왜 AOP를 따로 쓰냐 ?? 

예시로 쉽게 이해해보자.

더보기

개발자가 회원가입 처리에 걸리는 시간을 계산하고 로그를 찍는 코드를 추가했다.

그런데 구현된 것을 본 팀장님이 너무 좋아서 모든 service에 적용하게 만들라고 했다.

근데 그 프로젝트의 로직은 10만개 이다.

카드 값을 위해 퇴사하지 못한 개발자는 돌아와서 모든 로직에 적용을 하던 중 생각한다.

서비스를 처리하는 로직에다 로그 찍는 코드(부가기능)를 추가하는게 맞나??

중복 코드들은 어떻게 하지..? 란 고민에 휩싸이게 된다.

 

출처 :  유튜브 우아한테크 https://www.youtube.com/watch?v=Hm0w_9ngDpM&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9CTech

이것을 해결하기 위해 AOP를 사용하는 것이다. 

OOP 와 AOP 에서의 쿠팡 서비스

AOP에서는 횡단 관심사를 가진다.

각 서비스 마다 물품등록,  구매 , 추천 등이 중복이 가로로 일어나기 때문에

각 서비스 별이 아닌 각 기능을 기준으로 나누게 된다.

 

그럼 AOP와 OOP는 다른 것을 지향하고 있기 때문에 다른 개념인가?

더보기

AOP와 OOP는 다른 개념이 아니라 

AOP는 프로그램 구조에 대한 다른 관점으로, OOP를 만들기 위해 AOP를 OOP의 보완적인 개념으로 보는게 옳다고 생각한다.

 

AOP의 기본 개념

핵심 기능에 공통된 기능을 삽입하는 것. 

 

공통기능을 삽입하는 방법.

AspectJ

  • 컴파일 시점에서 코드에 공통 기능을 삽입한다. ( A.java -> A.class 로 컴파일 하기 전)
  • 클래스 로딩 시점에서 바이트 코드에 공통 기능을 삽입한다. ( A.class를 클래스로더가 메모리 상에 올릴 때)

Spring AOP

  • 런타임에 프록시 객체를 생성해서 공통 기능을 삽입한다. ( A라는 타겟을 프록시로 감싸서 실행. Ioc, DI때문에 가능)

 

※ proxy  - 대신 해주는 사람, 대리인의 뜻.

 

AOP의 용어

용어 의미
Aspect  여러 객체에 공통으로 적용되는 기능
ex) 트랜잭션 , 보안 , 위의 쿠팡 예시에서 물품등록 등
Target 어떤 대상에 부가 기능을 부여할 것인가
Advice 언제 공통관심 기능을 핵심 로직에 적용할지 정의.
ex ) Before , After Returnning, After Throwing , After , Around 
Join point Advice를 적용 가능한 지점 (어디에) : 메서드 , 필드 , 객체 , 생성자 등.
스프링 AOP는 Proxy를 이용해서 AOP 구현해서 메서드 호출에 대한 Join point만 지원.
weaving Advice를 핵심 로직 코드에 적용하는 것.
Point cut 실제 advice가 적용될 지점을 선정.
Spring AOP에서는 advice가 적용될 메서드를 선정.

 

스프링 AOP 사용.

  1. Aspect로 사용할 클래스에 @Aspect 어노테이션 붙인다.
  2. @Pointcut 어노테이션으로 공통 기능을 적용할 Pointcut 정의한다.
  3. 공통 기능을 만든 메서드에 @Around 어노테이션을 적용.

스프링 프레임워크가 프록시 알아서 만들어 줌!!

 

// @Aspect를 적용한 클래스는 Advice와 Pointcut을 함께 제공.
@Aspect
public class ExeTimeAspect {

	//chap07 패키지와 하위 패키지의 public 메서드를 Pointcut으로 설정.
	@Pointcut("execution(public * chap07..*(..))")
	private void publicTarget() {
	}
	// 실행전&후 , 예외 발생 시점에 공통기능 실행.
    // publicTarget() 메서드의 Pointcut에 공통기능 적용.
	@Around("publicTarget()")
	public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
		long start = System.nanoTime();
		try {
			Object result = joinPoint.proceed();
			return result;
		} finally {
			long finish = System.nanoTime();
			Signature sig = joinPoint.getSignature();
			System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n",
					joinPoint.getTarget().getClass().getSimpleName(),
					sig.getName(), Arrays.toString(joinPoint.getArgs()),
					(finish - start));
		}
	}

}

@Aspect 클래스를 정의 했으면 , 설정 클래스에 추가해줘야 한다.

//설정 클래스 AppCtx.java

@Configuration
@EnableAspectJAutoProxy //AspectJ 를 사용할거다라는 어노테이션
public class AppCtx {
	@Bean
	public ExeTimeAspect exeTimeAspect() {
		return new ExeTimeAspect();
	}

	@Bean
	public Calculator calculator() {
		return new RecCalculator();
	}

}

@EnableAspectJAutoProxy 어노테이션을 붙이면,  

  1. @Aspect 어노테이션이 붙은 빈 객체를 찾아서
  2. @Pointcut 과 @Around 설정을 사용한다.

 

public class MainAspect {
	
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = 
				new AnnotationConfigApplicationContext(AppCtx.class);
		// 팩토리얼 구하는 클래스. 지금은 이게 중요한게 아니라 안 쓰겠다.
		Calculator cal = ctx.getBean("calculator", Calculator.class);
		long fiveFact = cal.factorial(5);
		System.out.println("cal.factorial(5) = " + fiveFact);
		System.out.println(cal.getClass().getName());
		ctx.close();
	}

}

 

더보기

결과 -----

RecCalculator.factorial([5]) 실행 시간 : 34600 ns   //ExeTimeAspect.java 의 measure()
cal.factorial(5) = 120 
com.sun.proxy.$Proxy17 // 

cal.getClass().getName() 메서드를 호출한 결과의 마지막 줄의 타입이 Proxy인 이유는 스프링이 생성한 프록시가 

위의 Appctx에서 생성한 Calculator 빈을 프록시라는 객체로 감쌌기 때문이다.

 

 

@ProceedingJoinPoint ? 

@Around Advice를 사용할 공통 기능 메서드는 ProceedingJoinPoint의 proceed() 를 호출하면 된다.

(? : 잘 이해 안 감.)

@ProceedingJoinPoint는 아래 메소드를 제공한다.

  • Signature getSignature() :  호출되는 메서드에 대한 정보를 구함.
  • Object getTarget() : 대상 객체를 구한다.
  • Object[] getArgs() : 파라미터 목록을 구한다.

또 Signature 인터페이스는 아래 클래스를 제공 

  • String getName() : 호출되는 메서드의 이름을 구함.
  • String toLongString() :호출되는 메서드의 리턴 타입 , 파라미터 타입 등의 자세한 정보를 표현한 문장 구현
  • String toShortString() : 축약한 문장. 메서드의 이름 정도.

 

위 : toLongString() 아래 : toShortString()

 

execution 명시자 표현식

 

더보기

execution 명시자는 Advice를 적용할 메서드를 지정할 때 사용한다.

기본 형식 : 

execution(  수식어패턴? 리턴타입패턴  클래스이름패턴? 메서드이름패턴(파라미터패턴)  )

  -  *은 전부 ,  ..은 0개 이상 , ?는 생략 가능하다는 뜻   -

수식어     : public , protected , private ( 생략 가능 - 스프링AOP는 public 메서드만 적용 가능함.)

리턴타입  : void , String , * 등    

클래스이름 : 클래스 이름. 클래스 이름 대신 패키지 명이 올 수도 있음.

@Pointcut("execution(public * chap07..*(..))")
	private void publicTarget() {
	}

위를 풀어 설명하면 Advice를 적용할 메서드를 지정할건데 ,

chap07 패키지 및 하위패키지까지 모든 메서드의 파라미터 갯수에 상관없이 적용할 것이다.라는 뜻이다.

 

Aspect의 순서 정하기

 

Aspect가 적용되는 순서는 스프링 프레임워크나 자바 버전에 따라 달라질 수 있기 때문에

순서를 직접 정하고 싶다면 @Order 어노테이션을 사용해준다.

 

@Aspect
@Order(1) // 우선순위가 1인 Aspect
public class ExeTimeAspect {

 

@Around에서 Pointcut 설정  

@Pointcut이 아니라 언제 실행될지 정하는 Advice인 @Around에서도 execution 명시자를 직접 지정할 수 있다.

 

@Pointcut 재사용

만약 같은 Pointcut을 여러 Advice에서 사용한다면 공통 Pointcut을 재사용할 수도 있다.

재사용하려면 Pointcut은 public 이어야 한다. 

 

@Pointcut("execution(public * chap07..*(..))")
	private void publicTarget() {
}


//다른 패키지에서 쓸 때에는 패키지 명까지 적어줘야 한다. 

@Around("aspect.ExeTimeAspect.publicTarget()")
public Object  execute(ProceedingJoinPoint joinPoint)........