Keep going

AOP라는 패러다임 본문

Records/Spring Framework

AOP라는 패러다임

코딩천재홍 2021. 3. 22. 04:18

 

  • AOP는 흔히 '관점 지향 프로그래밍' 이라는 용어로 번역되는데, 이때 관점 이라는 용어는 개발자들에게는 관심사라는 말로 통용된다.
  • 관심사는 개발 시 필요한 고민이나 염두에 두어야 하는 일이라고 생각할 수 있는데, "파라미터가 올바르게 들어왔는지" "이 작업을 하는 사용자가 적절한 권한을 가진 사용자인지", "이 작업에서 발생할 수 있는 예외는 어떻게 처리해야 하는지" 등이 있다.
  • 핵심 로직은 아니지만 , 코드를 온전하게 만들기 위한 고민들로, 전통적인 방식에선 개발자가 반복적으로 이 고민을 코드에 반영한다.
  • AOP는 이러한 고민을 다른 방식으로 접근하는데. 바로 "관심사의 분리"이다. AOP는 개발자가 염두에 두어야 할 것들을 관심사로 분리하고, 핵심 비즈니스 로직만을 작성할 것을 권장한다.
  • AOP는 과거에 개발자가 작성했던, 관심사 + 비즈니스 로직을 분리해서 별도의 코드로 작성하도록 하고, 실행할 때 이를 결합하는 방식으로 접근한다.
  • 개발자가 작성한 코드와 분리된 관심사를 구현한 코드를 컴파일 혹은 실행 시점에 결합시킨다.
  • 실제 실행은 결합된 상태의 코드가 실행되기 때문에 개발자들은 핵심 비즈니스 로직에만 근거해서 코드를 작성하고, 나머지는 어떤 관심사들과 결합할 것인지를 설정한 것만으로 모든 개발을 마칠 수 있다.

 


18.1 AOP 용어들

  • AOP는 기존의 코드를 수정하지 않고, 원하는 기능들과 결합할 수 있는 패러다임

  • Target : 개발자가 작성한 핵심 비즈니스 로직을 가지는 객체
    • 순수한 비즈니스 로직을 의미하고, 어떠한 관심사들과도 관계를 맺지 않는다.
    • 순수한 코어라고 볼 수 있다.
  • Proxy : Target을 전체적으로 감싸고 있는 존재
    • Proxy는 내부적으로 Target을 호출하지만, 중간에 필요한 관심사들을 거쳐서 Target을 호출하도록 자동 혹은 수동으로 작성된다.
    • 직접 코드를 통해 구현하는 경우도 있지만, 대부분 스프링 AOP의 기능 이용해서 자동으로 생성되는 방식 이용.
  • JoinPoint : Target 객체가 가진 메서드
    • 외부에서의 호출은 Proxy 객체를 통해서 Target 객체의 JoinPoint를 호출하는 방식

 

Advice와 JoinPoint의 관계

  • JoinPoint는 Target이 가진 여러 메서드.
  • Target에는 여러 메서드가 존재하기 때문에 어떤 메서드에 관심사를 결합할 것인지를 결정해야 하는데 이 결정을 Pointcut 이라고 한다.
    • Pointcut : 관심사와 비즈니스 로직이 결합되는 지점을 결정
    • Aspect : 관심사 자체를 의미하는 추상 명사
    • Advice : Aspect를 구현한 코드 (실제 걱정거리를 분리해 놓은 코드)

 

구분 설명
Before Advice Target의 JoinPoint를 호출하기 전에 실행되는 코드이다. 코드의 실행 자체에는 관여할 수 없다.
After Returning Advice 모든 실행이 정상적으로 이루어진 후에 동작하는 코드이다.
After Throwing Advice 예외가 발생한 뒤에 동작하는 코드이다.
After Advice 정상적으로 실행되거나 예외가 발생했을 때 구분 없이 실행되는 코드이다.
Around Advice 메서드의 실행 자체를 제어할 수 있는 가장 강력한 코드이다. 직접 대상 메서드를 호출하고 결과나 예외를 처리할 수 있다.

Advice는 과거의 스프링에서 별도의 인터페이스로 구현되고, 이를 클래스로 구현하는 방식으로 제작했으나 스프링 3버전 이후에는 어노테이션만으로도 모든 설정이 가능하다.

 

Target에 어떤 Advice를 적용하는 방법

- XML을 이용한 설정

- 어노테이션을 이용하는 방법

 

 

 

