티스토리 뷰

728x90
반응형

 

 

프록시 패턴(Proxy Pattern)

 

지난 글에서 스프링 AOP란 관점 지향 프로그래밍이며, 부가 기능을 모듈화 함으로써

 

핵심 기능 개발에 집중할 수 있도록 하는 패러다임이라는 것을 언급했다.

 

https://gnidinger.tistory.com/458

 

[Spring]AOP(Aspect Oriented Programming) - 용어 정리

이전 글에서 스프링 프레임워크의 네 가지 특징과 AOP에 대해 살펴봤었다. 관련 글: https://gnidinger.tistory.com/450 [Spring]Spring Framework, Spring Triangle Spring Framework, 혹은 Spring은 Java/Kotl..

gnidinger.tistory.com

따라서 비즈니스 로직의 중간에 소스코드를 삽입해야 할 필요성이 생기는데,

 

이때 스프링에서 사용하는 디자인 패턴이 바로 프록시 패턴(Proxy Pattern)이다.

 

프록시라는 단어는 대리, 대리인이라는 뜻을 가지고 있는데,

 

스프링에서 프록시는 타겟을 감싸서 타겟의 요청을 대신 받아주는 랩핑(Wrapping) 오브젝트라 할 수 있다.

 

또한 프록시 패턴은 연산을 할 때 객체 스스로가 직접 처리하지 않고 중간에 다른 프록시 객체를 통해 처리하는 방법을 말한다.

 

프록시 패턴의 대략적인 Work Flow는 다음과 같다.

 

  1. 클라이언트에서 인터페이스(IService)를 통해 타겟(Service)을 호출하면, 타겟을 감싸고 있는 Proxy가 호출된다.
  2. 호출된 프록시는 타겟을 실행하기 전에 어드바이스에 등록된 전처리(preprocess)를 한다.
  3. 프록시는 어드바이스에 등록된 기능을 수행 후 타겟(실제 클래스, 메서드, 여기선 Service)을 호출한다.
  4. 프록시가 타겟 메서드 실행 후 후처리(존재한다면)를 한다.
  5. 타겟에서 반환받은 값을 클라이언트에게 반환한다.

이때 프록시 패턴의 특징은 아래와 같다.

  • 프록시와 타겟이 공유하는 인터페이스(IService)가 존재하고, 클라이언트는 인터페이스 타입으로 프록시를 사용
  • 클라이언트는 프록시를 거쳐 타겟을 사용해야 함, 즉 프록시는 타겟에 대한 참조변수를 갖는다.
  • 프록시는 타겟과 같은 메서드를 구현하며 기존 코드의 변경 없이 타겟에 대한 접근 관리, 부가기능 제공, 리턴 값 변경 등이 가능함
  • 리얼 서브젝트는 핵심 기능만 수행하면서 프록시를 사용해 부가기능(로깅, 트랜잭션 등) 제공 가능

기존 코드의 변경 없이 부가 기능을 추가할 수 있다는 부분이 핵심이다.

 

계속해서 짧은 코드로 프록시 패턴에 대한 예를 보자.

 

  • EventService.java (Subject)
package proxyexam.start;

public interface EventService {

    void createEvent();

    void publishEvent();

}

 

  • SimpleEventService.java (Real Subject)
package proxyexam.start;

import org.springframework.stereotype.Service;

@Service
public class SimpleEventService implements EventService {

    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    @Override
    public void publishEvent() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Published an event");
    }

}

 

  • AppRunner.java (Client)
package proxyexam.start;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    EventService eventService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        eventService.createEvent();
        eventService.publishEvent();
    }

}

위 다이어그램에서 AppRunner는 Client, EventService는 Subject, SimpleEventService는 Real Subject라고 할 수 있다.

 

EventService를 상속받은 SimpleEventService에 동작 코드가 작성되어 있고, AppRunner를 통해서 실행되기 때문이다.

 

계속해서 프록시 객체를 이용해 기존 코드를 수정하지 않고 기능을 추가해 보자.

 

  • ProxySEService.java (Proxy)
