스프링 AOP

de_sj_awa·2021년 6월 26일
0
post-custom-banner

1. 개념 소개

Aspect-oriendted Programming (AOP)은 OOP를 보완하는 수단으로, 흩어진 Aspect를 모듈화 할 수 있는 프로그래밍 기법이다.

흩어진 관심사(Crosscutting Concerns)

AOP를 적용하면?

ex) 트랜잭션, 로깅 ...

AOP 주요 개념

  • Aspect와 Target
  • Advice
  • Join point와 Pointcut

Advice는 해아할 일, Pointcut은 어디에 적용해야 하는지 Aspect는 Advice + Pointcut
Target은 위의 그림에서는 Class A, Class B, Class C 이다. JoinPoint는 끼어들 수 있는 모든 지점이다(생성자 호출, 메서드 호출 전, 필드 접근 전 등).

https://velog.io/@jsj3282/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%82%BC%EA%B0%81%ED%98%95%EA%B3%BC-%EC%84%A4%EC%A0%95-%EC%A0%95%EB%B3%B42-AOP1

AOP 구현체

AOP 적용 방법

  • 컴파일
  • 로드 타임
  • 런타임(스프링 AOP) : 스프링에서 빈을 만드는 시점은 런타임이다. 따라서 A 클래스의 빈을 만들 때 A라는 타입의 프록시 빈을 만들어 프록시 빈이 AOP를 실행하고 A를 호출한다.

2. 프록시 기반 AOP

스프링 AOP 특징

  • 프록시 기반의 AOP 구현체
  • 스프링 빈에만 AOP를 적용할 수 있다.
  • 모든 AOP 기능을 제공하는 것이 목적이 아니라, 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제에 대한 해결책을 제공하는 것이 목적.

프록시 패턴

https://velog.io/@jsj3282/%EC%8A%A4%ED%94%84%EB%A7%81%EC%9D%B4-%EC%82%AC%EB%9E%91%ED%95%9C-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B41

  • 왜? (기존 코드 변경 없이) 접근 제어 또는 부가 기능 추가

기존 코드

public interface EventService {

    void createEvent();
    void publishEvent();
    void deleteEvent();

}
@Service
public class SimpleEventService implements EventService{

    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    @Override
    public void publishEvent() {
        try {
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("Published an event");
    }
    public void deleteEvent(){
        System.out.println("Delete an event");
    }
}
@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    EventService eventService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        eventService.createEvent();
        eventService.publishEvent();
        eventService.deleteEvent();
    }
}
  • 기존 코드를 건드리지 않고 성능을 측정해 보자. (프록시 패턴으로)
@Service
public class SimpleEventService implements EventService{
    
    @Override
    public void createEvent() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("Created an event");
        System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("Published an event");
        System.out.println(System.currentTimeMillis() - begin);
    }
    public void deleteEvent(){ // 이 메소드에서는 성능 측정 기능을 추가하고 싶지 않다.
        System.out.println("Delete an event");
    }
}

문제는 기존 코드를 건드리지 않고 싶다는 것이다. 따라서 프록시 패턴을 사용한다.

@Primary
@Service
public class ProxySimpleEventService implements EventService{

    @Autowired
    EventService simpleEventService; // 빈의 이름에 기반해 SimpleEventService를 주입받는다.

    //@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();
    }
}

프록시 클래스가 하는 일은 의존성을 주입받은 simpleEventService 인스턴스에게 위임되었으나, 성능은 프록시 클래스에서 측정한다.

참고 : 웹 서버 모드로 실행하지 않는 방법

@SpringBootApplication
public class Springtest11Application {

    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(Springtest11Application.class);
        app.setWebApplicationType(WebApplicationType.NONE);
        app.run(args);
        //SpringApplication.run(Springtest11Application.class, args);
    }
}

문제점

  • 매번 프록시 클래스를 작성해야 하는가?
  • 여러 클래스 여러 메소드에 적용하려면?
  • 객체들 관계도 복잡하고...

그래서 등장한 것이 스프링 AOP

  • 스프링 IoC 컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용하여 여러 복잡한 문제 해결.
  • 동적 프록시: 동적으로 프록시 객체 생성하는 방법
    - 자바가 제공하는 방법은 인터페이스 기반 프록시 생성.
    - CGlib은 클래스 기반 프록시도 지원.
  • 스프링 IoC: 기존 빈을 대체하는 동적 프록시 빈을 만들어 등록 시켜준다.
    - 클라이언트 코드 변경 없음.
    - AbstractAutoProxyCreator implements BeanPostProcessor

3. @AOP

애노테이션 기반의 스프링 @AOP

의존성 추가

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

애스팩트 정의

  • @Aspect
  • 빈으로 등록해야 하니까 (컴포넌트 스캔을 사용한다면) @Component도 추가.

포인트컷 정의

  • @Pointcut(표현식)
  • 주요 표현식
    - execution
    - @annotation
    - bean
  • 포인트컷 조합
    - &&, ||, !

어드바이스 정의

  • @Before
  • @AfterReturning
  • @AfterThrowing
  • @Around : 메소드 호출 자체를 감싸고 있어서 메소드 호출 전후와 에러처리까지 할 수 있다.
@Component
@Aspect
public class PerfAspect {
    //@Around("execution(* com.example..*.EventService.*(..))") 
    //com.example 패키지에 속한 EventService에 있는 모든 메서드에 적용한다.
    //@Around("@annotation(PerfLogging)")
    //PerfLogging 애노테이션이 달려있는 메소드에만 적용한다.
    @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("bean(simpleEventService)")
    public void hello(){
        System.out.println("Hello");
    }
}
/**
 * 이 애노테이션을 사용하면 성능을 로깅해 줍니다.
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerfLogging {
}
@Service
public class SimpleEventService implements EventService{
    @PerfLogging
    @Override
    public void createEvent() {
        try {
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("Created an event");
    }

    @PerfLogging
    @Override
    public void publishEvent() {
        try {
            Thread.sleep(2000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("Published an event");
    }
    public void deleteEvent(){
        System.out.println("Delete an event");
    }
}

주의할 점은 애노테이션을 만들 때 Retention 정보를 CLASS(default) 이상으로 줘야 한다. SOURCE가 되면 컴파일 이후에 애노테이션이 사라진다.

참고

profile
이것저것 관심많은 개발자.
post-custom-banner

0개의 댓글