본문 바로가기

Spring

Spring AOP의 동작 원리 (1) - JDK Dynamic Proxy

728x90

이전에 AOP 사용에 대해 정리한 글이 있는데, 생각해보니 정확한 원리에 대해서 알아보지 못한거 같아 정리하려고 한다.

Spring에서 AOP는 프록시 방식으로 동작한다. 크게 두가지 프록시 방식이 있는데

 

1. JDK Dynamic Proxy

2. CGLib

 

이 중 1번 jdk의 동적 프록시에 대해서 개인적으로 찾아보고 생각한 내용을 정리해본다.

 

프록시가 뭐시여

프록시는 대리 수행하는 놈이라고 생각한다. 사실 프록시는 다양한 곳에서 널리 쓰이는 개념인데, 그렇다면 Spring에서 프록시는 어떤 것으로 생각해야할까. 이렇다는건 아니지만 느낌을 쉽게 받기위해 프록시 유무에 따라 동작을 비교해 본다.

 

프록시X

 

 

프록시 O

 

 

Service Proxy는 실제 Service가 아니지만 Service인척 Controller부터 요청을 이어받고, 실제 필요한 Service의 Something()을 수행하기 전 설정된 부가기능들을 수행하는것이 Spring에서의 프록시라고 생각하면 편하다.

사실 크게보면 이게 Spring AOP의 동작이다.

 

Spring을 사용하면서 AOP는 @Aspect를 활용해 쉽게 적용이 가능한데, AOP적용을 위한 프록시를 개발자가 직접 구현하는건 상당히 번거로운 작업일 것이다.

 

프록시 직접 구현 예시

 

Contoller

@Controller
public class UserController {

	@Autowired
	private UserService userService;
	
	@RequestMapping("/user")
	public String getUserName(HttpServletRequest request, HttpServletResponse response) {
		
		// ...
		// Do Somethings
		String user_name = userService.getUserName();
		// ...
		
		return "";
	}
}

 

 

UserService와 UserServiceImpl

public interface UserService {

	public String getUserName();
	
    ...수 많은 메서드들
}


@Service
public class UserServiceImpl implements UserService {

	@Override
	public String getUserName() {
    	//... do something logic
        System.out.println("홍길동");
		return "홍길동";
	}

	...수 많은 메서드들
}

 

위와 같은 구성이라고 가정할 때, 프록시 없이 성능을 체크하는 부가 기능을 몇개 메서드에 적용하고 싶다면

@Service
public class UserServiceImpl implements UserService {

	@Override
	public String getUserName() {
    
    	System.out.println("시간 체크 시작");
    	//... do something logic
        System.out.println("홍길동");
        System.out.println("시간 체크 종료");
        System.out.println("시간 : 2초");
        
		return "홍길동";
	}

	...수 많은 메서드들
}

이런 느낌이 되지않을까 싶다.

당연히 이 방법은 메서드마다 중복된 코드가 상당히 많아져서 비효율적이다.

 

개발자가 직접 프록시를 만들어 사용한다면?

@Service
public class UserServiceProxy implements UserService{

	private UserService userService;
	
	public UserServiceProxy() {
		userService = new UserServiceImpl();
	}
	
	@Override
	public String getUserName() {

		System.out.println("시간 체크 시작");
		
		//실제 메서드 호출
		String res = userService.getUserName();
        
		System.out.println("시간 체크 종료");
        System.out.println("시간 : 2초");
		
		return res;
	}

	... 메서드들
}

약간 이런 느낌의 구성이라고 생각한다. 대충 생각한거고 실제로 new UserServiceImpl을 하지 않겠지만 의존성을 주입한거라고 가정한다.

 

아무튼 이렇게 개발자가 직접 프록시를 구현한다면 생기는 단점은

실제 프로젝트는 Service가 많이 있을텐데 그만큼 프록시를 생성해줘야하고

많은 Service마다 또 많은 메서드에 대해 정의를 해야 하는점이다.

 

Service가 몇개든지, 그 안에 메서드가 몇개든지 애플리케이션 런타임 시, 동적으로 그만큼 프록시가 알아서 생긴다면 얼마나 편리할까? 그게 JDK Dynamic Proxy다.

 

 

JDK Dynamic Proxy

JDK Dynamic Proxy는 두가지 특징이 있다

  • Java의 Reflection을 활용해 동적으로 생성
  • 인터페이스를 기준으로 Proxy 인스턴스 생성

