본문 바로가기
Java/Spring

Spring) AOP 흐름 이해하기, 원리 및 구조 파악

by 박채니 2022. 8. 25.

안녕하세요, 코린이의 코딩 학습기 채니 입니다.

 

개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.


AOP 흐름 이해하기

 

관련 의존 추가

#9. AOP 관련 의존 추가

pom.xml

<!-- #9. AOP관련 의존 추가 -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${org.aspectj-version}</version>
</dependency>

 

 

#9.1 AOP 관련 어노테이션 등록 처리

servlet-context.xml

	<!-- #9.1 aop관련 어노테이션 등록처리 -->
	<aop:aspectj-autoproxy />

 

LogAspect

@Component
@Aspect // AOP적으로 활용되기 위해 어노테이션 추가!
@Slf4j
public class LogAspect {
	
	// 모든리턴타입 todo패키지하위.모든클래스.모든메소드(타입이 있거나없거나)
	@Pointcut("execution(* com.ce.spring2.todo..*(..))") 
	public void pointcut() {}
	
	@Around("pointcut()")
	public Object logAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
		Signature signature = joinPoint.getSignature(); // 메소드 시그니처
		String typeName = signature.getDeclaringTypeName(); // 클래스명
		String methodName = signature.getName(); // 메소드명
		
		// Before
		log.debug("{}.{} 실행 전!", typeName, methodName);
		
		// 주업무 메소드 실행
		Object obj = joinPoint.proceed();
		
		// After
		log.debug("{}.{} 실행 후!", typeName, methodName);
		
		return obj;
	}
}

 

주업무 메소드가 실행되지 전과 후에 Aspect가 실행되도록 하였습니다.

또한 모든리턴타입 todo패키지하위.모든클래스.모든메소드(타입이 있거나 없거나)에 한하여 pointcut을 지정하였습니다.

@Around를 통해 joinpoint와 결합하여 동작하는 시점을 지정해주었습니다.

 

 

TodoList 조회 후 콘솔 출력값

DEBUG: com.ce.spring2.common.aop.LogAspect - com.ce.spring2.todo.controller.TodoController.todoList 실행 전!
DEBUG: com.ce.spring2.common.aop.LogAspect - com.ce.spring2.todo.model.service.TodoService.selectAllTodo 실행 전!
DEBUG: com.ce.spring2.todo.model.dao.TodoDao.selectAllTodo - ==>  Preparing: select * from (select * from todo where completed_at is null order by no) union all select * from (select * from todo where completed_at is not null order by completed_at desc) 
DEBUG: com.ce.spring2.todo.model.dao.TodoDao.selectAllTodo - ==> Parameters: 
DEBUG: com.ce.spring2.todo.model.dao.TodoDao.selectAllTodo - <==      Total: 5
DEBUG: com.ce.spring2.common.aop.LogAspect - com.ce.spring2.todo.model.service.TodoService.selectAllTodo 실행 후!
DEBUG: com.ce.spring2.todo.controller.TodoController - list = [Todo(no=1, todo=우산 청소하기, createdAt=2022-08-24T12:57:38, completedAt=null), Todo(no=2, todo=형광등 교체, createdAt=2022-08-24T12:57:48, completedAt=null), Todo(no=21, todo=딸기 요거트 사기, createdAt=2022-08-24T14:34:18, completedAt=null), Todo(no=3, todo=장 보기, createdAt=2022-08-24T12:57:56, completedAt=2022-08-24T14:58:36), Todo(no=4, todo=차에 물 퍼내기, createdAt=2022-08-24T12:58:02, completedAt=2022-08-24T14:54:47)]
DEBUG: com.ce.spring2.common.aop.LogAspect - com.ce.spring2.todo.controller.TodoController.todoList 실행 후!

Controller, Service 메소드가 실행되기 전과 후에 Aspect가 끼어든 것을 확인할 수 있습니다.

 

위와 같이 메소드 사이사이에 끼어드는 것이 가능한 이유는! 의존 주입 때문!!!


원리 파악하기

 

TodoController

public class TodoController {
	@Autowired
	private TodoService todoService;
	
