ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [5-ch18 Spring AOP] 설정, execution, args, @Around, @before, @AfterThrowing
    Back-End/Spring Legacy 2022. 9. 7. 14:50

     

    이전 포스팅에서 AOP 개념에 대한 설명을 적어봤다. 이번엔 AOP가 그래서 어떻게 사용되는건데?에 대한 궁금증을 풀어보려고 한다. 

     

    AOP기능은 주로 일반적인 Java API를 이용하는 클래스(POJO-Plain OldJava Object)들에 적용한다. Controller에 적용이 불가능한 것은 아니지만, Controller의 경우 인터셉터나 필터 등을 이용한다. 포스팅에 적어볼 예제에서는 서비스 계층에 AOP를 적용한다. 

     

     

    AOP 기능 구현 과정

     

    1. 타깃(Target) 클래스 지정 -> 핵심코드로 아래 예제에서는 SampleServiceImpl이 핵심 코드
    2. 어드바이스(Advice) 클래스 지정 ->  LogAdvice
    3. root-context.xml에서 빈 생성, aspectj 설정 

     

     

     

    1. 타깃 클래스 지정

     

    타깃으로 사용될 메인 클래스는 서비스이다. 서비스 인터페이스와 서비스 클래스를 생성해서 doAdd() 메서드를 대상으로 예제를 진행해볼까 한다. SampleServiceImpl 클래스는 단순히 문자열을 변환해서 더하기 연산을 하는 단순 작업으로 작성한다. 

     

    package org.zerock.service;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class SampleServiceImpl implements SampleService {
    
    	@Override
    	public Integer doAdd(String str1, String str2) throws Exception {
    		
    		return Integer.parseInt(str1) + Integer.parseInt(str2);
    	}
    
    }

     

     

     

    2. Adivce 작성

     

    위의 SampleServiceImpl를 살펴보면 기존에 코드 작성할 때 우리가 수기로 적었던 log.info() 등을 이용해 로그를 기록해 오던 부분이 빠진 걸 확인할 수 있다. 지금까지 로그 기록이란 '반복적이면서 핵심 로직이 아니고, 필요하기는 한' 기능이기 때문에 '관심사'로 간주할 수 있다. AOP의 개념에서 Advice는 '관심사'를 실제로 구현한 코드이므로 지금부터 로그를 기록해주는 LogAdvice를 설계한다.

     

    AOP 기능의 설정은 XML 방식이 있긴 하지만, 아래 예제에서는 어노테이션만 이용해 AOP관련 설정을 진행하려고 한다.

     

     

    <LogAdvice 클래스>

    package org.zerock.aop;
    
    import java.util.Arrays;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    
    import lombok.extern.log4j.Log4j2;
    
    @Aspect
    @Log4j2
    @Component
    public class LogAdvice {
    	
    	//excution : 메서드를 기준으로 Pointcut을 설정한다. 
    	@Before("execution(* org.zerock.service.SampleService*.*(..))")
    	public void logBefore() {
    		log.info("=========================");
    	}
    	
       
    }

     

    LogAdvice 클래스의 선언부에는 @Aspect 어노테이션이 추가되었다. @Aspect는 해당 클래스의 객체가 Aspect를 구현한 것임으로 나타내기 위해 사용한다. 

     

     

    ! 여기서 Aspect란  

    더보기

    Ponint Cut + Advice 구현하고자 하는 보조기능을 의미, 어드바이저라고도 불린다.

    여러 객체에 공통으로 적용되는 공통 관심 객체를 의미한다. 애스팩트 설정에 따라 AOP의 동작 방식이 결정

    앞에서 설명한 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화한다. (공통기능)

     

     

    @Component는 AOP와는 관계 없지만 스프링 빈(bean)으로 인식하기 위해서 사용한다. logBefore()는 @Before 어노테이션을 적용하고 있다. @Before는 BeforeAdvice를 구현한 메서드에 추가한다. @After, @AfterReturning, @AfterThrowing, @Around 역시 동일한 방식을 적용한다.

     

    Advice와 관련된 어노테이션들은 내부적으로 Pointcut을 지정한다. Pointcut은 별도의 @Pointcut으로 지정해서 사용할 수도 있다. @Before 내부의 'execution...'문자열은 AspectJ의 표현식이다. 'execution'의 경우 접근제한자와 특정 클래스의 메서드를 지정할 수 있다. 

     

     

    3. AOP 설정

     

    스프링 프로젝트에 AOP를 설정하는 것은 스프링 2버전 이후에는 간단히 자동으로 Proxy 객체를 만들어주는 설정을 추가해주면 된다.  프로젝트 root-context.xml을 선택해 네임스페이스에 'aop'와 'context'를 추가한다.

     

     

     

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:aop="http://www.springframework.org/schema/aop"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
    		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
    		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    	
    	<!-- Root Context: defines shared resources visible to all other web components -->
    	
    	
    	<context:annotation-config></context:annotation-config>
    	<!-- component-scan을 통해 스프링의 빈(객체)로 아래 패키지 클래스들이 등록된다. -->
    	<context:component-scan base-package="org.zerock.service"></context:component-scan>
    	<context:component-scan base-package="org.zerock.aop"></context:component-scan>
    	
    	<!-- LogAdvice에 설정한 advice가 동작하게 된다.  -->
    	<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    		
    </beans>

     

    root-context.xml에서는 <component-scan>을 이용해 'org.zerock.service' 패키지와 'org.zerock.aop' 패키지를 스캔한다. 이 과정에서 SampleServiceImpl 클래스와 LogAdvice는 스프링의 빈(객체)으로 등록될 것이고 <aop:aspectj-autoproxy>를 이용해 LogAdvice에 설정한 @Before가 동작하게 된다. 

     

     

     

     

    AOP 테스트

     

    <SampleServiceTests.java>

     

    아래 코드는 AOP 테스트에 대한 전체 코드이다. 

    package org.zerock.service;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import lombok.Setter;
    import lombok.extern.log4j.Log4j2;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
    @Log4j2
    public class SampleServiceTests {
    	
    	@Setter(onMethod_ = @Autowired)
    	private SampleService service;
    	
    	@Test
    	public void testClass() {
    		log.info(service);
    		log.info(service.getClass().getName());
    	}
    	
    	@Test
    	public void testAdd() throws Exception{
    		log.info(service.doAdd("123", "456"));
    	}
    	
    	@Test
    	public void testAddError() throws Exception{
    		log.info(service.doAdd("123", "ABC"));
    	}
    
    }

     

     

    1번 testClass를 살펴봤을 때 가장 먼저 작성해 봐야 하는 코드는 AOP 설정을 한 Target에 대해 Proxy 객체가 정상적으로 만들어져 있는지를 확인하는 것이다. <aop:aspectj-autoproxy></aop:aspectj-autoproxy> 정상적으로 동작하고 LogAdvice에 설정 문제가 없다면 service 변수의 클래스는 단순히 org.zerock.service.SampleServiceImpl의 인스턴스가 아닌 생성된 Proxy 클래스의 인스턴스가 된다.

     

     

     

     

    Test 코드에서 testAdd 메서드를 살펴보면 service.doAdd에 인자값으로 String 문자열을 보내는 걸 확인 가능하다. 보낸 문자열을 Service에서 parseInt를 통해 int형으로 형변환하고 값을 더하는 로직을 볼 수 있다. 따라서 테스트를 실행했을 때 아래와 같이 로그가 기록된다. AOP의Target의 JoinPoint를 호출하기 전 실행되도록  @Before Advice를 설정해뒀으므로 log에 =======이 찍히는 걸 확인할 수 있다. 

     

     

     

     

     

    AOP 테스트 - args를 이용한 파라미터 추적

     

    LogAdvice가 SampleService의 doAdd()를 실행하기 직전에 간단한 로그를 기록하지만, 상황에 따라 해당 메서드에 전달되는 파라미터가 무엇인지 기록하거나, 예외가 발생했을 때 어떤 파라미터에 문제가 있는지 알고 싶은 경우도 많다. 따라서 args를 이용해 알아보려고 한다.

     

    //args : 파라미터를 알 수 있다. 그러나 다른 여러 종류의 메서드를 적용하는데는 간단하지 않다.
    //이런 문제를 해결하기 위해 @Around와 ProceedingJoinPoint를 이용할 예정
    @Before("execution(* org.zerock.service.SampleService*.doAdd(String,String)) && args(str1, str2)")
    public void logBeforeWithParam(String str1, String str2) {
        log.info("str1: " + str1);
        log.info("str2: " + str2);
    
    }

     

    logBeforeWithParam()에서는 'execution'으로 시작하는 Pointcut 설정에 doAdd() 메서드를 명시하고, 파라미터의 타입을 지정했다. 뒤쪽의 '&& args(..)' 부분에는 변수명을 지정한다. 

     

     

     

     

    '&& args'를 이용하는 설정은 간단히 파라미터를 찾아서 기록할 때는 유용하지만 파라미터가 다른 여러 종류의 메서드에는 간단하지 않다는 단점이 있다.

     

     

     

    AOP 테스트 - @AfterThrowing

     

    코드를 실행하다 보면 파라미터의 값이 잘못되어 예외가 발생하는 경우가 많다. 그럴 때 AOP의 @AfterThrowing 어노테이션은 지정된 대상이 예외를 발생한 후에 동작하면서 문제를 찾을 수 있도록 도와줄 수 있다.

     

    @AfterThrowing(pointcut = "execution(* org.zerock.service.SampleService*.*(..))", throwing = "exception")
    public void logException(Exception exception) {
        log.info("Exception!!!!!!");
        log.info("exception: " + exception);
    }

     

    logException()에 적용된 @AfterThrowing은 'pointcut'과 'throwing' 속성을 지정하고 변수 이름을'exception'으로 지정한다. 테스트 코드에서 고의적으로 예외가 발생하도록 작성해 테스트해본다.

     

     

     

     

    @Around와 ProceedingJoinPoint

     

    @Around는 조금 특별하게 동작하는데 직접 대상 메서드를 지정할 수 있는 권한을 가지고 있다. 메서드의 실행 전과 실행 후 처리가 가능하다.

     

    @Around("execution(* org.zerock.service.SampleService*.*(..))")
    	public Object logTime(ProceedingJoinPoint pjp) {
    		
    		long start = System.currentTimeMillis();
    		
    		log.info("Target: " + pjp.getTarget());
    		log.info("Param: " + Arrays.toString(pjp.getArgs())) ;
    		
    		//invoke method
    		Object resultObject = null;
    		
    		try {
    			resultObject = pjp.proceed();
    		}catch(Throwable e) {
    			e.printStackTrace();
    		}
    		
    		long end = System.currentTimeMillis();
    		log.info("TIME: " +(end-start));
    		return resultObject;
    		
    	}

     

     

    logTime()의 Pointcut 설정은 위와 같이 지정하고, 특별하게 ProceedingJoinPoint라는 파라미터를 지정하는데 ProceedingJoinPoint는 AOP의 대상이 되믄 Target이나 파라미터 등을 파악할 뿐만 아니라, 직접 실행을 결정할 수도 있다. @Before 등과 달리 @Around가 적용되는 메서드의 경우 리턴 타입이 void가 아닌 타입으로 설정하고, 메서드의 실행 결과 역시 직접 반환하는 형태로 작성해야 한다.

     

     

     

    반응형

    댓글

Designed by Tistory.