Reflection은 클래스의 생성자, 메서드, 변수 등의 정보를 가져올 수 있는 java.lang에 있는 라이브러리이자 기능이다.

String의 문자열로 클래스를 가져올 수 있는데, 사실 정확히 모르지만 메모리에 올라간 class파일들을 탐색한다는 글을 얼핏 본 것 같다.

 

아무튼, 동적 프록시는 Reflection을 활용해 인터페이스들을 탐색하고, 그만큼 동적으로 프록시를 생성하는 것만 알아둬도 될것 같다. 프록시를 생성하는 Proxy.newProxyInstance에 대해 분석한 글이 있는데 참고하면 상당히 좋다.

 

https://taes-k.github.io/2021/05/15/dynamic-proxy-reflection/

 

몰라도 되는 Spring - 리플렉션으로 만드는 Dynamic proxy

Dynamic proxy 이전 AOP Proxy 포스팅에서 Spring 에서 사용하는 Dynamic proxy와 CGLib를 이용한 proxy에 대해 말씀드리면서 다음과 같은 설명이 있었습니다. JDK dynamic proxy Reflection을 통해 동적으로 proxy 객체

taes-k.github.io

 

그리고 인터페이스를 기준으로 프록시를 생성하기 때문에 Service와 ServiceImpl을 항상 1:1로 구현해온 프로젝트가 많이 있고 그 관습이 이어져왔다고 생각한다.

 

(개인적으로 해소되지 않은 의문이 있는데, Reflection이 인터페이스만 탐색이 가능한게 아니고 클래스 파일도 될텐데 왜 굳이 프록시는 인터페이스를 기준으로 생성할까 하는 의문이 있다. 이는 아무리 찾아봐도 아직 만족스러운 답을 못얻었다.)

 

 

InvocationHandler

JDK Dynamic Proxy에서 사용하는 프록시 생성의 핵심, 생성할 프록시가 어떻게 동작할지 정의하는 인터페이스다.

말보다는 코드로 봐야한다.

 

 

InvocationHandler 활용해서 dynamic proxy 구현해본다.

 

InvocationHandler 

public class UserServiceProxyHandler implements InvocationHandler{

	private UserService service;
	
	public UserServiceProxyHandler(UserService service) {
		this.service = service;
	}
	
	public void before() {
		System.out.println("========before=========");
	}
	
	public void after() {
		System.out.println("========after=========");
	}
	
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		
		before();
		Object result = method.invoke(service, args);
		after();
		
		return result;
	}

}

invoke 메서드를 통해 프록시 동작을 정의한다. 실제로는 타겟 메서드, 포인트 컷 등 어떤 시점에 적용할지 어떤 메서드에 적용할지가 있는데 Method method 매개변수의 Reflection을 활용해 원하는 aop로직을 구현할 수 있다.

 

 

 

UserController

@Controller
public class UserController {

	private UserService userService;
	
	public UserController() {
		//의존성 주입이라고 가정
		this.userService = (UserService) Proxy.newProxyInstance(
				UserService.class.getClassLoader(), 
				new Class[] {UserService.class}, 
				new UserServiceProxyHandler(new UserServiceImpl()));
	}
	
	@RequestMapping("/user")
	public String getUserName(HttpServletRequest request, HttpServletResponse response) {
		
		// ...
		// Do Somethings
		String user_name = userService.getUserName();
		// ...
		
		return "";
	}
}

 

컨트롤러에서 사용할 Service 의존성 주입은 실제로 빈컨테이너가 해주겠지만 임의로 넣어주었다.

 

 

실행 결과

 

잘된다.

 

 

느낀점

AOP 동작원리에 대해서 찾아보며 알게된 건, 위에서 참고한 사이트를 보면 알겠지만 Reflection은 aop를 적용할 메서드가 아니라고 해도 인터페이스에 있는 메서드는 일단 전부 프록시 객체 생성에 불러오기 때문에,

상대적으로 CGLib보다 성능이 낮다고 한다. 먼저나온 이유가 있다. 

 

그리고 직접 구현을 해보면서 느낀건 @Aspect 적용한 클래스 하나 만들어서 어노테이션들로 aop를 쉽게 구현이 된다는게 역시 머리 좋은 사람 여러명 모여서 만들고, 많이 쓰는 이유가 있구나 싶었다.

 

다음엔 CGLib에 대해 정리할 예정이다.

 

728x90