본문 바로가기

개발&컴퓨터/개발강좌

스프링(Spring) 프레임워크 기본 개념 강좌 (5) - AOP Proxy

반응형

* 스프링의 핵심 개념

 - 이어서 계속

 

 5) AOP Proxy (AOP 프록시)

 

  5-1) 프록시(Proxy)란?

 

 - 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장하여 클라이언트 클라이언트의 요청을 받아주어 처리하는 대리자 역할.

프록시의 단어 자체로는 '대리인'이라는 의미를 내포하고 있음. 스프링 AOP에서의 프록시란 말그대로 대리하여 업무를 처리. 함수 호출자는 주요 업무가 아닌 보조 업무를 프록시에게 맡기고, 프록시는 내부적으로 이러한 보조 업무를 처리.

 

이렇게 함으로써, 주 업무 코드는 보조 업무가 필요한 경우, 해당 Proxy 만 추가하면 되고, 필요없게 되면 Proxy를 제거하면 됨.

보조 업무의 탈 부착이 쉬워지고, 그리하여 주 업무 코드는 보조 업무 코드의 변경으로 인해서 발생하는 코드 수정 작업이 필요 없게 됨.

 

 

 

- 프록시의 호출 및 처리 순서 -

1) Proxy 호출
2) 보조 업무 처리
3) Proxy 처리 함수(메서드) 가 실제 구현 함수(메서드) 호출 및 주 업무 처리
4) 제어권이  다시 Proxy 함수(메서드)로 넘어오고 나머지 보조 업무 처리
5) 처리 작업 완료 후, 호출 함수(메서드)로 반환.

 

  5-2) 프록시의 사용 목적

 * 클라이언트가 타깃(Target)에 접근하는 방법을 제어.

 * 타깃에 부가적인 기능을 부여.

 

 (실제 이러한 목적으로 어떻게 사용되는지는 아래에서 보자!)

 

  5-3) 프록시 구현

 

 * 먼저 사칙 연산을 위한 인터페이스(Interface)와 실제 기능을 구현한 클래스(Class) 정의.

// 사칙 연산을 정의하는 인터페이스

package aoptest;

 

public interface Calculator {

    public int add(int x, int y);

    public int subtract(int x, int y);

    public int multiply(int x, int y);

    public int divide(int x, int y);

}

 

 

// 사칙 연산을 구현하는 클래스

package aoptest;

 

public class myCalculator implements Calculator {

   

    @Override

    public int add(int x, int y) {

        return x + y;

    }

   

    @Override

    public int subtract(int x, int y) {

        return x – y;

    }

   

    @Override

    public int multiply(int x, int y) {

        return x * y;

    }

 

    @Override

    public int divide(int x, int y) {

        return x / y;

    }

}

 

 

 

// 실제 위의 사칙연산 클래스를 사용하는 예제 코드

public static void main(String[] args) {

    Calculator cal = new myCalculator(); // 다형성 

    System.out.println(cal.add(3, 4)); // add 메서드를 호출하여 3 + 4 결과를 출력

}

 

 

여기서!!

실제 사칙 연산을 하는데에 있어서 실제 소요되는 시간 측정 이라는 기능이 필요하다고 가정하자!

그렇다면 어떻게 해야 할까?

 

위의 예제 코드에서 cal.add(3, 4) 를 사용하고 있으니, 이 시간 측정을 위한 코드를 일단 myCalculator 클래스의 add 메서드에 추가하여 보자!

 

package aoptest;

 

public class myCalculator implements Calculator {

   

    @Override

    public int add(int x, int y) {

 

 // 보조 업무 (시간 측정 시작 & 로그 출력)

Log log = LogFactory.getLog(this.getClass());

StopWatch sw = new StopWatch();

sw.start();

log.info(“Timer Begin”);

 

int sum = x + y; // 주 업무 (덧셈 연산)

 

 // 보조 업무 (시간 측정 끝 & 측정 시간 로그 출력)

sw.stop();

log.info(“Timer Stop – Elapsed Time : ”+ sw.getTotalTimeMillis());

 

return sum;

    }

 

 

...

 

}

 

 

위와 같이 시간을 측정하기 위한 업무(실제 연산 업무가 아니기 때문에 보조 업무)가 add 메서드에 추가됨.

