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
super(null) : OrderServiceV2 :자바 기본 문법에 의해 자식 클래스를 생성할때는 항상 super()로 부모클래스의 생성자를 호출해야한다. 이 부분을 생략하면 기본 생성자가 호출된다. 그런데 부모 클래스인 OrderServiceV2 는 기본 생성자가 없고 생성자에서 파라미터 1개를 필수로 받는다. 따라서 파라미터를 넣어서 super()를 호출해야 한다.
프록시를 사용한 덕분에 원본 코드를 전혀 변경하지 않고, V1, V2 애플리케이션에 LogTrace 기능을 적용할 수 있었다.
인터페이스 기반 프록시 vs 클래스 기반 프록시
인터페이스가 없어도 클래스 기반으로 프록시를 생성할 수 있다.
클래스 기반 프록시는 해당 클래스에만 적용할 수 있다. 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
부모 클래스의 생성자를 호출해야 한다.( 앞서 본 예제 )
클래스에 final 키워드가 붙으면 상속이 불가능하다.
메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
이렇게 보면 인터페이스 기반의 프록시가 더 좋아보인다. 맞다. 인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭다. 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다. 인터페이스 기반 프록시의 단점은 인터페이스가 필요하다는 그 자체이다. 인터페이스가 없으면 인터페이스 기반 프록시를 만들 수 없다.
참고
인터페이스 기반 프록시는 캐스팅 관련해서 단점이 있는데 이 내용은 뒷부분에서 설명한다.
이론적으로는 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다. 이렇게 하면 역할과 구현을 나누어서 구현체를 매우 편리하게 변경할 수 있다. 하지만 실제로는 구현을 거의 변경할 일 이 없는 클래스도 많다. 인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적인데 구현을 변경할 가능성이 거의 없는 코드에 무작정 인터페이스를 사용하는 것은 번거롭고 그렇게 실용적이지 않다. 이런 곳에는 실용적인 관점에서 인터페이스를 사용하지 않고 구체클래스를 바로 사용하는 것이 좋다 생각한다. (물론 인터페이스를 도입하는 다양한 이유가 있다. 여기서 핵심은 인터페이스가 항상 필요하지 않다는 것이다.)
결론
실무에서는 프록시를 적용할 때 v1처럼 인터페이스도 있고 v2처럼 구체 클래스도 있다. 따라서 2가지 상황을 모두 대응할 수 있어야 한다.
너무 많은 프록시 클래스
지금까지 프록시를 사용해서 기존 코드를 변경하지 않고 로그추적기라는 부가 기능을 적용할 수 있었다.
그런데 문제는 프록시 클래스를 너무 많이 만들어야 한다는 점이다. 잘 보면 프록시 클래스가 하는 일은 LogTrace 를 사용하는 것인데 그 로직이 모두 똑같다. 대상 클래스만 다를 뿐이다. 만약 적용해야 하는 대상 클래스가 100개라면 프록시 클래스도 100개 만들어야 한다.
프록시 클래스를 하나만 만들어서 모든 곳에 적용하는 방법은 없을까?
바로 다음에 설명할 동적 프록시 기술이 이 문제를 해결해준다.
05. 동적 프록시 기술 - 리플렉션
지금까지 프록시를 사용해서 기존 코드를 변경하지 않고 로그추적기라는 부가 기능을 적용할 수 있었다. 그런데 문제는 대상 클래스 수 만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 점이다.
로그 추적을 위한 프록시 클래스들의 소스코드는 거의 같은 모양을 하고 있다.
자바가 기본으로 제공하는 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() 메서드를 호출한다.
그런데 target.callA()나 target.callB() 메서드를 직접 호출하면 되지 이렇게 메서드 정보를 획득해서 메서들르 호출하면 어떤 효과가 있을까? 여기서 중요한 핵심은 클래스나 메서드 정보를 동적으로 변경할 수 있다는 점이다.
기존의 callA() , callB() 메서드를 직접 호출하는 부분이 Method 로 대체되었다. (추상화) 덕분에 이제 공통 로직을 만들 수 있게 되었다.
Method method : 첫번째 파라미터는 호출할 메서드 정보가 넘어온다. 이것이 핵심이다. 기존에는 메서드 이름을 직접 호출했지만 이제는 Method 라는 메타정보를 통해서 호출할 메서드 정보가 동적으로 제공된다.
Object target : 실제 실행할 인스턴스 정보가 넘어온다. 타입이 Object 라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다. 물론 method.invoke(target) 를 사용할 때 호출할 클래스와 메서드 정보가 서로 다르면 예외가 발생한다.
정리
정적인 target.callA(), target.callB() 코드를 리플렉션을 사용해서 Method 라는 메타정보로 추상화햇다. 덕분에 공통 로직을 만들 수 있게 되었다.
주의
리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만 리플렉션 기술은 런타임에 동작하기 대문에 컴파일 시점에 오류를 잡을 수 없다.
예를 들어서 지금까지 살펴본 코드에서 getMethod("callA") 안에 들어가는 문자를 실수로 getMethod("call2") 로 작성해도 컴파일 오류가 발생하지 않는다. 대신 해당 코드를 직접 실행하는 시점에 발생하는 오류인 런타임에 오류가 발생한다. 사용자가 실행하는 시점에 발생하는 런타임 오류가 나는 것은 안좋다.
따라서 리플렉션은 일반적으로 사용하면 안된다. 지금까지 프로그래밍 언어가 발달하면서 타입 정보를 기반으로 컴파일 시점에 오류를 잡아준 덕분에 개발자가 편하게 살았는데 리플렉션은 그것에 역행하는 방식이다.
리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
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.
구체 클래스 기반 프록시 - 적용
이번에는 앞서 학습한 내용을 기반으로 구체 클래스만 있는 V2 애플리케이션에 프록시 기능을 적용해보자.
OrderRepositoryConcreteProxy
OrderServiceConcreteProxy
클래스 기반 프록시의 단점
OrderServiceV2:자바 기본 문법에 의해 자식 클래스를 생성할때는 항상super()로 부모클래스의 생성자를 호출해야한다. 이 부분을 생략하면 기본 생성자가 호출된다. 그런데 부모 클래스인OrderServiceV2는 기본 생성자가 없고 생성자에서 파라미터 1개를 필수로 받는다. 따라서 파라미터를 넣어서super()를 호출해야 한다.super(null)을 입력해도 된다.OrderServiceV2의 생성자 - 참고
OrderControllerConcreteProxy
ConcreteProxyConfig
ProxyApplication
추가한 부분
실행
localhost:8080/v2/request?itemId=hello이것과 결과가 비슷하다.
runtime 에서 클라이언트가 프록시를 호출하고 구현체를 호출하는것