구분 설명
execution(@execution) 메서드를 기준으로 Pointcut을 설정한다.
within(@within) 특정한 타입을 기준으로 Pointcut을 설정한다.
this 주어진 인터페이스를 구현한 객체를 대상으로 Pointcut을 설정한다.
args(@args) 특정한 파라미터를 가지는 대상들만을 Pointcut을 설정한다.
@annotation 특정한 어노테이션이 적용된 대상들만을 Pointcut으로 설정한다.

Pointcut은 Advice를 어떤 JoinPoint에 결합할 것인지를 결정하는 설정.

AOP에서 Target은 결과적으로 Pointcut에 의해서 자신에게는 없는 기능들을 가지게 된다.

 

 


18.2 AOP 실습

  • AOP 기능은 주로 일반적인 Java API를 이용하는 클래스들에 적용한다.
  • (아래 실습에서는 서비스 계층에 AOP를 적용, AOP의 예제는 ①서비스 계층 메서드 호출 시 모든 파라미터들을 로그로 기록, ②메서드들의 실행 시간을 기록 )

 

  • 예제 프로젝트 생성

    • AOP는 AspectJ라는 라이브러리 도움을 많이 받기 때문에 스프링 버전을 고려하여 AspectJ의 버전 역시 1.9.0 으로 버전을 높여준다.
    • AOP 설정과 관련해서 가장 중요한 라이브러리는 AspectJ Weaver라는 라이브러리이다. 스프링은 AOP 처리가 된 객체를 생성할 때 AspectJ Weaver 라이브러리의 도움을 받아서 동작한다.

 

  • AspectJweaver 라이브러리
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${org.aspectj-version}</version>
        </dependency>
cs

 

 

 

  • 서비스 계층 설계

    • 예제로 사용할 객체는 SampleService 인터페이스의 doAdd() 메서드를 대상으로 진행한다.
    • SampleServiceImpl 을 작성할 때는 반드시 @Service라는 어노테이션을 추가해서 스프링에서 빈으로 사용될 수 있도록 설정한다.

 

  • SampleService 인터페이스
package org.zerock.service;
 
public interface SampleService {
    public Integer doAdd(String str1, String str2)throws Exception;
}
cs

 

 

  • 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);
    }
 
}
 
cs

 

 

 

  • Advice 작성

    • log.info( )를 이용해서 로그를 기록해 오던 부분을 제외했다.
    • 지금까지 해왔던 수많은 로그를 기록하는 일은 '반복적이면서 핵심 로직도 아니고, 필요하기는 한' 기능이기 때문에 '관심사'로 간주할 수 있다.

 

  • LogAdvice - 로그를 기록해주는 관심사
package org.zerock.aop;
 
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
 
import lombok.extern.log4j.Log4j;
 
@Aspect
@Log4j
@Component
public class LogAdvice {
 
    @Before( "execution(* org.zerock.service.SampleService*.*(..))")
    public void logBefore() {
        
        log.info("====================");
    }
}
 
cs

- @Aspect : 해당 클래스의 객체가 Aspect를 구현한 것임으로 나타내기 위해서 사용

- @Component는 AOP와는 관계가 없지만 스프링에서 빈으로 인식하기 위해 사용

- logBefore( )는 @Before 어노테이션을 적용하고 있다. @Before는 BeforeAdvice를 구현한 메서드에 추가한다.

- Advice와 관련된 어노테이션들은 내부적으로 Pointcut을 지정한다. 별도의 @Pointcut으로 지정해서 사용할 수도 있다.

- @Before 내부의 execution 문자열은 AspectJ의 표현식이다. execution의 경우 접근제한자와 특정 클래스의 메서드를 지정할 수도 있다.

 

 


18.3 AOP 설정

  • 스프링 프로젝트에 AOP를 설정하는 것은 스프링 2버전 이후에는 간단히 자동으로 Proxy 객체를 만들어주는 설정을 추가해주면 된다.

 

  • root-context.xml
<?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:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    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>
    <context:component-scan base-package="org.zerock.service">
    </context:component-scan>
    
    <context:component-scan base-package="org.zerock.aop">
    </context:component-scan>
    
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
        
</beans>
 
cs

- <component-scan>을 이용해서 'org.zerock.service' 패키지와 'org.zerock.aop' 패키지를 스캔한다. 이 과정에서 SamplerServiceImpl 클래스와 LogAdvice는 스프링의 빈으로 등록될 것이다.

- <aop:aspectj-autoproxy>를 이용해서 LogAdvice에 설정한 @Before가 동작하게 된다.

 

 


