AOP(Aspect Oriented Programming)이란?

  AOP는 흔히 '관점 지향 프로그래밍'이라는 용어로 번역된다.

  AOP가 추구하는 것은 '관심사의 분리(separate concerns)'다.

  개발자가 염두에 두어야 하는 일들은 별도의 '관심사'로 분리하고, 핵심 비즈니스 로직만을 작성할 것을 권장한다.

  관심사라는 것을 예를 들면 나눗셈을 구현한다고 했을 때 핵심로직은 두개의 숫자를 나누는 것이지만,

  주변 로직은 0을 나누는 것이 아닌지 등을 체크하는 것이다. 관심사는 이런 중요한 로직은 아니지만 사전 조건이나

  사후 조건등이라고 간주할 수 있다.

 

  AOP는 과거에 개발자가 작성했던 관심사 + 비즈니스 로직을 분리해서 별도의 코드로 작성하도록 하고,

  컴파일 혹은 실행시점에 이를 결합하는 방식으로 접근한다.

  실제 실행은 결합된 상태의 코드가 실행되기 때문에 개발자들은 핵심 비즈니스 로직에만 근거해서 코드를 작성하고,

  나머지는 어떤 관심사들과 결합할 것인지를 설정하는 것 만으로 모든 개발을 마칠 수 있게 된다.

  예를들어 AOP를 이용하면 작성된 모든 메서드의 실행 시간이 얼마인지를 기록하는 기능을 기존 코드의 수정 없이도

  작성할 수 있고, 잘못된 파라미터가 들어와서 예외가 발생하는 상황을 기존 코드의 수정 없이도 제어할 수 있다.

  스프링이 AOP를 지원한다는 것이 스프링의 가장 중요한 특징 중에 하나로 말하게 된 이유 역시 별도의 복잡한

  설정이나 제약없이 스프링 내에서 간편하게 AOP의 기능들을 구현할 수 있기 때문이다.

 

AOP의 용어들

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

  AOP를 구현하기 위해서는 다음과 같은 핵심적인 그림들을 이해할 필요가 있다.

  개발자의 입장에서 AOP를 적용한다는 것은 기존의 코드를 수정하지 않고도 원하는 관심사(Cross-Concern)들을

  엮을 수 있다는 것이다.

  위의 그림에서 Target에 해당하는 것이 바로 개발자가 작성한 핵심 비즈니스 로직을 갖는 객체다.

 

  Target은 순수한 비즈니스 로직을 의미하고, 어떠한 관심사들과도 관계를 맺지 않는다.

  순수한 코어(core)라고 볼 수 있다. Target을 전체적으로 감싸고 있는 존재를 Proxy라고 한다.

  Proxy는 내부적으로 Target을 호출하지만, 중간에 필요한 관심사들을 거쳐서 Target을 호출하도록 자동 혹은

  수동으로 작성된다. Proxy의 존재는 직접 코드를 통해서 구현하는 경우도 있지만 대부분의 경우 스프링 AOP 기능을

  이용해서 자동으로 생성되는(Auto-Proxy)방식을 이용한다. JoinPoint는 Target객체가 가진 메서드다.

  외부에서의 호출은 Proxy객체를 통해서 Target 객체의 JoinPoint를 호출하는 방식이라고 이해할 수 있다.

 

  Joinpoint는 Target이 가진 여러 메서드라고 보면 된다. 엄밀하게 스프링 AOP에서는 메서드만이 JoinPoint가 된다.

  Target에는 여러 메서드가 존재하기 때문에 어떤 메서드에 관심사를 결합할 것인지를 결정해야 하는데

  이 결정을 'Pointcut'이라고 한다.

 

  Pointcut은 관심사와 비즈니스 로직이 결합되는 지점을 결정하는 것이다. 앞의 Proxy는 이 결합이 완성된 상태이므로

  메서드를 호출하게 되면 자동으로 관심사가 결합된 상태로 동작하게 된다.

 

  Advice는 실제 걱정거리를 분리해놓은 코드를 의미한다. Advice는 그 동작 위치에 따라 다음과 같이 구분된다.

구분

설명

Before Advice

TargetJoinPoint를 호출하기 전에 실행되는 코드다.

코드의 실행 자체에는 관여할 수 없다.

After Returning Advice

모든 실행이 정상적으로 이루어진 후에 동작하는 코드다.

After Throwing Advice

예외가 발생한 뒤에 동작하는 코드다.

After Advice

정상적으로 실행되거나 예외가 발생했을 때 구분없이 실행되는 코드다.

Around Advice

메서드의 실행 자체를 제어할 수 있는 가장 강력한 코드다.

직접 대상 메서드를 호출하고 결과나 예외를 처리할 수 있다.

  Advice는 별도의 인터페이스로 구현되고 이를 클래스로 구현하는 방식으로 제작했으나 Spring 3버전 이후에는

  어노테이션만으로도 모든 설정이 가능하다.

  Target에 어떤 Advice를 적용할 것인지는 XML을 이용한 설정을 이용할 수 있고, 어노테이션을 이용하는 방식을

  이용할 수 있다.

 

  Pointcut은 Advice를 어떤 JoinPoint에 결합할 것인지를 결정하는 설정이다. AOP에서 Target은 결과적으로

  Pointcut에 의해서 자신에게는 없는 기능들을 갖게 된다.

  다양한 형태로 선언해서 사용할 수 있는데 주로 사용되는 설정은 다음과 같다.

