You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
지금까지 프록시 기술을 사용해 기존 코드 변경 없이 로그 추적기라는 부가 기능을 적용할 수 있었음.
문제는 프록시 적용 필요한 클래스만큼 프록시 클래스를 만들어야 했다,
로그 추적을 위한 프록시 클래스들의 소스코드는 거의 같은 모양을 하고 있다.
자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다. 쉽게 이야기해서 프록시 클래스를 지금처럼 계속 만들지 않아도 된다는 것이다.
프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다. 자세한 내용은 조금 뒤에 코드로 확인해보자.
JDK 동적 프록시를 이해하려면 먼저 자바의 리플랙션 기술을 알아한다.
리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
여기서는 JDK 동적 프록시를 이해하기 위한 최소한의 리플렉션 기술을 알아보자.
Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello") : 클래스 메타정보를 획득
한다. 참고로 내부 클래스는 구분을 위해 $ 를 사용한다.
classHello.getMethod("call") : 해당 클래스의 call 메서드 메타정보를 획득한다.
methodCallA.invoke(target) : 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다. 여기서 methodCallA 는 Hello 클래스의 callA() 이라는 메서드 메타정보이다. methodCallA.invoke(인스턴스) 를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의 callA() 메서드를 찾아서 실행한다. 여기서는 target 의 callA() 메서드를 호출한다.
메서드를 직접 호출하면 되지 이게 어떤 의미가 있나?
여기서 중요한 점은 메서드를 직접 호출하는 부분이 Method 클래스로 대체되었다. (추상화 되었다) 덕분에 이제 공통 로직을 만들 수 있게 되었다.
dynamicCall(Method method, Object target)
- 공통 로직1, 공통 로직2를 한번에 처리할 수 있는 통합된 공통 처리 로직이다.
- Method method : 첫 번째 파라미터는 호출할 메서드 정보가 넘어온다. 이것이 핵심이다. 기존에는 메서드 이름을 직접 호출했지만, 이제는 Method 라는 메타정보를 통해서 호출할 메서드 정보가 동적으로 제공된다.
- Object target: 실제 실행할 인스턴스 정보가 넘어온다. 타입이Object라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다. 물론method.invoke(target)` 를 사용할 때 호출할 클래스와 메서드 정보가
서로 다르면 예외가 발생한다.
정리
정적인 target.callA() , target.callB() 코드를 리플렉션을 사용해서 Method 라는 메타정보로 추상화했다. 덕분에 공통 로직을 만들 수 있게 되었다.
주의
리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
예를 들어서 지금까지 살펴본 코드에서 getMethod("callA") 안에 들어가는 문자를 실수로 getMethod("callZ") 로 작성해도 컴파일 오류가 발생하지 않는다. 그러나 해당 코드를 직접 실행하는 시점에 발생 하는 오류인 런타임 오류가 발생한다.
가장 좋은 오류는 개발자가 즉시 확인할 수 있는 컴파일 오류이고, 가장 무서운 오류는 사용자가 직접 실행할 때 발생하는 런타임 오류다.
따라서 리플렉션은 일반적으로 사용하면 안된다. 지금까지 프로그래밍 언어가 발달하면서 타입 정보를 기반으로 컴파일 시점에 오류를 잡아준 덕분에 개발자가 편하게 살았는데, 리플렉션은 그것에 역행하는 방식이다.
리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
JDK 동적 프록시 - 소개
지금까진 프록시 적용을 위해서 대상 클래스를 다 만들었다.
그런데 프록시 로직 기본 코드와 흐름은 거의 같고 프록시를 적용하는 대상 정도만 차이가 있었다.
쉽게말해 프록시 로직은 같은데 적용 대상만 차이가 있는 것.
이 문제를 해결하는 것이 바로 동적 프록시 기술.
동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다. 이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다. 그리고 동적 프록시에 원하는 실행 로직을 지정할 수 있다.
코드로 확인해보자.
주의
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.
이후에 오픈소스인 CGLIB 도 알아볼 예정.
최종 요약
인터페이스를 구현한 프록시 객체를 만들면, JVM이 자동으로 인터페이스 메서드를 구현한 프록시 클래스를 동적으로 생성한다.
해당 프록시 객체의 메서드를 호출하면, 내부적으로 InvocationHandler의 invoke()가 호출되며, (프록시 생성시 파라미터로 handler를 넣어줬기 때문에 가능)
이때 JVM은 호출된 메서드에 대한 Method 객체와 인자들을 전달해주고,
핸들러는 이를 사용해 실제 구현 객체(target)의 메서드를 리플렉션으로 실행한다.
정리
예제를 보면 AImpl , BImpl 각각 프록시를 만들지 않았다. 프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고 TimeInvocationHandler 는 공통으로 사용했다.
JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다. 그리고 같은 부가 기능 로직을 한번만 개발해서 공통으로 적용할 수 있다. 만약 적용 대상이 100개여도 동적 프록시를 통해서 생성하고, 각각 필요한 InvocationHandler 만 만들어서 넣어주면 된다.
결과적으로 프록시 클래스를 수 없이 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 되었다.
JDK 동적 프록시 없이 직접 프록시를 만들어서 사용할 때와 JDK 동적 프록시를 사용할 때의 차이를 그림으로 비교해
보자.
LogTrace 에 사용할 메시지이다. 프록시를 직접 개발할 때는 "OrderController.request()" 와 같이 프록시마다 호출되는 클래스와 메서드 이름을 직접 남겼다. 이제는 Method 를 통해서 호출되는 메서
드 정보와 클래스 정보를 동적으로 확인할 수 있기 때문에 이 정보를 사용하면 된다.
JDK 동적 프록시 - 한계
JDK 동적 프록시는 인터페이스가 필수이다.
그렇다면 V2 애플리케이션 처럼 인터페이스 없이 클래스만 있는 경우에는 어떻게 동적 프록시를 적용할 수 있을까?
이것은 일반적인 방법으로는 어렵고 CGLIB 라는 바이트코드를 조작하는 특별한 라이브러리를 사용해야 한다.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
리플렉션
지금까지 프록시 기술을 사용해 기존 코드 변경 없이 로그 추적기라는 부가 기능을 적용할 수 있었음.
문제는 프록시 적용 필요한 클래스만큼 프록시 클래스를 만들어야 했다,
로그 추적을 위한 프록시 클래스들의 소스코드는 거의 같은 모양을 하고 있다.
자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다. 쉽게 이야기해서 프록시 클래스를 지금처럼 계속 만들지 않아도 된다는 것이다.
프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다. 자세한 내용은 조금 뒤에 코드로 확인해보자.
JDK 동적 프록시를 이해하려면 먼저 자바의 리플랙션 기술을 알아한다.
리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
여기서는 JDK 동적 프록시를 이해하기 위한 최소한의 리플렉션 기술을 알아보자.
공통 로직1과 공통 로직2는 호출하는 메서드만 다르고 전체 코드 흐름이 완전히 같다.
- 먼저 start 로그를 출력한다.
- 어떤 메서드를 호출한다.
- 메서드의 호출 결과를 로그로 출력한다.
target.callA(),target.callB()이 부분만 동적으로 처리할 수 있다면 문제를 해결할 수 있을 듯 하다.
이럴 때 사용하는 기술이 바로 리플렉션이다. 리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다. 바로 리플렉션 사용해보자.
호출하는 메서드 callA(), callB() 는 실시간으로 바꿀 수 없음. (컴파일 후 자바 실행시 바꿀 수 없다는 의미)
근데 이걸 가능하게 하는게 리플렉션 이라는 기능
참고: 람다를 사용해서 공통화 하는 것도 가능하다. 여기서는 람다를 사용하기 어려운 상황이라 가정하자. 그리고 리플렉션 학습이 목적이니 리플렉션에 집중하자.
Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello"): 클래스 메타정보를 획득한다. 참고로 내부 클래스는 구분을 위해
$를 사용한다.classHello.getMethod("call"): 해당 클래스의call메서드 메타정보를 획득한다.methodCallA.invoke(target): 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다. 여기서methodCallA는Hello클래스의callA()이라는 메서드 메타정보이다.methodCallA.invoke(인스턴스)를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의callA()메서드를 찾아서 실행한다. 여기서는target의callA()메서드를 호출한다.메서드를 직접 호출하면 되지 이게 어떤 의미가 있나?
여기서 중요한 점은 메서드를 직접 호출하는 부분이
Method클래스로 대체되었다. (추상화 되었다) 덕분에 이제 공통 로직을 만들 수 있게 되었다.dynamicCall(Method method, Object target)- 공통 로직1, 공통 로직2를 한번에 처리할 수 있는 통합된 공통 처리 로직이다.
-
Method method: 첫 번째 파라미터는 호출할 메서드 정보가 넘어온다. 이것이 핵심이다. 기존에는 메서드 이름을 직접 호출했지만, 이제는Method라는 메타정보를 통해서 호출할 메서드 정보가 동적으로 제공된다.- Object target
: 실제 실행할 인스턴스 정보가 넘어온다. 타입이Object라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다. 물론method.invoke(target)` 를 사용할 때 호출할 클래스와 메서드 정보가서로 다르면 예외가 발생한다.
정리
정적인
target.callA(),target.callB()코드를 리플렉션을 사용해서Method라는 메타정보로 추상화했다. 덕분에 공통 로직을 만들 수 있게 되었다.주의
리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
예를 들어서 지금까지 살펴본 코드에서
getMethod("callA")안에 들어가는 문자를 실수로getMethod("callZ")로 작성해도 컴파일 오류가 발생하지 않는다. 그러나 해당 코드를 직접 실행하는 시점에 발생 하는 오류인 런타임 오류가 발생한다.가장 좋은 오류는 개발자가 즉시 확인할 수 있는 컴파일 오류이고, 가장 무서운 오류는 사용자가 직접 실행할 때 발생하는 런타임 오류다.
따라서 리플렉션은 일반적으로 사용하면 안된다. 지금까지 프로그래밍 언어가 발달하면서 타입 정보를 기반으로 컴파일 시점에 오류를 잡아준 덕분에 개발자가 편하게 살았는데, 리플렉션은 그것에 역행하는 방식이다.
리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
JDK 동적 프록시 - 소개
지금까진 프록시 적용을 위해서 대상 클래스를 다 만들었다.
그런데 프록시 로직 기본 코드와 흐름은 거의 같고 프록시를 적용하는 대상 정도만 차이가 있었다.
쉽게말해 프록시 로직은 같은데 적용 대상만 차이가 있는 것.
이 문제를 해결하는 것이 바로 동적 프록시 기술.
동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다. 이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다. 그리고 동적 프록시에 원하는 실행 로직을 지정할 수 있다.
코드로 확인해보자.
주의
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.
이후에 오픈소스인 CGLIB 도 알아볼 예정.
이렇게 A 와 B 가 있음.
동적 프록시로 A와 B 를 한번에 적용시켜보자.
JDK 동적 프록시 - 예제 코드
JDK 동적 프록시에 적용할 로직은
InvocationHandler인터페이스를 구현해서 작성하면 된다.제공되는 파라미터는 다음과 같다.
Object proxy: 프록시 자신Method method: 호출한 메서드Object[] args: 메서드를 호출할 때 전달한 인수프록시는 항상 프록시가 호출할 대상이 있어야함.
target필요.은InvocationHandler` 인터페이스를 구현한다. 이렇게해서 JDK 동적 프록시에 적용할 공통 로직을 개발할 수 있다.Object target: 동적 프록시가 호출할 대상method.invoke(target, args): 리플렉션을 사용해서target인스턴스의 메서드를 실행한다.args는 메서드 호출시 넘겨줄 인수이다.이를 적용해보자.
new TimeInvocationHandler(target): 동적 프록시에 적용할 핸들러 로직이다.Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[] {AInterface.class}, handler)java.lang.reflect.Proxy를 통해서 생성할 수 있다.실행 결과

AInterface proxy가 어떤 로직을 실행하나?handler에 있는 로직을 수행함.즉 우리가 구현한
TimeInvocationHandler의invoke()가 실행된다.근데 우리가 지금 proxy.call() 을 호출하고 있으므로, invoke 의 method 에 call 이 넘어간다.
B 도 동일하게 가능
프록시 클래스 1개만 만들면 2개 이상 로직(클래스)에 적용 가능!
실행 순서를 세밀하게 봐보자.
InvocationHandler.invoke()를 호출한다.TimeInvocationHandler가 구현체로 있으로TimeInvocationHandler.invoke()가 호출된다.TimeInvocationHandler가 내부 로직을 수행하고,method.invoke(target, args)를 호출해서target인 실제 객체(AImpl)를 호출한다.AImpl인스턴스의call()이 실행된다.AImpl인스턴스의call()의 실행이 끝나면TimeInvocationHandler로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.proxy.call()을 호출했는데 → 어떻게 JVM이 InvocationHandler.invoke()를 호출하고
그 안에서 method.invoke(target, args)로 진짜 메서드가 호출되는지?
프록시를 만들면 JVM이 런타임에 클래스 코드를 직접 만들어 메모리에 올림.
즉 자동으로 Method 를 만들어서 invoke 메서드의 파라미터에 넣어줬기 때문에 진짜 메서드가 호출됨.
메서드에 파라미터가 있는경우엔?
프록시 내부 코드
최종 요약
인터페이스를 구현한 프록시 객체를 만들면, JVM이 자동으로 인터페이스 메서드를 구현한 프록시 클래스를 동적으로 생성한다.
해당 프록시 객체의 메서드를 호출하면, 내부적으로 InvocationHandler의 invoke()가 호출되며, (프록시 생성시 파라미터로 handler를 넣어줬기 때문에 가능)
이때 JVM은 호출된 메서드에 대한 Method 객체와 인자들을 전달해주고,
핸들러는 이를 사용해 실제 구현 객체(target)의 메서드를 리플렉션으로 실행한다.
정리
예제를 보면
AImpl,BImpl각각 프록시를 만들지 않았다. 프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고TimeInvocationHandler는 공통으로 사용했다.JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다. 그리고 같은 부가 기능 로직을 한번만 개발해서 공통으로 적용할 수 있다. 만약 적용 대상이 100개여도 동적 프록시를 통해서 생성하고, 각각 필요한
InvocationHandler만 만들어서 넣어주면 된다.결과적으로 프록시 클래스를 수 없이 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬 수 있게 되었다.
JDK 동적 프록시 없이 직접 프록시를 만들어서 사용할 때와 JDK 동적 프록시를 사용할 때의 차이를 그림으로 비교해
보자.
JDK 동적 프록시 도입 후

지금까지 학습한 JDK 동적 프록시를 애플리케이션에 적용해보자.
JDK 동적 프록시 - 적용1
JDK 동적 프록시는 인터페이스가 필수이기 때문에 V1 애플리케이션에만 적용할 수 있다.
먼저
LogTrace를 적용할 수 있는InvocationHandler를 만들자.LogTraceBasicHandler는InvocationHandler인터페이스를 구현해서 JDK 동적 프록시에서 사용된다.private final Object target: 프록시가 호출할 대상이다.String message = method.getDeclaringClass().getSimpleName() + "." ...LogTrace에 사용할 메시지이다. 프록시를 직접 개발할 때는"OrderController.request()"와 같이 프록시마다 호출되는 클래스와 메서드 이름을 직접 남겼다. 이제는Method를 통해서 호출되는 메서드 정보와 클래스 정보를 동적으로 확인할 수 있기 때문에 이 정보를 사용하면 된다.
동적 프록시를 사용하도록 수동 빈 등록을 설정하자.
Controller,Service,Repository에 맞는 동적 프록시를 생성해주면 된다.LogTraceBasicHandler: 동적 프록시를 만들더라도LogTrace를 출력하는 로직은 모두 같기 때문에 프록시는 모두LogTraceBasicHandler를 사용한다.남은 문제
LogTraceBasicHandler가 실행되기 때문에 로그가 남는다. 이 부분을 로그가 남지 않도록 처리해야 한다.JDK 동적 프록시 - 적용2
http://localhost:8080/v1/no-log
요구사항에 의해 이것을 호출 했을 때는 로그가 남으면 안된다.
이런 문제를 해결하기 위해 메서드 이름을 기준으로 특정 조건을 만족할 때만 로그를 남기는 기능을 개발해보자.
LogTraceFilterHandler는 기존 기능에 다음 기능이 추가되었다.LogTrace로직을 실행한다. 이름이 매칭되지 않으면 실제 로직을 바로 호출한다.프링이 제공하는
PatternMatchUtils.simpleMatch(..)를 사용하면 단순한 매칭 로직을 쉽게 적용할수 있다.
xxx: xxx가 정확히 매칭되면 참xxx*: xxx로 시작하면 참*xxx: xxx로 끝나면 참*xxx*: xxx가 있으면 참http://localhost:8080/v1/no-log 로 요청이 오면 컨트롤러 메서드가
이기 때문에 패턴에서 맞지 않아 로그가 남지 않음.
JDK 동적 프록시 - 한계
JDK 동적 프록시는 인터페이스가 필수이다.
그렇다면 V2 애플리케이션 처럼 인터페이스 없이 클래스만 있는 경우에는 어떻게 동적 프록시를 적용할 수 있을까?
이것은 일반적인 방법으로는 어렵고
CGLIB라는 바이트코드를 조작하는 특별한 라이브러리를 사용해야 한다.Beta Was this translation helpful? Give feedback.
All reactions