언어별 - 프록시 디자인 패턴

2023. 4. 16. 19:07프로그래밍

728x90
 

Various Proxy Design Pattern implementation variants in Java, ABAP and JavaScript | SAP Blogs

0 1 3,461 This blog gives an introduction about various proxy design pattern implementation variant in Java and ABAP. Below paragraph is quoted directly from Wikipedia: “A proxy, in its most general form, is a class functioning as an interface to someth

blogs.sap.com

 

 

자바 프록시 디자인 패턴 – 정적 프록시

Dynamic proxy in Java – Variant1: InvocationHandler

Dynamic proxy in Java – Variant2: Proxy using CGLIB

Dynamic proxy in Java – Variant3: Create Proxy class dynamically via compiler API

Proxy Pattern in ABAP

Proxy Pattern in JavaScript

 

이 블로그에서는 Java 및 ABAP의 다양한 프록시 디자인 패턴 구현 변형들에 대해 소개합니다.

 

아래 단락은 Wikipedia에서 직접 인용한 것입니다.

“가장 일반적인 형태의 프록시는 어떤 것에 대한 인터페이스 역할을 하는 클래스를 말합니다.

프록시는 모든 것에 대한 인터페이스 : 네트워크 연결, 메모리 상 커다란 객체, 파일 또는 비용이 많이 들거나 복제가 불가능한 기타 리소스와 같은 것들이 될 수 있습니다. 요컨대, 프록시는 배후에서 실제 제공 객체에 액세스하기 위해 클라이언트가 호출하는 래퍼 또는 에이전트 개체입니다. 프록시를 사용하면 단순히 실제 객체로 전달하거나 추가적인 논리를 제공할 수 있습니다.

프록시에서 추가 기능을 제공할 수 있습니다. 예를 들어 실제 객체에 대한 작업이 리소스를 많이 사용하는 경우 캐싱하거나 실제 객체에 대한 작업이 호출되기 전에 전제 조건을 확인합니다.

클라이언트의 경우 프록시 객체를 사용하는 것은 실제 객체를 사용하는 방식과 비슷합니다. 둘 다 동일한 인터페이스를 구현하기 때문입니다."

 

프록시 디자인 패턴에 대한 UML 다이어그램:

 

먼저 Java에서 다양한 프록시 구현 접근 방식을 소개한 다음 이러한 접근 방식을 ABAP에서도 시뮬레이션할 수 있는지 확인 해 볼 것입니다.

 

Java 프록시 디자인 패턴 – 정적 프록시

예제는 매우 간단합니다 : 하나의 메서드 writeCode만 있는 IDeveloper 인터페이스:

public interface IDeveloper {
	public void writeCode();
}
// An implementation class for this interface:
public class Developer implements IDeveloper{
	private String name;
	public Developer(String name){
		this.name = name;
	}
	@Override
	public void writeCode() {
		System.out.println("Developer " + name + " writes code");
	}
}
 

테스트 코드:

public class DeveloperTest {
	public static void main(String[] args) {
		IDeveloper jerry = new Developer("Jerry");
		jerry.writeCode();
	}
}
 

테스트 출력물:

Developer Jerry writes code
 

이제 문제는 Jerry의 프로젝트 리더가 Jerry와 같은 팀의 개발자가 문서 없이 코드를 작성한다는 사실에 만족하지 않는다는 것입니다.

이 문제에 대해 논의 해보니, 모든 팀원들은 문서가 항상 코드와 함께 제공되어야 한다는 데 동의하고 있습니다.

개발자가 기존 구현 클래스 Developer를 직접 수정하지 않고 문서를 작성하도록 강제하기 위해 이제 정적 프록시가 무대에 등장해야 합니다:

public class DeveloperProxy implements IDeveloper{
	private IDeveloper developer;
	public DeveloperProxy(IDeveloper developer){
		this.developer = developer;
	}
	@Override
	public void writeCode() {
		System.out.println("Write documentation...");
		this.developer.writeCode();
	}
}
 