package proxyexam.start;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

@Primary // Client에서 Eventservice를 주입받아 메서드를 호출하면 @Primary에 의해 프록시 개체 호출
@Service
public class ProxySEService implements EventService { // 같은 인터페이스 구현

    @Autowired
    SimpleEventService simpleEventService; // 프록시는 원래 객체의 빈 주입받아 사용

    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis(); // 새로 추가한 기능
        simpleEventService.createEvent(); // 원래 객체의 기능 위임
        System.out.println(System.currentTimeMillis() - begin); // 새로 추가한 기능
    }

    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis(); // 새로 추가한 기능
        simpleEventService.publishEvent(); // 원래 객체의 기능 위임
        System.out.println(System.currentTimeMillis() - begin); // 새로 추가한 기능
    }
}

위와 같이 코드를 입력하면 기존 코드에 손을 대지 않고도 기능을 추가할 수 있게 된다.

 

하지만 여기도 문제가 발생하는데, 프록시 객체에 중복 코드가 발생할 수 있고

 

다른 클래스에 동일한 기능을 사용하고자 할 때, 매번 프록시 객체를 생성해줘야 하는 점이 그것이다.

 

이런 문제를 해결하는 방법은 런타임시 동적으로 프록시객체를 만들어주는 것인데,

 

스프링 IoC 컨테이너가 제공하는 기반 시설을 이용해 이 작업을 수행하는 것을 스프링 AOP라고 한다.

 

 

스프링 AOP

 

애너테이션을 기반으로 한 AOP에 대해 알아보자.

 

AOP를 사용하기 위해, 먼저 build.gradle에 다음과 같은 의존성을 추가해야 한다.

 

  • build.gradle
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-aop'

	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
}

이어서 Aspect 클래스를 생성한다.

 

  • PerfAspect.java
package proxyexam.start;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component // @ComponentScan을 통해서 빈 등록
@Aspect // Aspect 클래스임을 선언
public class PerfAspect {

    // 완성된 Advice는 @Around 애너테이션을 붙임
    @Around("execution(* proxyexam.*.EventService.*(..))") 
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }
}

Aspect에는 2가지 정보가 필요한데, Pointcut(적용 위치)Advice(적용 내용)가 그것이다.

 

ProceedingJoinPoint는 Advice가 적용되는 createEvent, publishEvent 등의 대상이다.

 

Object에는 타겟에 해당하는 메서드를 호출한다.

 

execution은 Pointcut 표현식으로, Advice를 어디에 적용할지를 표시할 수 있다.

 

예를 들면 (* proxyexam.*.EventService.*(..))는 proxyexam 패키지 밑에 있는 클래스 중

 

EventService에 있는 모든 메서드에 Advice를 적용하라고 표시한 것이다.

 

마지막으로 이렇게 완성된 Advice에는 @Around를 붙여준다.

 

프록시 클래스를 설정할 때에 비해 코드가 절반으로 짧아진 것을 확인할 수 있다.

 

만약 전체 메서드가 아니라 원하는 메서드에만 Advice를 적용하고 싶을 경우엔

 

execution이 아닌 애너테이션을 만들어 사용하면 된다.

 

  • PerfLogging.java (애너테이션 파일 생성)
package proxyexam.start;

import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD) // 타겟을 메서드로 설정
@Retention(RetentionPolicy.CLASS) // 애너테이션을 .class 파일까지 유지
public @interface PerfLogging {
}

 

  • PerfAspect.java (수정) - execution 대신 @annotation 사용. 해당 빈의 모든 메서드에 적용할 경우 bean(bean이름) 입력.
package proxyexam.start;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class PerfAspect {

    @Around("@annotation(PerfLogging)") // execution 제거, @annotation 추가
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis() - begin);
        return retVal;
    }

}

 

  • SimpleEventService.java (수정)
package proxyexam.start;

import org.springframework.stereotype.Service;

@Service
public class SimpleEventService implements EventService {