인터페이스가 아닌 구현체 기반으로 프록시가 생성한다.
인터페이스 기반 프록시와 클래스 기반 프록시
프록시
프록시를 사용한 덕분에 원본 코드를 전혀 변경하지 않고, V1, V2 애플리케이션에 LogTrace 기능을 적용할 수 있었다.
인터페이스 기반 프록시 vs 클래스 기반 프록시
이렇게 보면 인터페이스 기반의 프록시가 더 좋아보인다. 맞다. 인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭다. 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다. 인터페이스 기반 프록시의 단점은 인터페이스가 필요하다는 그 자체이다. 인터페이스가 없으면 인터페이스 기반 프록시를 만들 수 없다.
참고
인터페이스 기반 프록시는 캐스팅 관련해서 단점이 있는데 이 내용은 뒷부분에서 설명한다.
이론적으로는 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다. 이렇게 하면 역할과 구현을 나누어서 구현체를 매우 편리하게 변경할 수 있다. 하지만 실제로는 구현을 거의 변경할 일 이 없는 클래스도 많다. 인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적인데 구현을 변경할 가능성이 거의 없는 코드에 무작정 인터페이스를 사용하는 것은 번거롭고 그렇게 실용적이지 않다. 이런 곳에는 실용적인 관점에서 인터페이스를 사용하지 않고 구체클래스를 바로 사용하는 것이 좋다 생각한다. (물론 인터페이스를 도입하는 다양한 이유가 있다. 여기서 핵심은 인터페이스가 항상 필요하지 않다는 것이다.)
결론
너무 많은 프록시 클래스
지금까지 프록시를 사용해서 기존 코드를 변경하지 않고 로그추적기라는 부가 기능을 적용할 수 있었다.
그런데 문제는 프록시 클래스를 너무 많이 만들어야 한다는 점이다. 잘 보면 프록시 클래스가 하는 일은
LogTrace를 사용하는 것인데 그 로직이 모두 똑같다. 대상 클래스만 다를 뿐이다. 만약 적용해야 하는 대상 클래스가 100개라면 프록시 클래스도 100개 만들어야 한다.프록시 클래스를 하나만 만들어서 모든 곳에 적용하는 방법은 없을까?
바로 다음에 설명할 동적 프록시 기술이 이 문제를 해결해준다.
05. 동적 프록시 기술 - 리플렉션
지금까지 프록시를 사용해서 기존 코드를 변경하지 않고 로그추적기라는 부가 기능을 적용할 수 있었다. 그런데 문제는 대상 클래스 수 만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 점이다.
로그 추적을 위한 프록시 클래스들의 소스코드는 거의 같은 모양을 하고 있다.
자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다. 쉽게 이야기해서 프록시 클래스를 지금처럼 계속 만들지 않아도 된다는 것이다. 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다.
JDK 동적 프록시를 이해하기 위해서는 먼저 자바의 리플렉션 기술을 이해해야 한다. 리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고 코드도 동적으로 호출할 수 있다. 여기서는 JDK 동적 프록시를 이해하기 위한 최소한의 리플렉션 기술을 알아보자.
Test > jdkdynamic > ReflectionTest
target.callA(),target.callB이 부분만 동적으로 처리할 수 있다면 문제를 해결할 수 있을 듯 하다.이럴 때 사용하는 기술이 바로 리플렉션이다. 리플렉션은 클래스나 메서드의 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다. 바로 리플렉션을 사용해보자.
참고
람다를 사용해서 공통화 하는 것도 가능하다. 여기서는 람다를 사용하기 어려운 상황이라 가정하자. 그리고 리플렉션 학습이 목적이니 리플렉션 학습에 집중하겠다.
Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello"): 클래스 메타 정보를 획득한다. 참고로 내부 클래스는 구분을 위해$를 사용한다.classHello.getMethod("call"): 해당 클래스의call메서드 메타정보를 획득한다.methodCallA.invoke(target): 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다. 여기서methodCallA는Hello클래스의callA()라는 메서드 메타 정보이다.methodCallA.invoke(인스턴스)를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의callA()메서드를 찾아서 실행한다. 여기서는target의callA()메서드를 호출한다.그런데
target.callA()나target.callB()메서드를 직접 호출하면 되지 이렇게 메서드 정보를 획득해서 메서들르 호출하면 어떤 효과가 있을까? 여기서 중요한 핵심은 클래스나 메서드 정보를 동적으로 변경할 수 있다는 점이다.기존의
callA(),callB()메서드를 직접 호출하는 부분이Method로 대체되었다. (추상화) 덕분에 이제 공통 로직을 만들 수 있게 되었다.reflection2 추가
dynamicCall(Method method, Object target)Method method: 첫번째 파라미터는 호출할 메서드 정보가 넘어온다. 이것이 핵심이다. 기존에는 메서드 이름을 직접 호출했지만 이제는Method라는 메타정보를 통해서 호출할 메서드 정보가 동적으로 제공된다.Object target: 실제 실행할 인스턴스 정보가 넘어온다. 타입이Object라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다. 물론method.invoke(target)를 사용할 때 호출할 클래스와 메서드 정보가 서로 다르면 예외가 발생한다.정리
정적인
target.callA(),target.callB()코드를 리플렉션을 사용해서Method라는 메타정보로 추상화햇다. 덕분에 공통 로직을 만들 수 있게 되었다.주의
리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만 리플렉션 기술은 런타임에 동작하기 대문에 컴파일 시점에 오류를 잡을 수 없다.
예를 들어서 지금까지 살펴본 코드에서
getMethod("callA")안에 들어가는 문자를 실수로getMethod("call2")로 작성해도 컴파일 오류가 발생하지 않는다. 대신 해당 코드를 직접 실행하는 시점에 발생하는 오류인 런타임에 오류가 발생한다. 사용자가 실행하는 시점에 발생하는 런타임 오류가 나는 것은 안좋다.따라서 리플렉션은 일반적으로 사용하면 안된다. 지금까지 프로그래밍 언어가 발달하면서 타입 정보를 기반으로 컴파일 시점에 오류를 잡아준 덕분에 개발자가 편하게 살았는데 리플렉션은 그것에 역행하는 방식이다.
리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
All reactions