정적 프록시의 장점

당신은 해당 코드를 수정하지 않고 인터페이스에 대해 기존 STABLE 구현을 향상시키고 싶어한다고 하면 당신은 동일한 인터페이스를 구현하는 새 프록시를 만들고 원래 구현을 프록시 내의 개인 속성으로 래핑할 수 있습니다.

향상된 기능은 소비자 코드에 완전히 투명한 프록시 구현으로 만들어 집니다.

위의 Developer 예제로 돌아가서 소비자 코드는 writeCode 메서드를 호출하는 데 사용하는 변수가 실제 developer를 가리키는지 developer 프록시를 가리키는지 전혀 신경 쓰지 않습니다.

정적 프록시의 잇점:

1. 구현하고 이해하기 쉽다

2. 원래 구현과 프록시 간의 관계는 컴파일 시에 일찍 결정됩니다. 런타임 실행 시 추가 오버헤드가 없습니다.

 

정적 프록시의 단점

나는 정적 프록시의 단점을 설명하기 위해 여전히 예제를 사용 할 것입니다.

이제 문서 누락의 문제가 동료 QA에게도 계속 된다고 가정합니다.

우리가 여전히 정적 프록시를 통해 문제를 해결하려 한다면, Tester에 대한 또 다른 프록시 클래스를 도입해야 합니다.

아래는 Tester 용 인터페이스입니다.

public interface ITester {
	public void doTesting();
}
Original tester implementation class:
public class Tester implements ITester {
	private String name;
	
	public Tester(String name){
		this.name = name;
	}
	@Override
	public void doTesting() {
		System.out.println("Tester " + name + " is testing code");
	}
}
 

Tester의 프록시:

public class TesterProxy implements ITester{
	private ITester tester;
	public TesterProxy(ITester tester){
		this.tester = tester;
	}
	@Override
	public void doTesting() {
		System.out.println("Tester is preparing test documentation...");
		tester.doTesting();
	}
}
 

테스트 코드와 결과:

Tester 프록시의 소스 코드에서 Developer 프록시와 정확히 동일한 로직을 가지고 있음을 쉽게 알 수 있습니다.

나중에 소프트웨어 제공 프로세스에서 다른 역할에 대한 문서화 형식의 차이가 있어야 하는 경우 새로운 정적 프록시 클래스를 계속해서 도입해야 하므로 정적 프록시 클래스 번호가 팽창하고 DRY 위반이 발생합니다. 반복하지 마십시오.

 

Java의 동적 프록시 – Variant1: InvocationHandler