    @PerfLogging // Advice 적용 원하는 메서드 지정
    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    @Override
    public void publishEvent() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Published an event");
    }
}

위 예제에서는 @Around 만을 이용해 진행했다. @Around는 가장 강력한 어드바이스이며 대부분의 기능을 제공하지만,

 

타겟 등 고려해야 할 사항이 있을 때 정상적으로 작동이 되지 않는 경우가 있다.

 

이럴 때 @Before, @After와 같은 Advice를 사용하면 제약이 생기지만 역할을 명확하게 알 수 있다.

 

좋은 설계는 @Around만 사용해서 모든 것을 해결하는 것보다는 제약을 가지더라도 실수를 미연에 방지하는 것이기 때문에

 

다른 타입의 Advice를 알아두는 것은 도움이 된다. 짧게 정리하고 넘어가자

 

 

타입별 Advice

 

들어가기 전에, Advice는 기본적으로 순서를 보장하지 않는다.

 

순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애너테이션을 적용해야 하는데,

 

이때 Advice 단위가 아니라 클래스 단위로만 순서를 지정할 수 있다.

 

즉, 하나의 Aspect에 여러개의 Advice가 존재하면 순서를 보장받을 수 없다.

 

따라서 순서를 정확하게 지정하고 싶으면 Aspect를 개별 클래스로 분리해야 한다.

 

계속해서 타입별 Advice에 대해 알아보자.

 

  • @Before (이전)

    • 조인 포인트 이전에 실행
    • 타겟 메서드가 호출되기 전에 처리해야 할 부가 기능 실행
    • @Before를 구현한 메서드는 일반적으로 리턴 타입이 void - 리턴 값이 있어도 Advice 적용 과정에 영향 없음
    • 메서드에서 예외를 발생시킬 경우 대상 객체의 메서드가 호출되지 않음
    • 작업 흐름을 변경할 수 없으며, 종료시 자동으로 다음 타겟 호출(예외 발생시 제외)
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
    log.info("[before] {}", joinPoint.getSignature());
}

 

  • @AfterReturning (정상적 반환 이후)

    • 조인 포인트 정상 완료 후 실행
    • 메서드가 예외 없이 실행된 이후에 부가 기능 실행
    • 메서드 실행이 정상적으로 반환될 때 실행
    • returning 속성에 사용된 이름은 Advice 메서드의 매개변수 이름과 일치해야 함
    • returning 절에 지정된 타입의 값을 반환하는 메서드만 대상을 실행
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
    log.info("[return] {} return={}", joinPoint.getSignature(), result);
}

 

  • @AfterThrowing (예외 발생 이후)

    • 타겟 메서드가 수행 중 예외를 던져 종료되는 경우에 실행
    • 메서드를 실행하는 도중 예외가 발생한 경우 부가 기능을 실행
    • throwing 속성에 사용된 이름은 Advice 메서드의 매개변수 이름과 일치해야 함
    • throwing 절에 지정된 타입과 맞은 예외를 대상으로 실행
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
    log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}

 

  • @After (finally)(이후)

    • 조인 포인트의 동작(성공 또는 예외)과 상관없이 실행
    • 결과에 관계없이 타겟 메서드가 완료 되면 부가 기능을 수행
    • 일반적으로 리소스를 해제하는데 사용
  • @Around (메소드 실행 전후)

    • 메서드 호출 전후에 수행하는 가장 강력한 Advice - 조인 포인트 실행 여부, 반환 값 변환, 예외 처리 등이 가능

      • 조인 포인트 실행 여부 선택 - joinPoint.proceed()
      • 전달 값 변환 - joinPoint.proceed(args[])
      • 반환 값 변환
      • 예외 변환
      • try ~ catch ~ finally  들어가는 구문 처리 가능
    • Advice가 타겟 메서드를 감싸서 타겟 메서드 호출 전/후 예외 발생 시점에 부가 기능을 실행
    • Advice의 첫 번째 파라미터는 ProceedingJoinPoint여야 함
    • 여러 번 사용할 수 있는 proceed()를 통해 대상을 실행
반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함