4. AOP


AOP란

  • Aspect-Oriented Programming / 애스펙트 지향 프로그래밍
  • 애플리케이션 전체에 걸쳐 사용되는 기능을 재사용할 수 있는 컴포넌트에 담을 수 있게 해줌
  • 기존 비즈니스 로직에 영향을 주지 않고 필요한 추가처리를 곳곳에 넣을 수 있는 개발 기법
    • 인증이나 로깅 등
  • 소프트웨어 시스템 내부의 관심사들을 서로 분리하는 기술
    • 시스템은 보통 특정한 기능을 책임지는 여러개의 컴포넌트로 구성된다
    • 각 컴포넌트는 특정 기능 외에 로깅이나 트랜잭션 관리, 보안등의 시스템 서비스도 수행해야 하는 경우가 있다
    • 이러한 시스템 서비스는 여러 컴포넌트에 관련되는 경향이 있다 (횡단관심사)
    • 이러한 관심사가 여러 컴포넌트에 퍼지게 되면, 중복되는 코드가 나타난다
  • 애플리케이션 코드에 산재해서 나타나는 부가적인 기능을 독립적으로 모듈화
    • 시스템 서비스를 모듈화해 컴포넌트에 선언적으로 적용 가능
    • 게시판 기능을 만들 때
    • 게시글 쓰기나 목록읽기와 같은 핵심 로직을 구현할 때
    • 트랜잭션 적용이나 보안검사같은 공통 기능 코드를 핵심로직에 삽입할 필요가 없다
    • AOP라이브러리가 공통기능을 알맞게 삽입해주기 때문

스프링에서의 AOP

  • 메서드의 실행시간을 출력할 때
    • AOP 미적용 시
      public void write(Article article) {
      long start = System.currentTimeMillis();
      try {
      System.out.println("articleServiceImpl2.write() 메서드 호출");
      articleDao.insert(article);
      } finally {
      long finish = System.currentTimeMillis();
      long time = finish - start;
      System.out.println("수행시간: " + time + "밀리초");
      }
      }
      
    • AOP 적용 시
      package mysite.spring.di;
      import org.apache.commons.logging.Log;
      import org.apache.commons.logging.LogFactory;
      import org.aspectj.lang.ProceedingJoinPoint;
      import org.springframework.util.StopWatch;
      public class LoggingAspect {
      private Log log = LogFactory.getLog(getClass());
      public Object logging(ProceedingJoinPoint joinPoint) throws Throwable {
      log.info("기록 시작 : " + joinPoint.toString());
      long start = System.currentTimeMillis();
      try {
        Object retValue = joinPoint.proceed();
        return retValue;
      } finally {
        long finish = System.currentTimeMillis();
        long time = finish - start;
        log.info("기록 종료");
        log.info(joinPoint.getSignature().getName() + "메서드 실행 시간 : "+ time + "밀리초");
      }
      }
      }
      
  • logging()메서드는 Aspect가 적용되는 메서드의 실행시간을 구한 뒤 Log를 통해 출력하도록 구현된다
  • JoinPoint : 공통 관심사항이 적용될 수 있는 지점 / 메서드 호출 시, 객체 생성 시 등
  • ProceddingJoinPoint : Aspect가 적용되는 객체 및 메서드에 대한 정보를 담고 있다

AOP 설정

  • 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:aop="http://www.springframework.org/schema/aop"
  xsi:schemaLocation="http://www.springframework.org/schema/beans 
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/aop
  http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

  <bean id="loggingAsp" class="mysite.spring.di.LoggingAspect" />
  <aop:config>
    <aop:pointcut id="servicePointcut" expression="execution(* *..*Service.*(..))" />
    <aop:aspect id="loggingAspect" ref="loggingAsp">
      <aop:around pointcut-ref="servicePointcut" method="logging" />
    </aop:aspect>
  </aop:config>
</beans> 
  • loggingAspect의 logging()메서드를 이름이 Service로 끝나는 인터페이스를 구현한 모든 클래스의 모든 메서드 앞, 뒤로 실행
package com.spring.di;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainForAop {
  public static void main(String[] args) {
    String configLocations = "applicationContext.xml";
    ApplicationContext context = new ClassPathXmlApplicationContext(configLocations);
    ArtService articleService = (ArtService) context.getBean("articleService3");
    articleService.insert(new ArticleBean());
  }
}

  • BeanFactory 대신 ApplicationContext 사용
  • 스프링 설정파일로 commonConcern.xml 추가
  • 스프링은 프록시를 이용해 AOP를 구현
  • MainForAOP클래스가 사용하는 객체는 ArticleServiceImpl 객체가 아니라 스프링이 런타임에 생성한 프록시 객체가 된다
    • 프록시가 내부적으로 다시 logging()메서드를 호출하여 ArticleServiceImpl 객체에 적용된다