각 원래 구현 클래스에 대해 전용 정적 프록시 클래스를 사용하는 대신 이제 나는 공통 프록시 클래스 EnginnerProxy를 사용하여 모든 구체적인 역할을 제공합니다.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class EnginnerProxy implements InvocationHandler
{
    Object obj;
    public Object bind(Object obj)
    {
        this.obj = obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
                .getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable
    {
        System.out.println("Enginner writes document");
        Object res = method.invoke(obj, args);
        return res;
    }
}
 

키 노트

1. 비즈니스 인터페이스( IDeveloper 또는 ITester )의 전용 인터페이스에서 상속하는 대신 이 변형에서 일반 프록시는 JDK에서 제공하는 기술 인터페이스 인 InvocationHandler에서 상속합니다.

2. 일반 프록시가 가능한 모든 구체적인 구현 클래스에 대해 작동할 수 있도록 프록시 내에서 일반 유형 Object 변수가 정의됩니다.

3. 프록시된 인스턴스의 인터페이스 메소드가 호출되면 InvocationHandler에 정의된 invoke 메소드에 의해 인터셉트되며, 여기서 애플리케이션 개발자가 선언한 향상된 로직과 Java Reflection에서 호출하는 원래 로직이 함께 호출됩니다.

 

다음은 InvocationHandler가 설계한 동적 프록시가 사용되는 코드와 테스트 출력입니다:

 

동적 프록시의 제한 - 변형 1: 호출 처리기

이 변형은 정적 프록시의 중복 단점을 성공적으로 피했지만 여전히 구현 클래스가 인터페이스에 상속되지 않는 다면 동작이 제한 된다는 것입니다.

다음 예를 고려하십시오. 제품 소유자는 어떤 인터페이스도 구현하지 않습니다.

public class ProductOwner {
	private String name;
	public ProductOwner(String name){
		this.name = name;
	}
	public void defineBackLog(){
		System.out.println("PO: " + name + " defines Backlog.");
	}
}
 

다음 코드는 컴파일 시에는 구문 오류가 없습니다.

ProductOwner po = new ProductOwner("Ross");
ProductOwner poProxy = (ProductOwner) new EnginnerProxy().bind(po);
poProxy.defineBackLog();
 

안타깝게도 런타임 예외가 발생하죠:

Java의 동적 프록시 – Variant2: CGLIB를 사용하는 프록시

CGLIB는 Java 바이트 코드를 생성하고 변환하기 위한 높은 수준의 API를 제공하는 바이트 코드 생성 라이브러리입니다. AOP, 테스트, 데이터 액세스 프레임워크에서 동적 프록시 객체를 생성하고 필드 액세스를 가로채는 데 사용됩니다. 자세한 문서와 샘플 코드는 github에서 확인하세요.

이제 저는 CGLIB를 사용하여 인터페이스를 구현하지 않는 ProductOwner 클래스에 대한 프록시 클래스를 생성 할 것입니다.

CGLIB API를 통해 구현된 새로운 공통 프록시:

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class EnginnerCGLibProxy {
	Object obj;
    public Object bind(final Object target)
    {
        this.obj = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(obj.getClass());
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args,
                    MethodProxy proxy) throws Throwable
            {
                System.out.println("Enginner 2 writes document");
                Object res = method.invoke(target, args);
                return res;
            }
        });
        return enhancer.create();
    }
}
 

Consumer 코드:

ProductOwner ross = new ProductOwner("Ross");
ProductOwner rossProxy = (ProductOwner) new EnginnerCGLibProxy().bind(ross);
rossProxy.defineBackLog();
 

테스트 결과에서 우리는 구현 클래스 ProductOwner가 어떤 인터페이스도 구현하지 않고 여전히 공용 메서드 defineBackLog가 성공적으로 프록시됨을 분명히 알 수 있습니다.

 

Java에서 동적 프록시의 제한 – 변형 2: CGLIB를 사용하는 프록시

 

CGLIB의 마법은 제 블로그 Simulate Mockito in ABAP에서 이미 설명했습니다.

CGLIB에 의해 생성된 동적 프록시는 실제로 프록시되는 원래 클래스의 임시 하위 클래스입니다.

이 동적 생성 기능은 매우 강력하여 Mockito와 같은 많은 Java Framework에서 Spring의 테스트 객체 및 AOP를 모의하기 위해 널리 사용됩니다.반면에 이 변형의 한계는 상속에 기반한 구현 방식으로 인해 명백합니다. 이제 막 ProductOwner 클래스를 final 로 표시 했습니다. 이렇게 변경하면, CGLIB 프록시 접근 방식은 더 이상 작동하지 않습니다: 이 라이브러리는 final로 표시되지 않은 클래스에 대해서만 작동합니다.

 

Java의 동적 프록시 - Variant3: 컴파일러 API를 통해 프록시 클래스를 동적으로 생성

아래 왼쪽 클래스 ProductOwner는 어떤 인터페이스도 구현하지 않는 최종 클래스입니다.

백로그 정의의 실제 작업이 실행되기 전에 나는 문서 준비 로그를 인쇄하는 새 프록시 클래스를 그 위에 만들고 싶다고 가정 해 봅시다.이전 두 변형에서 설명한 것처럼 이 요구 사항은 변형 1 또는 2로 충족될 수 없습니다.마지막 해결책으로 완전히 새로운 클래스를 동적으로 생성해야 합니다.