구분

설명

execution(@execution)

메서드를 기준으로 Pointcut을 설정한다.

within(@within)

특정한 타입(클래스)을 기준으로 Pointcut을 설정한다.

this

주어진 인터페이슬르 구현한 객체를 대상으로 Pointcut을 설정한다.

args(@args)

특정한 파라미터를 갖는 대상들만을 Pointcut으로 설정한다.

@annotation

특정한 어노테이션이 적용된 대상들만을 Pointcut으로 설정한다.

 

예제코드

  AOP를 위해 의존성을 추가한다.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 

  간단한 인터페이스를 구현하는 클래스를 빈으로 정의한다.

public interface EventService {
  
  public void created();
  
  public void operation();
  
  public void deleted();
}
import org.springframework.stereotype.Component;

@Component
public class SimpleServiceEvent implements EventService {
  
  @Override
  public void created() {
    long begin = System.currentTimeMillis();
    
    try {
      Thread.sleep(1000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("created");
    System.out.println(System.currentTimeMillis() - begin);
  }
  
  @Override
  public void operation() {
    long begin = System.currentTimeMillis();
    
    try {
      Thread.sleep(2000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("operation");
    System.out.println(System.currentTimeMillis() - begin);
  }
  
  @Override
  public void deleted() {
    System.out.println("deleted");
  }
}

  created()와 operation() 안에는 수행시간을 측정하는 코드를 담고 있다.

  Runner클래스를 만들어 이 클래스를 활용하도록 한다.

@Component
public class AppRunner implements ApplicationRunner {
  
  @Autowired
  EventService eventService;
  
  @Override
  public void run(ApplicationArguments args) throws Exception {
    eventService.created();
    eventService.operation();
    eventService.deleted();
  }
}

  결과값은

  created

  1013

  operation

  2007

  deleted

  이렇게 출력된다. 물론 값은 계속 차이가 있을 수 있다.

 

  execution expression

  이제 이것을 AOP를 이용하여 개선한다.

  위 코드에서는

long begin = System.currentTimeMillis();
System.out.println(System.currentTimeMillis() - begin);

 

  이 코드들이 반복해 여러곳에서 사용되고 있다.

  이 코드는 Aspect로 묶어서 관리되는 것이 편한것이다.

 

  Aspect클래스는 다음과 같이 정의한다.

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


@Component
@Aspect
public class PerfAspect {
  
  @Around("execution(* com.example..*.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 로 Aspect클래스임을 정의하고 Advice를 정의한다.

  Pointcut 시점은 @Around 형태로 정의한다.

  execution의 경우는 com.example밑에 있는 모든 클래스 중 EventService 안에 들어 있는 모든 메서드에 이 행위를

  적용하라는 것이다.

  이부분이 execution expression이라고 한다. Pointcut을 설정하는 부분이다.

 

  AOP를 적용했으니 적용될 클래스에서 Aspect로 대체된 부분은 삭제해준다.

import org.springframework.stereotype.Component;

@Component
public class SimpleServiceEvent implements EventService {
  
  @Override
  public void created() {
  
    try {
      Thread.sleep(1000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("created");
  }
  
  @Override
  public void operation() {
    
    try {
      Thread.sleep(2000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("operation");
  }
  
  @Override
  public void deleted() {
    System.out.println("deleted");
  }
}

  다시 실행하면 결과값은

  created

  1006

  operation

  2013

  deleted

  0

  이렇게 출력된다.

 

  여기서의 문제는 created()와 operation()에만 Aspect가 적용되었어야 했는데 deleted()에도 적용이 되었다.

  이를 어노테이션 기반 Advice 정의로 해결한다.

  

  일단 다음과 같은 어노테이션을 만들어야 한다.

import java.lang.annotation.*;

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

  그리고 Aspect 클래스를 다음과 같이 수정한다.

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


@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;
  }
}

  execution expression을 어노테이션으로 대체한 것이다.

  이제 적용될 클래스의 메서드에 @PerfLogging을 붙여야 한다.

import org.springframework.stereotype.Component;

@Component
public class SimpleServiceEvent implements EventService {
  
  @PerfLogging
  @Override
  public void created() {
  
    try {
      Thread.sleep(1000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("created");
  }
  
  @PerfLogging
  @Override
  public void operation() {
    
    try {
      Thread.sleep(2000);
    }catch(InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("operation");
  }
  
  @Override
  public void deleted() {
    System.out.println("deleted");
  }
}

  결과값은

  created

  1011

  operation

  2001

  deleted

  이렇게 deleted에 한번 더 출력되지 않고 정상적으로 출력된다.

 

 

레퍼런스

● 코드로 배우는 스프링 웹 프로젝트

● dailyheumsi.tistory.com/202

'Spring' 카테고리의 다른 글

JPA란?  (0) 2022.01.13
IOC(Inversion Of Control)  (0) 2021.02.21
의존성 주입(Dependency Injection, DI)  (0) 2021.02.18
Spring MVC (Front Controller Pattern)  (0) 2021.02.17
Spring MVC  (0) 2021.02.16

+ Recent posts