용어 및 기타 사항

  • Advice - 언제 공통관심기능을 핵심로직에 적용할지를 정의
  • Joinpoint - Advice를 적용가능한 시점을 의미
  • Pointcut - Joinpoint의 부분집합으로서 실제로 Advice가 적용되는 Joinpoint를 나타낸다
  • Weaving - Advice를 핵심 로직코드에 적용하는 것
  • Aspect - 여러 객체에 공통으로 적용되는 공통 관심 사항
  • Advice 종류
    • Before Advice : 대상 객체의 메서드 호출 전에 공통 기능 실행
    • After Returning Advice : 대상 객체의 메서드가 예외 없이 실행한 이후에 공통 기능 실행
    • After Throwing Advice : 대상 객체의 메서드를 실행하는 도중 예외가 발생한 경우에 공통 기능 실행
    • After Advice : 대상 객체의 메서드를 실행하는 도중에 예외가 발생했는지의 여부와 상관없이 메서드 실행 후 공통 기능 실행
    • Around Advice : 대상객체의 메서드 실행 전, 후, 또는 예회 발생 시점에 공통 기능 실행
  • Advice 정의 태그
    • aop:before : 메서드 실행 전에 적용되는 Advice 정의
    • aop:after-returning
    • aop:after-throwing
    • aop:after
    • aop:around
  • Pointcut 표현식

<aop:config>
  <aop:pointcut id="servicePointcut" expression="execution(* *..*Service.*(..))" />
  <aop:aspect id="loggingAspect" ref="loggingAsp">
    <aop:around pointcut-ref="servicePointcut" method="logging" />
  </aop:aspect>
</aop:config>

  • execution 명시자 : Advice를 적용할 메서드를 명시할 때 사용

    execution(수식어패턴?리턴타입패턴 클래스이름패턴?이름패턴(파라미터패턴)

    • 수식어패턴 : 생략 가능, public, protected 등
    • 리턴타입패턴 : 리턴타입 명시
    • 클래스이름패턴, 이름패턴 : 클래스이름 및 메서드 이름을 패턴으로 명시
    • 파라미터패턴 : 매칭될 파라미터에 대해 명시
      • : 모든 값을 표현
    • .. : 0개 이상이라는 의미

AOP 예시

  • 설정 xml

<bean name="loggingAsp" class="com.di.aop.LogingAspect"></bean>
<aop:config>
  <aop:pointcut expression="execution(* *..*Service.*(..))" id="servicePointCut"/>
  <aop:aspect id="logAspect" ref="loggingAsp">
    <aop:around method="logging" pointcut-ref="servicePointCut"/>
  </aop:aspect>
</aop:config>

<bean name="loggingDaoAsp" class="com.di.aop.LoggingDAOAspect"></bean>
<aop:config>
  <aop:pointcut expression="execution(* *..*DAO*.*(..))" id="daoPointCut"/>
  <aop:aspect id="logDaoAspect" ref="loggingDaoAsp">
    <aop:before method="before" pointcut-ref="daoPointCut"/>
    <aop:after-returning method="afterReturing" pointcut-ref="daoPointCut"/>
    <aop:after-throwing method="afterError" pointcut-ref="daoPointCut"/>
  </aop:aspect>
</aop:config>

  • Aspect
package com.di.aop;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
public class LoggingDAOAspect {
  private Log log = LogFactory.getLog(getClass());
  
  public void before(JoinPoint joinPoint) {
    log.info("[ 기록 시작, target="+ joinPoint.getTarget()+"]");
    log.info("메서드 - " + joinPoint.getSignature().getName());
    Object[] argsArr=joinPoint.getArgs();
    for(int i=0;i<argsArr.length;i++) {
      log.info(i+"번째 매개변수 - " + argsArr[i]);
    }
  }
  
  public void afterReturing(JoinPoint joinPoint) {
    log.info("[ 기록 끝 target="+ joinPoint.getTarget()+"]");
  }
  
  public void afterError(JoinPoint joinPoint) {
    log.info("DAO error : joinPoint="+ joinPoint);
    for(Object obj : joinPoint.getArgs()) {
      log.info("DAO error : 매개변수 - " + obj);
    }
  }
}