18.4 AOP 테스트

  • 정상적인 상황이라면 SampleServiceImpl, LogAdvice는 같이 묶여서 자동으로 Proxy 객체가 생성된다.

 

 

  • SampleServiceTests 클래스
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.Log4j;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
//Java Config
//@ContextConfiguration(classes={org.zerock.config.RootConfig.class, org.zerock.config.ServletConfig.class})
@Log4j
public class SampleServiceTests {
    @Setter(onMethod_=@Autowired)
    private SampleService service;
    
    @Test
    public void testClass() {
        log.info(service);
        log.info(service.getClass().getName());
    }
}
cs

- AOP 설정을 한 Target에 대해서 Proxy 객체가 정상적으로 만들어져 있는지를 확인하는 것이다.

- <aop:aspectj-aitoproxy>가 정상적으로 모든 동작을 하고, LogAdvice에 설정 문제가 없다면 service 변수의 클래스는 단순히 org.zerock.service.SampleServiceImpl의 인스턴스가 아닌 생성된 Proxy 클래스의 인스턴스가 된다.

 

 

 

  • SampleServiceTests 클래스 - testAdd( ) 메서드
@Test
    public void testAdd() throws Exception {
        log.info(service.doAdd("123""456"));
    }
cs

 

 

 

 

  • args를 이용한 파라미터 추적

    • LogAdvice가 SampleService의 doAdd( )를 실행하기 직전에 간단한 로그를 기록하지만, 상황에 따라서는 해당 메서드에 전달되는 파라미터가 무엇인지 기록하거나, 예외가 발생했을 때 어떤 파라미터에 문제가 있는지 알고 싶은 경우도 많다.
    • LogAdvice에 적용된 @Before 은 어떤 위치에 Advice를 적용할 것인지를 결정하는 Pointcut인데 설정 시에 args를 이용해 간단히 파라미터를 구할 수 있다.

 

 

  • LogAdvice 클래스
@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);
    }
cs

-logBeforeWithParam()에서는 'execution'으로 시작하는 Pointcut 설정에 doAdd( ) 메서드를 명시하고, 파라미터의 타입을 지정했다.

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

 

 

 

  • @AfterThrowing

    • 파라미터의 값이 잘못되어서 예외가 발생하는 경우가 많다.
    • AOP의 @AfterThrwoing 어노테이션은 지정된 대상이 예외를 발생한 후에 동작하면서 문제를 찾을 수 있도록 도와줄 수 있다.
@AfterThrowing(pointcut="execution(* org.zerock.service.SampleService*.*(..))",
            throwing="exception")
    public void logException(Exception exception) {
        log.info("Exception....!!!");
        log.info("exception : " + exception);
    }
cs

 logException에 적용된 @AfterThrowing은 pointcut과 throwing 속성을 지정하고 변수 이름을 exception으로 지정한다.

 

 

 

  • SampleServiceTests - 에러 발생할 테스트 코드
@Test
    public void testAddError() throws Exception {
        
        log.info(service.doAdd("123","ABC"));
    }
cs

 

 


  • @Around와 ProceedingJoinPoing

    • @Around는 조금 특별하게 동작하는데 직접 대상 메서드를 실행할 수 있는 권한을 가지고 있고, 메서드의 실행 전과 실행 후에 처리가 가능하다.
    • ProceedingJoinPoint는 @Around와 같이 결합해서 파라미터나 예외 등을 처리할 수 있다.

 

 

  • LogAdvice 클래스
@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 result = null;
        try {
            result = pjp.proceed();
        } catch(Throwable e) {
            e.printStackTrace();
        }
        
        long end = System.currentTimeMillis();
        log.info("TIME : " + (end + start));
        return result;
    }
cs

- logTIme의 Pointcut 설정은 SampleService*.*(..)로 지정한다.

- logTime은 특별하게 ProceedingJoinPoint 파라미터를 지정하는데 이는 AOP의 대상이 되는 Target이나 파라미터를 파락할 뿐 아니라 직접 실행을 결정할 수도 있다.

- @Before 등과 달리 @Around가 적용되는 메서드의 경우 리턴 타입이 void 아닌 타입으로 설정하고 메서드 실행 결과 역시 직접 반환하는 형태로 작성해야 한다.

 

 


출처 : 코드로 배우는 스프링 웹 프로젝트 [구멍가게 코딩단]

'Records > Spring Framework' 카테고리의 다른 글

댓글과 댓글 수에 대한 처리  (0) 2021.03.26
스프링에서 트랜잭션 관리  (0) 2021.03.22
Ajax 댓글 처리  (0) 2021.03.18
REST 방식으로 전환  (0) 2021.03.16
검색처리  (0) 2021.03.15
Comments