	@GetMapping("/todoList.do")
	public void todoList(Model model) {
		try {
			// new TodoServiceImpl()
			log.debug("todoService = {}", new TodoServiceImpl().getClass());
			log.debug("todoService = {}", todoService.getClass());
			
			
			List<Todo> list = todoService.selectAllTodo();
			log.debug("list = {}", list);
			model.addAttribute("list", list);
		} catch(Exception e) {
			log.error(e.getMessage(), e);
			throw e;
		}
	}
    
@콘솔출력값
DEBUG: com.ce.spring2.todo.controller.TodoController - todoService = class com.ce.spring2.todo.model.service.TodoServiceImpl
DEBUG: com.ce.spring2.todo.controller.TodoController - todoService = class com.sun.proxy.$Proxy95

이처럼 의존 주입 받은 todoService의 class를 가져와 출력해보니 이상한 Proxy객체가 리턴 되는 것을 확인할 수 있습니다.

TodoService interface의 구현체인 TodoServiceImpl 객체를 주입받는 줄 알았는데 아니였던 것이죠.

 

class com.sun.proxy.$Proxy95 jdk가 제공하는 dynamic proxy 객체 (동적으로 Proxy객체를 생성!)

즉, 생성한 todoService의 역할을 대신 해주는 객체가 만들어져있는 것!!

생성된 proxy 객체 안에서 차례대로 Advice를 호출해주고, TodoServiceImpl에 있는 내용을 호출해주어 마치 끼어들어간 것처럼 느껴지는 겁니다.

 

AOP에서 사용하는 Proxy 객체

인터페이스 구현 객체 : jdk 동적 proxy객체 (class com.sun.proxy.$Proxy95)
인터페이스 구현하지 않은 객체 : cglib 라이브러리에서 생성한 proxy객체

 


Proxy 대략적인 구조 파악하기

 

ProxyMain

public class ProxyMain {
	
	FooService fooService = new FooProxy(new FooServiceImpl()); // FooServiceImpl을 의존주입 받는다고 가정!
	
	public static void main(String[] args) {
		new ProxyMain().test();
	}

	private void test() {
		String name = fooService.getName();
		System.out.println(name);
	}
	
}

class FooProxy implements FooService {
	FooService fooService;
	
	public FooProxy(FooService fooService) {
		this.fooService = fooService;
	}
	
	@Override
	public String getName() {
		return "abcde";
	}
}

class Aspect {
	public void beforeAspect() {
		System.out.println("before!!!");
	}
	
	public void afterAspect() {
		System.out.println("after!!!");
	}
}

interface FooService {
	String getName();
}

class FooServiceImpl implements FooService {
	@Override
	public String getName() {
		return "abcde";
	}
}

@콘솔출력값
abcde

FooService interface가 있고, 그의 구현체인 FooServiceImpl, FooProxy 객체가 있습니다.

FooService의 getName() 메소드를 호출하려고 하는데, FooService는 new FooProxy(new FooServiceImpl()) 객체를 주입받았습니다.

따라서 FooProxy 클래스의 FooService에는 FooServiceImpl() 객체가 주입되고, 해당 객체의 getName()을 출력하게 됩니다.

 

이 때, Aspect 클래스의 beforeAspect와 afterAspect를 실행하려고 합니다. 단! 기존 test()메소드는 그대로 둔 상태에서!

아래와 같이 해결할 수 있을겁니다.

public class ProxyMain {
	
	FooService fooService = new FooProxy(new FooServiceImpl(), new Aspect()); // FooServiceImpl을 의존주입 받는다고 가정!
	
	public static void main(String[] args) {
		new ProxyMain().test();
	}

	private void test() {
		String name = fooService.getName();
		System.out.println(name);
	}
	
}

class FooProxy implements FooService {
	FooService fooService;
	Aspect aspect;
	
	public FooProxy(FooService fooService, Aspect aspect) {
		this.fooService = fooService;
		this.aspect = aspect;
	}
	
	@Override
	public String getName() {
		// before
		aspect.beforeAspect();
		
		// 주업무
		String name = fooService.getName(); // 주기능 joinpoint 실행
		
		// after
		aspect.afterAspect();
		
		return name;
	}
}

class Aspect {
	public void beforeAspect() {
		System.out.println("before!!!");
	}
	
	public void afterAspect() {
		System.out.println("after!!!");
	}
}

interface FooService {
	String getName();
	
}

class FooServiceImpl implements FooService {
	@Override
	public String getName() {
		return "abcde";
	}
}

@콘솔출력값
before!!!
after!!!
abcde

 

 

의존 주입을 Spring이 대신 해주기 때문에 위와 같은 구조가 내부적으로 일어나는 지 몰랐던 것이죠.

(우리가 만든 객체를 감싼 Proxy객체가 의존주입되고, advice까지 실행됨)

 

이를 이용하여 아래와 같은 부가기능을 수행할 수 있겠죠.

대문자로 변환

class FooProxy implements FooService {
	FooService fooService;
	Aspect aspect;
	
	public FooProxy(FooService fooService, Aspect aspect) {
		this.fooService = fooService;
		this.aspect = aspect;
	}
	
	@Override
	public String getName() {
		// before
		aspect.beforeAspect();
		
		// 주업무
		String name = fooService.getName(); // 주기능 joinpoint 실행
		
		// after
		return aspect.afterAspect(name);
		
//		return name;
	}
}

class Aspect {
	public void beforeAspect() {
		System.out.println("before!!!");
	}
	
	public String afterAspect(String str) {
		System.out.println("after!!!");
		return str.toUpperCase();
	}
}


@콘솔출력값
before!!!
after!!!
ABCDE