하지만, 시간 측정을 위한 이러한 코드를 add 메서드 뿐만 아니라, subtract, multiply, divide 메서드에도 추가해 주어야 하고, 설령 측정 방식이 달라지거나 로그 출력 내용이 변경 되기라도 한다면 모든 메서드를 수정해야 한다. 게다가 연산을 위한 주 업무 코드를 수정하는 것도 아니다.

 

 

그렇다면 좀 더 코드를 깔끔하게 하고, 관리도 쉽게 하고, 직관적인 프로그래밍을 위해서는 어떻게 변화를 줄 수 있을까?

바로 방금(또 이전 포스팅에서) 배운 주 업무와 보조 업무를 분리(크로스 컷팅, Cross Cutting)하고 보조 업무를 프록시(Proxy)에게 넘긴다!

 

 

AOP Proxy 를 구현한 예제 코드

// 보조 업무를 처리할 프록시 클래스 정의 (여기서는 LogPrintHandler 라 이름 지었음)

public class LogPrintHandler implements InvocationHandler { // 프록시 클래스 (핸들러)

    private Object target; // 객체에 대한 정보

 

    public LogPrintHandler(Object target) { // 생성자

        this.target = target;

    }

 

    @Override

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable   {

        Log log = LogFactory.getLog(this.getClass());

        StopWatch sw = new StopWatch();

        sw.start();

        log.info(“Timer Begin”);

        int result = (int) method.invoke(target, args); // (3) 주업무를 invoke 함수를 통해 호출

        sw.stop();

        log.info(“Timer Stop – Elapsed Time : ”+ sw.getTotalTimeMillis());

        return result;

}

 

 

 

public static void main(String[] args) {

    Calculator cal = new myCalculator();

 

    Calculator proxy_cal = (Calculator) Proxy.newProxyInstance( // (1)

        cal.getClass().getClassLoader(),  // Loader

        cal.getClass().getInterfaces(), // Interface

        new LogPrintHandler(cal)); // Handler (보조 업무를 구현하고 있는 실제 클래스)

 

    System.out.println(proxy_cal.add(3, 4));    // (2) 주 업무 처리 클래스의 add 메서드를 호출

}

 

 

 

위의 클래스와 메서드에 대한 설명.

* (1) InvocationHandler 인터페이스를 구현한 객체는 invoke 메소드를 구현해야 함. 
  해당 객체에 의하여 요청 받은 메소드를 리플렉션 API를 사용하여 실제 타깃이 되는 객체의 메소드를 호출해준다. (실제 LogPrintHandler 클래스의 invoke 메서드 내에서 method.invoke(target, args) 메서드를 호출하는데, 이는 주 업무의 메서드를 호출하는 것. main 메서드에서 Proxy.newProxyInstance 를 통해 주 업무를 처리할 클래스(myCalculator)와 보조 업무를 처리할 Proxy 클래스를 결합.

 

  - cal(변수) 실제 객체를 proxy_cal(변수) 객체에 핸들러를 통해서 전달. (LogPrintHandler())

  - getClassLoader() : 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더.
  - getInterfaces() : 구현할 인터페이스.
  - LogPrintHandler(cal) : 부가 기능과 위임 코드를 담은 핸들러.

Example)
 Hello proxiedHello = (Hello)Proxy.newProxyInstance(
     getClass().getClassLoader(), -> 동적으로 생성되는 다이내믹프록시 클래스의 로딩에 사용할 클래스 로더
     new Class[] {Hello.class}, <- 구현할 인터페이스
     new UppercaseHandler(new HelloTarget())); -> 부가기능과 위임코드를 담은 핸들러

* (2) 그런다음 주 업무 클래스의 메서드를 호출하게 되면 프록시 클래스의 invoke 메서드가 호출되어 자신의 보조 업무를 처리하고, 주 업무의 메서드를 호출한다.


* (3) invoke() : 메소드를 실행시킬 대상 오브젝트와 파라미터 목록을 받아서 메소드를 호출한 뒤에 그 결과를 Object  타입으로 돌려준다.

[실제 프록시 및 주 업무(타깃) 처리 순서]

 호출 및 처리 순서 : 클라이언트 ---> 프록시 ---> 타깃

 

AOP 개념은 스프링에 한정되어 사용되는 개념이 아님.

실제 위의 코드는 AOP Proxy의 구현(객체를 생성하고, 값을 주입)하는 것을 순수 JAVA 코드 단에서 처리한 것. (스프링 개념 없이)

스프링을 통해서라면 AOP 는 XML과 어노테이션(Annotation)을 통해서 더 쉽게 구현할 수 있음.

 

 

 

반응형