리플렉션
Java가 제공하는 JDK 동적 프록시, CGLIB같은 프록시 생성 오픈소스를 사용하면 프록시 객체를 동적으로 만들어낼 수 있다.
하나의 대상 클래스를 위해 프록시 클래스를 계속 만들지 않아도된다!
먼저 Java의 리플렉션에 대해 알아야하는데,
리플렉션을 사용하면 클래스나 메서드의 메타 정보를 동적으로 획득하고 코드도 동적으로 호출할 수 있다.
위와 같이 callA, callB 메서드를 가지고 있는 정적 클래스가 있다.
이 메서드를 호출하고 싶을 때 우리는 대부분 아래와 같이 코드를 짠다.
객체 생성 -> 호출
이 방식은 소스 코드에 명시적으로 작성되어있기 때문에 정적이라고 말할 수 있다.
이 자바 코드는 클래스 단위로 컴파일되어 바이트 코드가 되는데 이 바이크 코드를 조작하는 방법이 바로 리플렉션이다.
- Class.forName : 클래스의 메타 정보를 획득, 내부 클래스는 $를 붙힘
- .getMethod(메서드명) : 해당 클래스의 메서드 메타 정보를 획득
- .invoke(객체 인스턴스) : 메서드 메타 정보로 실제 인스턴스의 메서드를 호출
리플렉션을 사용하게되면 호출 로직을 Method를 사용해 처리하게되므로 공통으로 묶을 수 있다.
또한 메서드의 메타 정보를 얻기위해 넘기는 파라미터는 String이기 때문에 동적으로 변경할 수 있다!!
다른 메소드를 호출하기 때문에 공통 메서드로 묶을 수 없던 것을 리플렉션을 사용하면 깔끔하게 묶을 수 있다.
더 나아가 메서드 명을 넘기는 부분도 공통으로 묶어 처리할 수 있다.
존재하지 않는 클래스이거나 메서드라면 예외가 발생한다.
주의해야할 점은 리플렉션은 앞서 말했듯이 런타임 시점에 동작하므로 컴파일 오류를 잡아낼 수 없다.
일반적으로 사용하지말자! 정말정말 필요할 때 고민해서 적용시켜야한다.
가장 좋은 에러는 컴파일 에러다!
리플렉션은 타입 정보를 기반으로 컴파일 시점에 오류를 잡는 방식을 역행한다.
JDK 동적 프록시
Java가 제공하는 JDK 동적 프록시에 적용할 로직은 InvocationHandler를 구현하여 작성하면 된다.
메서드 명으로 짐작할 수 있듯이 invoke가 target의 메서드를 호출해주는 역할을 한다.
어떤 target이든 주입받을 수 있게 하기위해 Object 타입의 target을 정의하였다.
invoke내에 프록시에서 추가할 기능을 부여하고(안해도 ㄱㅊ) 파라미터로 넘어오는 method를 invoke를 통해 호출해준다.
이는 주입받는 실제 인스턴스(target)의 메서드를 호출하는 것이다.
이제 프록시를 사용해보자.
프록시에 주입하게될 클래스를 정의하였다.
실제 인스턴스를 생성하고 InvocationHandler를 구현한 클래스에 주입해준다.
그다음 Proxy.newProxyInstance를 통해 프록시 객체를 생성한다.
- loader : 프록시 클래스를 정의하는 클래스 로더
- interfaces : 프록시 클래스가 구현하는 인터페이스들(인터페이스는 다중 상속이 가능하므로 배열 형식)
- h : 메서드 호출을 처리하는 핸들러
사진에서 볼 수 있듯이 반환타입이 Object이기 때문에 target의 타입으로 캐스팅해주었다.
캐스팅하였기 때문에 AInterface에 존재하는 call 메서드를 호출할 수 있는데 결과를 먼저 살펴보자.
🤔🤔call()을 호출했는데 왜 handler의 invoke()가 실행되었지?
클라이언트가 JDK 동적 프록시의 함수를 호출하면(여기선 call()) 파라미터로 넘어온 handler의 invoke()를 호출한다.
위에서는 TimeInvocationHandler.invoke()가 호출되는 것이다.
여기서 내부 로직을 수행하는데 이 때 method.invoke(target, args)가 있는데 파라미터로 넘어온 method가 바로 클라이언트가 호출한 메서드이다.(call())
따라서 여기서 target의 call()이 실행되고 다시 handler로 응답이 돌아오는 것이다.
추가로 새로운 인터페이스를 정의하든 기존 인터페이스를 새로 구현하는 클래스를 만들든 TimeInvocationHandler를 구현한 클래스는 전혀 변경없이 그대로 사용할 수 있다.
이것이 바로 JDK 동적 프록시!!!!
JDK 동적 프록시를 사용하지 않았을 때는 각 클래스를 위한 프록시 클래스를 각각 만들어야했지만
JDK 동적 프록시를 통해 InvocationHandler를 구현하여 만들기만하면 각 클래스를 위한 동적 프록시를 생성해준다.
- method.getDeclaringClass(), method.getName() 등을 이용해 호출된 클래스와 메서드 메타 정보를 동적으로 얻을 수 있다.
JDK 동적 프록시는 인터페이스가 필수이다.
인터페이스없이 클래스만 있는 경우에는 동적 프록시를 적용 못하는거야?😰
CGLIB
CGLIB(Code Generator Library)는 바이트코드를 조작해 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
이를 사용하면 인터페이스 없이 구현체를 가지고 동적 프록시를 만들어 낼 수 있다.
JDK 동적 프록시에서의 InvocationHandler 대신 CGLIB은 MethodInterceptor을 제공한다.
- var1 : CGLIB이 적용된 객체
- var2 : 호출된 메서드
- var3 : 메서드를 호출할 때 넘어온 파라미터
- var4 : 메서드 호출에 사용
내부 구조는 JDK 동적 프록시와 비슷하다.
JDK 동적 프록시와 마찬가지로 method.invoke(target)을 통해 호출할 수도 있지만 CGLIB 공식 문서에서는 methodProxy.invoke(target)을 권장하고 있다.(내부 최적화를 통해서 더 빠르다)
CGLIB을 사용해 동적 프록시를 만드는 법은 Enhancer 클래스를 사용한다.
JavaDoc의 내용을 가져온 것인데 쉽게 이야기하자면 슈퍼 메서드 호출하는 전후에 커스텀한 부가 기능을 추가할 수 있다는 것이다.
즉, setSuperClass를 통해 상속받아 만들 슈퍼 클래스를 지정해주고
setCallback을 통해 MethodInterceptor을 구현한 커스텀 기능을 추가해준다.
최종적으로 create()를 통해 동적 프록시가 생성된다.
반환타입은 Object이지만 setSuperClass로 지정한 슈퍼 클래스를 상속받아 만들기 때문에 해당 타입으로 캐스팅또한 당연히 가능하다!!
클래스 명은 같으나 CGLIB이 만들어준 동적 프록시라는 것을 확인할 수 있다.
CGLIB에는 몇가지 제약이 존재한다.(상속을 사용하기 때문에)
- 부모 클래스의 생성자를 체크해야함(기본 생성자가 필요)
- 부모 클래스, 메서드에 final이 붙어 있으면 상속 및 override가 불가능하다.
- 클래스에 final 붙으면 예외
- 메서드에 final 붙으면 프록시 로직이 동작 X
ProxyFactory를 통해 CGLIB을 적용하면 단점을 해결하고 더 편리하게 사용이 가능하다!
'Spring > Spring Core Advanced' 카테고리의 다른 글
빈 후처리기 (3) | 2022.01.15 |
---|---|
Spring의 프록시 기술 (0) | 2021.12.16 |
프록시 패턴과 데코레이터 패턴 (0) | 2021.11.08 |
템플릿 메서드 패턴과 콜백 패턴 (0) | 2021.11.01 |
ThreadLocal (0) | 2021.10.29 |