JDK의 파일 API를 통해 원래 클래스 ProductOwner의 소스 코드를 얻을 수 있다고 가정하고, 여기 단순하게 하기 위해서 getSourceCode 메서드 내에 소스코드를 하드코딩 하였습니다.

여기서는 단순화를 위해 getSourceCode 메서드에서 소스 코드를 하드 코딩합니다. 강조 표시된 35행에 주의하십시오. 인쇄 문서 작업에 대한 프록시 논리가 작성됩니다.

소비자 코드 및 테스트 출력: 예상대로 작동합니다.

A리고 동적으로 생성된 Java 파일은 IDE에서 즉시 볼 수 있습니다.

전체 마법은 getProxyClass() 메서드에 있습니다.

이 변형 3 예제의 동적 새 클래스 생성은 세 단계로 구성됩니다.

 

1. getSourceCode 메서드에서 어셈블된 새 Java 클래스의 소스 코드를 ProductOwnerSCProxy.java라는 새 로컬 파일에 채웁니다( SCProxy는 SourceCode 프록시를 의미함).

단순화를 위해 나는 다시 절대 파일 경로를 사용합니다.

2. JavaCompiler API를 통해 동적으로 채워진 이 Java 파일을 컴파일합니다:

3. ClassLoader를 통해 컴파일된 클래스를 로드합니다. 완료되면 클래스 개체를 다양한 리플렉션 API에서 사용할 수 있습니다.

이 변종은 큰 보안 및 성능 문제를 일으킬 수 있으므로 권장하지 않는 솔루션입니다.

지금까지 Java에서 정적 프록시와 세 가지 동적 프록시 패턴 구현을 소개했습니다. 이 블로그에서 사용된 예제의 모든 관련 소스 코드는 여기에서 찾을 수 있습니다.

 

ABAP의 프록시 패턴

정적 프록시 패턴은 변형 CRM 애플리케이션 또는 테스트 프레임워크에서도 광범위하게 사용됩니다.

내 블로그에서 한 가지 예를 찾을 수 있습니다. 단위 테스트에 CRM Mock 프레임워크를 사용하는 예입니다.

구현 클래스의 이름이 매우 명확한 힌트를 제공하므로 CL_CRM_PRODUCT_MOCK_PROXY에는 모의 논리가 포함되고 CL_CRM_PRODUCT_REAL_PROXY는 실제 생산 논리를 담당합니다.

ABAP의 동적 프록시 패턴 – 변형 1

이 변형을 통해 생성된 프록시 클래스는 일시적입니다. 즉, 현재 런타임 세션에서만 클래스를 사용할 수 있습니다.

Java 부분에 도입된 CGLIB 변종과 유사하게 작동합니다.

 

자세한 구현 단계는 다음 두 블로그를 참조하십시오:

ABAP에서 Mockito 시뮬레이션

ABAP에서 CGLIB 구현

 

ABAP의 동적 프록시 패턴 – 변형 2

이전 장 Java의 동적 프록시 - Variant3: 컴파일러 API를 통해 동적으로 프록시 클래스 생성에서 API를 통해 Java에서 완전히 새로운 클래스를 생성할 수 있는 가능성을 확인했습니다.

ABAP에도 비슷한 유틸리티 API가 있습니다.

예를 들어 함수 모듈 SEO_CLASS_CREATE_COMPLETE는 응용 프로그램 개발자가 지정한 소스 코드를 기반으로 지속성을 가진 클래스를 만드는 데 사용할 수 있습니다.

이 변형을 달성하는 방법에 대한 자세한 설명은 블로그 Create dynamic proxyly in Java and ABAP에서 찾을 수 있습니다.

 

JavaScript의 프록시 패턴

실제 사용 사례 보기 프록시 패턴을 사용하여 UI5에서 더 나은 이미지 로드 동작을 만듭니다.

