ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [16] Spring AOP (Proxy-based AOP)
    Spring/Spring 핵심 기술 2020. 5. 4. 08:54
    반응형

    Spring AOP는 Spring bean에만 적용할 수 있는 proxy pattern의 AOP 구현체입니다.

     

    [Proxy Pattern이란?]

    Proxy pattern은 software design pattern 중 하나입니다. 일반적으로 proxy란  실제 객체(여기서는 Spring bean)에 대한 interface 역할을 수행하는 class의 객체를 말합니다. proxy와 resource는 동일한 interface type을 구현합니다. proxy와 실제 객체는 동일한 interface type이기 때문에 동일한 책임(method)을 수행할 수 있어야 합니다. 하지만 proxy는 직접 책임을 수행하지 않고 실제 객체가 대신 처리하도록 delegation 합니다. 그렇기 때문에 proxy는 실제 객체를 감싸서 client와 실제 객체사이를 중계하는 역할을 하는 wrapper로 사용됩니다. 실제 객체를 수정하지 않고 책임을 추가하거나 접근제어를 하기 원하는 경우 실제 객체 대신 proxy만 수정해주면 되므로 유지보수에 대한 부담을 줄일 수 있습니다.

     

    Proxy Pattern

     

     


     

     

    method의 성능을 측정 logging 기능을 Spring AOP를 사용하여 만들어 보도록 하겠습니다.

     

    [AOP를 사용하지 않을 경우의 예]

    AOP를 사용하지 않고 성능 측정을 하기 원하는 모든 method에 logging 기능을 추가합니다. 먼저 client 객체(여기서는 ApplicationRunner 객체 - AppRunner.java)의 요청을 받을 수 있는 EventService라는 interface를 하나 만듭니다.

    public interface EventService {
    
        void createEvent();
    
        void publishEvent();
    
        void deleteEvent();
    }

     

    EventService interface를 구현하는 class(SimpleEventService.java)를 다음과 같이 작성합니다. 성능 측정 기능은 createEvent(), publishEvent() method만 적용하고 deleteEvent() method에 대해서는 성능측정을 하기 원하지 않는다고 가정합시다. 성능 측정을 위해 createEvent(), publishEvent() method에 각각 method 종료 시각 - method 시작 시각으로 method 실행 시간을 측정하는 code를 추가했습니다. 이렇게 공통된 code가 바로 crosscutting concern이며 AOP로 관리되어야 할 대상입니다.  

    @Service
    public class SimpleEventService implements EventService {
    
        @Override
        public void createEvent() {
            long begin = System.currentTimeMillis();    //Crosscutting concerns
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Create an Event");
    
            System.out.println(System.currentTimeMillis() - begin); //Crosscutting concerns
        }
    
        @Override
        public void publishEvent() {
            long begin = System.currentTimeMillis();    //Crosscutting concerns
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Publish an Event");
    
            System.out.println(System.currentTimeMillis() - begin); //Crosscutting concerns
        }
    
        @Override
        public void deleteEvent() { System.out.println("Delete an Event"); }
    }

     

    이제 client객체인 AppRunner 객체에서 EventService type의 bean을 주입받아 각 method를 실행하여 실행 시간을 측정하도록 만들어줍니다.

    @Component
    public class AppRunner implements ApplicationRunner {
    
        @Autowired
        EventService eventService;  //interface type이 정의되어 있는 경우는 interface type으로 주입 받는 것을 습관하 하는 것이 좋습니다.
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            eventService.createEvent();
            eventService.publishEvent();
            eventService.deleteEvent();
        }
    }

     

    code를 실행하면 다음과 같이 createEvent(), publishEvent() method의 실행 시간이 함께 출력 됩니다.

    Result

    그런데 이 예제에서는 성능 측정 기능을 추가하기 위해, bean을 생성하는 class의 code를 직접 수정하고 있습니다.  AOP는 기존의 code를 수정하지 않고 새로운 기능을 추가할 수 있어야 합니다. 이를 위해서 Proxy pattern을 사용하도록 합니다.

     

     

     

     

    [Proxy pattern을 사용 - 실제 객체 code 수정 없이 기능을 추가]

    기능 추가를 하고 싶은 객체와 동일한 interface type의 Proxy 객체(여기서는 ProxySimpleEventService.java)를 만들어줍니다.

    @Primary    //	①
    @Service
    public class ProxySimpleEventService 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);	//	③
        }
    
        @Override
        public void deleteEvent() {
            simpleEventService.deleteEvent();	//	④
        }
    }

    ① 기능 추가를 원하는 bean과 proxy bean이 동일 type이므로 client bean에서 주입받을 때 선택할 우선순위를 지정해야 합니다.

     

    ② 일반적으로는 구현하고 있는 interface type을 취하게 되지만 proxy pattern에서는 실제 객체로 책임을 위임해야 하기 때문에 실제객체 type의 bean을 주입받습니다. 또한 EventService simpleEventService의 형태로 실제 객체의 class명을 smallcase로 시작하는 객체명을 주는 식으로 선언해도 동일한 bean을 주입받을 수 있습니다.

     

    ③ 성능 측정 code를 추가해 줍니다. 이로써 실제 객체의 code는 전혀 수정하지 않고도 기능을 추가할 수 있습니다.

     

    ④ 실제 객체가 가진 책임은 그 객체가 처리하도록 위임해줍니다.

     

     

     

    성능 측정에 대한 책임은 proxy 객체가 수행하도록 변경했으니 실제 객체 class(SimpleEventService.java)에서는 해당 code들을 삭제하도록 합니다.

    @Service
    public class SimpleEventService implements EventService {
    
        @Override
        public void createEvent() {
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Create an Event");
        }
    
        @Override
        public void publishEvent() {
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Publish an Event");
        }
    
        @Override
        public void deleteEvent() { System.out.println("Delete an Event"); }
    }

     

     

    Proxy patter을 사용하여 실제 객체의 책임 추가 없이도 동일한 실행 결과를 얻을 수 있습니다.

    result

    하지만 Proxy 객체 code 안에는 주석으로 표기해 놓은 것과 같이 여전히 crosscutting concern이 남아있습니다. 또한 실제 객체의 class마다 짝을 이루는 Proxy class를 선언하고 각 method들을 실제 객체가 처리하도록 위임해야 하는 불편함도 따릅니다.

    @Primary
    @Service
    public class ProxySimpleEventService implements EventService {
        @Autowired
        SimpleEventService simpleEventService;
    
        @Override
        public void createEvent() {
            long begin = System.currentTimeMillis();	//Crosscutting concern
    
            simpleEventService.createEvent();
    
            System.out.println(System.currentTimeMillis() - begin);	//Crosscutting concern
        }
    
        @Override
        public void publishEvent() {
            long begin = System.currentTimeMillis();	//Crosscutting concern
    
            simpleEventService.publishEvent();
    
            System.out.println(System.currentTimeMillis() - begin);	//Crosscutting concern
        }
    
        @Override
        public void deleteEvent() {
            simpleEventService.deleteEvent();
        }
    }

     

     

    이러한 불편함을  효과적으로 해결하기 위해 proxy class를 runtime에 동적으로 생성하고 Spring IoC container와 연동될 수 있도록 해주는 것이 Spring AOP입니다. 이제 Spring AOP를 사용하여 proxy pattern을 사용했을 때보다 사용하기 편하고 유지보수가 용이하도록 바꿔봅시다.

     

     

     

    [Spring AOP를 사용]

    Spring AOP를 사용하기 위해서는 우선 AOP 관련 의존성을 넣어줘야 합니다. build.gradle의 의존성 관련 설정을 수정해줍니다. 수정된 전체 내용은 다음과 같습니다.

    buildscript {
        ext {
            springBootVersion = '2.1.7.RELEASE'
        }
        repositories {
            mavenCentral()
            jcenter()
        }
        dependencies {
            classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        }
    }
    
    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'
    
    group 'org.example'
    version '1.0-SNAPSHOT'+new Date().format("yyyyMMddHHmmss")
    
    sourceCompatibility = 1.8
    
    repositories {
        mavenCentral()
        jcenter()
    }
    
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-aop'   //AOP 의존성 추가
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testImplementation 'org.assertj:assertj-core:3.12.2'
    }

     이후 Control + Shift + 0으로 gradle 변경사항을 적용한 후 build가 끝나면 아래 그림과 같이 AOP 관련 의존성이 잘 들어왔는지 확인해 봅니다.

     

    The dependency of Spring AOP

     

     

    AOP 의존성이 문제없이 들어왔다면 지금부터 사용할 Spring AOP에서 동적으로 proxy bean을 생성해주기 때문에 이전에 만들었던 proxy class(ProxySimpleEventService.java)는  project에서 제거합니다. 

     

    Spring IoC에서 bean의 life cycle interface 중 하나인 BeanPostProcessor type의 bean(정확히는 BeanPostProcessor를 구현한 AbstractAutoProxyCreator class bean객체)을 사용해 AOP에 사용될 실제 객체를 대신하는 Proxy bean을 동적으로 생성하고 Container에 등록하는 작업을 해줍니다. 현재 예제를 빗대어 설명하자면 SimpleEventService class가 bean으로 등록되면 Spring이 AbstractAutoProxyCreator라는 BeanPostProcessor를 사용해서 실제 객체인 SimpleEventProcessor를 감싸는 proxy bean을 만들어 Container에 SimpleEventService bean 대신 등록합니다.

     

    우선 다음과 같이 Aspect를 정의하기 위한 class(PerfAspect.java)를 선언합니다.

    @Component  //	①
    @Aspect //	②
    public class PerfAspect {
    
        //해야할 일 (Advice)
        @Around("execution(* org.spring..*.SimpleEventService.*(..))")     //	③
        public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {    //	④
            long begin = System.currentTimeMillis();
            Object retVal = pjp.proceed();  //	⑤
            System.out.println(System.currentTimeMillis() - begin);
            return retVal;  //	⑥
        }
    
    }
    

    ① 이 proxy bean을 실제 bean 대신 container에 등록해야 하므로 @Component를 붙여줍니다.

     

    ② Aspect class임을 명시합니다. 이 annotation이 적용된 bean을 aspect로 사용하고 별도로 설정 file에 advice와 pointcut을 명시하지 않고도 advice를 적용해줄 수 있습니다.

     

    ③ @Around annotation으로 around adivce를 적용할 pointcut을 지정합니다. annotation의 value로 pointcut 이름을 주거나 혹은 직접 pointcut을 정의할 수 있습니다. 직접 pointcut을 정의할 때는 pointcut 표현식을 사용합니다. pointcut 표현식은 "execution"으로 시작합니다. 현재 예제 project를 예로 들어보겠습니다. value에 String 형태로 execution(* org.spring..*.SimpleEventService.*(..) 라고 지정하면 org.spring package 밑에 있는 모든 class 중에서 SimpleEventSerivce class 안에 있는 모든 method에 대해 pointcut을 적용하게 되는 것입니다.

     

    ProceedingJoinPoint는 advice가 적용되는 대상을 가리킵니다. 위의 예제에서 createEvnet(), publishEvent(), deleteEvent() method가 이에 해당합니다.

     

    ⑤ proceed()를 사용하여 pointcut에 해당하는 method를 실행하고 그 결과 값을 Object type의 객체에 넣어줍니다. proceed() 사용 시 오류가 발생할 수 있어 Throwable을 던져야 합니다.

     

    ⑥ target의 method를 호출 결과 값을 return 합니다.

     

     

     

    위의 코드를 실행하면 deleteEvent() method를 포함하여 SimpleEventService class의 모든 method를 pointcut으로 잡아 성능 측정을 진행합니다.

     

     

    위의 결과를 보면 @Around annotation의 value에 표현식이 SimpleEventService class 안에 있는 모든 method를 pointcut으로 잡도록  작성되어 있기 때문에 성능을 측정하지 않아야 하는 deleteEvent() method도 pointcut으로 포함되어버린 것입니다. 표현식 대신 annotation 기반으로 작성하면 pointcut을 유연하게 적용할 수 있습니다. 우선 다음과 같이 pointcut을 지정하기 위한 annotation을 하나 정의하도록 하도록 합니다.

    @Retention(RetentionPolicy.CLASS)   //	①
    @Target(ElementType.METHOD) //	②
    @Documented //	③
    public @interface PerfLogging {
    }
    

    ① SOURCE(in CompileTime) < CLASS(in LoadTime) < RUNTIME(in Runtime),  pointcut 지정에 사용할 annotation은 CLASS 이상의 RetentionPolicy를 지정해야 합니다.

     

    ② method에 적용하는 annotation임을 나타냅니다. pointcut은 method 호출할 때의 join point이므로 @Target annotation의 value를 ElementType.METHOD로 지정합니다.

     

    ③ JavaDoc으로 api 문서를 만들 때 annotation에 대한 설명도 포함하도록 합니다. JavaDoc이란 /***/로 감싸인 주설을 HTML 형태의 문서로 만들어주는 도구입니다.

     

     

     

    Pointcut annotation을 만들었으니 이제 aspect class에 적용했던 표현식 대신 annotation을 사용하여 pointcut을 지정합니다.

    @Component
    @Aspect
    public class PerfAspect {
    
        @Around("@annotation(PerfLogging)")	//	①
        public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
            long begin = System.currentTimeMillis();
            Object retVal = pjp.proceed();
            System.out.println(System.currentTimeMillis() - begin);
            return retVal;
        }
    }

    ① @Around annotation의 value로 pointcut annotation을 적용합니다.

     

     

     

    실제 객체(SimpleEventService bean)의 class에서 pointcut을 지정할 method에 pointcut annotation을 붙여줍니다.

    @Service
    public class SimpleEventService implements EventService {
    
        @Override
        @PerfLogging	// PointCut
        public void createEvent() {
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Create an Event");
        }
    
        @Override
        @PerfLogging	// PointCut
        public void publishEvent() {
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Publish an Event");
        }
    
        @Override
        @PerfLogging
        public void deleteEvent() { System.out.println("Delete an Event"); }
    }

     

     

     

    원래 예제 의도대로 createEvent()와 publishEvent()만 pointcut으로 지정하여 성능을 logging 하도록 하였습니다. project를 실행해보면 다음과 같이 의도했던 결과를 확인해볼 수 있습니다.

    result

     

    각 method를 pointcut으로 지정하기 위해 적용하는 pointcut annotation들은 논리 연산자 &&, ||, !과 함께 사용할 수 있으며 따라서 여러 가지 pointcut annotation을 조합하여 사용할 수도 있습니다. 

     

    execution advice를 여러 개 만들고 중복되는 method를 extract method로 빼내면 됩니다.

     

     

    다음과 같이 pointcut의 지정은 bean단위로도 가능합니다. @Around annotation의 value로 특정 bean을 주면 bean이 가진 모든 public method를 pointcut으로 지정하게 됩니다. 

    @Component
    @Aspect
    public class PerfAspect {
    
        @Around("bean(simpleEventService)")	//	①
        public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
            long begin = System.currentTimeMillis();
            Object retVal = pjp.proceed();
            System.out.println(System.currentTimeMillis() - begin);
            return retVal;
        }
    }

    ① simpleEventService bean의 모든 public method를 pointcut으로 지정합니다. 지정할 bean의 이름은 smallcase로 시작하는 class 이름입니다.

     

     

     

    세밀한 pointcut 지정이 필요 없는 경우는 @Around 대신 @Before annotation으로도 충분히 사용할 수 있습니다. @Before는 @Around와 마찬가지로 pointcut을 지정할 value로 execution 표현식, annotation, bean을 모두 사용할 수 있습니다.

    @Component
    @Aspect
    public class PerfAspect {
    
        @Around("bean(simpleEventService)")
        public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
            long begin = System.currentTimeMillis();
            Object retVal = pjp.proceed();
            System.out.println(System.currentTimeMillis() - begin);
            return retVal;
        }
    
        @Before("execution(* org.spring..*.SimpleEventService.*(..))")
        //@Before("@annotation(PerfLogging)")
        //@Before("bean(simpleEventService)")
        public void hello() {
            System.out.println("Hello");
        }
    }

     

    보다 다양한 advice 사용법은 Spring reference를 통해 확인해보세요.

     

     

     

     

     

     

     

     

    댓글

Designed by Tistory.