한편 JavaScript에는 내장 프록시 지원 기능도 있습니다. 다음 코드를 고려하십시오.

function Employee(name){
	this.name = name;
};
Employee.prototype.work = function(language){
	console.log(this.name + " is developing with: " + language);
}
let jerry = new Employee("Jerry");

function hireEmployee(employee, language){
    employee.work(language);
}
hireEmployee(jerry, "JavaScript");
 

Output:

Jerry is developing with: JavaScript
 

그래서 Jerry는 JavaScript 개발을 위해 고용되었습니다.

이제 Jerry는 여가 시간에 몇 가지 새로운 언어를 배우고 싶고 물론 Jerry는 여전히 고용 상태를 유지해야 합니다. 즉, Employee.prototype.work 및 HireEmployee 두 함수 모두 변경하지 않고 유지해야 합니다.

이제 Proxy가 무대에 오를 시간입니다.

먼저 사용자 지정 논리를 정의합니다. ABAPer는 아래 코드를 클래스 메서드에 정의된 일종의 사후 종료로 간주할 수 있습니다.

var proxyLogic = {
    get: function(target, name) {
    	if( name == "work"){
    		var oriFun = target[name].bind(target);
    		return function(language){
    			oriFun(language);
    			console.log("and also study other language in spare time");
    		}
    	}
    }
};
 

그리고 프록시 생성자의 첫 번째 인수로 Jerry의 원래 직원 인스턴스를 래핑하는 새 프록시를 만들고 두 번째 매개 변수는 변수 proxyLogic에 정의된 사용자 지정 동작입니다.

var jerryProxy = new Proxy(jerry, proxyLogic );
 

소비자 코드인hirEmployee는 이 변경 사항을 전혀 인식하지 못합니다. JavaScript는 동적 유형 언어이기 때문에 HierEmployee는 들어오는 매개 변수가 Employee의 인스턴스인지 또는 다른 무엇이든 상관하지 않습니다.

정말 중요한 것은 매개변수에 "work"라는 이름의 메서드가 있어야 한다는 것입니다.

hireEmployee(jerryProxy, "JavaScript");
 

위의 호출은 다음 출력을 생성합니다.

JavaScript 내장 프록시 구현의 마법은 프록시에 대해 속성 액세스가 수행될 때마다(제 예에서는 작업 메서드가 호출됨) 인터셉터 역할을 하는 사용자 지정 논리가 호출된다는 것입니다. 거기에 사후 향상.

 

추가 자료

ABAP, JavaScript 및 Java 간의 언어 기능을 비교하는 일련의 블로그를 작성했습니다. 아래에서 목록을 찾을 수 있습니다.

JavaScript 및 ABAP의 Lazy Loading, Singleton 및 Bridge 디자인 패턴

함수형 프로그래밍 – ABAP에서 Curry 시뮬레이션

함수형 프로그래밍 – JavaScript 및 ABAP에서 Reduce를 사용해 보십시오.

ABAP에서 Mockito 시뮬레이션

ABAP에서 Java Spring 종속성 주입 주석 @Inject의 시뮬레이션

싱글톤 바이패스 – ABAP 및 Java

ABAP 및 Java의 약한 참조

ES5, ES6 및 ABAP의 피보나치 수열

Java 바이트 코드 및 ABAP 로드

컴파일러에 의해 거부된 올바른 프로그램을 작성하는 방법: Java 및 ABAP의 예외 처리

Java 및 ABAP에서 가비지 수집을 배우는 작은 예

ABAP, ES6, Angular 및 React의 문자열 템플릿

ABAP RTTI 및 Java Reflection을 통해 정적 개인 속성에 액세스하려고 합니다.

ABAP, Java 및 JavaScript의 로컬 클래스

ABAP, Java 및 JavaScript의 정수

Java의 공분산과 ABAP의 시뮬레이션

Java 및 ABAP의 다양한 프록시 디자인 패턴 구현 변형

ABAP 및 Java의 태그(마커) 인터페이스

 

이상.

 

728x90